From 4a64a7c898aec6e42e66a665a2538a4d72985739 Mon Sep 17 00:00:00 2001 From: Toshiaki Maki Date: Wed, 3 Feb 2021 03:44:46 +0900 Subject: [PATCH 001/116] updates sample code in RSocketConnector Javadoc (#977) Signed-off-by: Toshiaki Maki --- .../main/java/io/rsocket/core/RSocketConnector.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 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 342fd9480..1c4e66477 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -61,18 +61,20 @@ *
{@code
  * import io.rsocket.transport.netty.client.TcpClientTransport;
  *
- * RSocketClient client =
- *         RSocketConnector.createRSocketClient(TcpClientTransport.create("localhost", 7000));
+ * Mono source =
+ *         RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000));
+ * RSocketClient client = RSocketClient.from(source);
  * }
* *

To customize connection settings before connecting: * *

{@code
- * RSocketClient client =
+ * Mono source =
  *         RSocketConnector.create()
  *                 .metadataMimeType("message/x.rsocket.composite-metadata.v0")
  *                 .dataMimeType("application/cbor")
- *                 .toRSocketClient(TcpClientTransport.create("localhost", 7000));
+ *                 .connect(TcpClientTransport.create("localhost", 7000));
+ * RSocketClient client = RSocketClient.from(source);
  * }
*/ public class RSocketConnector { @@ -112,7 +114,7 @@ public static RSocketConnector create() { * Static factory method to connect with default settings, effectively a shortcut for: * *
-   * RSocketConnector.create().connectWith(transport);
+   * RSocketConnector.create().connect(transport);
    * 
* * @param transport the transport of choice to connect with From 769ab2d52fc41e1e4691c7bc51ff680e25fc4c70 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 17 Feb 2021 10:33:04 +0000 Subject: [PATCH 002/116] Upgrade Reactor and Netty Closes gh-980 Signed-off-by: Rossen Stoyanchev --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 9b4dcd2f4..bd1c0b388 100644 --- a/build.gradle +++ b/build.gradle @@ -32,10 +32,10 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = 'Dysprosium-SR13' + ext['reactor-bom.version'] = 'Dysprosium-SR17' ext['logback.version'] = '1.2.3' - ext['netty-bom.version'] = '4.1.52.Final' - ext['netty-boringssl.version'] = '2.0.34.Final' + ext['netty-bom.version'] = '4.1.59.Final' + ext['netty-boringssl.version'] = '2.0.36.Final' ext['hdrhistogram.version'] = '2.1.10' ext['mockito.version'] = '3.2.0' ext['slf4j.version'] = '1.7.25' From 73ca3ffb8f4911fb44cca87240ec0bb163e01750 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 17 Feb 2021 11:19:52 +0000 Subject: [PATCH 003/116] Upgrade to Reactor 2020.0.4 Closes gh-981 Signed-off-by: Rossen Stoyanchev --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4c24872f5..40d97949a 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = '2020.0.0' + ext['reactor-bom.version'] = '2020.0.4' ext['logback.version'] = '1.2.3' ext['netty-bom.version'] = '4.1.59.Final' ext['netty-boringssl.version'] = '2.0.36.Final' From f6c88143f2ce5e001a98fda08361767d57cc2667 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 17 Feb 2021 19:01:02 +0000 Subject: [PATCH 004/116] Apply GJF + minor polishing in RSocketPool Signed-off-by: Rossen Stoyanchev --- .../io/rsocket/loadbalance/RSocketPool.java | 44 +++++++------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java index 514a5d3f4..24ed2933c 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,22 +39,18 @@ class RSocketPool extends ResolvingOperator implements CoreSubscriber> { - final DeferredResolutionRSocket deferredResolutionRSocket = new DeferredResolutionRSocket(this); - final RSocketConnector connector; - final LoadbalanceStrategy loadbalanceStrategy; - - volatile PooledRSocket[] activeSockets; - static final AtomicReferenceFieldUpdater ACTIVE_SOCKETS = AtomicReferenceFieldUpdater.newUpdater( RSocketPool.class, PooledRSocket[].class, "activeSockets"); - static final PooledRSocket[] EMPTY = new PooledRSocket[0]; static final PooledRSocket[] TERMINATED = new PooledRSocket[0]; - - volatile Subscription s; static final AtomicReferenceFieldUpdater S = AtomicReferenceFieldUpdater.newUpdater(RSocketPool.class, Subscription.class, "s"); + final DeferredResolutionRSocket deferredResolutionRSocket = new DeferredResolutionRSocket(this); + final RSocketConnector connector; + final LoadbalanceStrategy loadbalanceStrategy; + volatile PooledRSocket[] activeSockets; + volatile Subscription s; public RSocketPool( RSocketConnector connector, @@ -85,31 +81,27 @@ public void onSubscribe(Subscription s) { } } - /** - * This operation should happen rarely relatively compares the number of the {@link #select()} - * method invocations, therefore it is acceptable to have it algorithmically inefficient. The - * algorithmic complexity of this method is - * - * @param targets set which represents RSocket targets to balance on - */ @Override public void onNext(List targets) { if (isDisposed()) { return; } + // This operation should happen less frequently than calls to select() (which are per request) + // and therefore it is acceptable somewhat less efficient. + PooledRSocket[] previouslyActiveSockets; - PooledRSocket[] activeSockets; PooledRSocket[] inactiveSockets; + PooledRSocket[] socketsToUse; for (; ; ) { - HashMap rSocketSuppliersCopy = new HashMap<>(); + HashMap rSocketSuppliersCopy = new HashMap<>(targets.size()); int j = 0; for (LoadbalanceTarget target : targets) { rSocketSuppliersCopy.put(target, j++); } - // checking intersection of active RSocket with the newly received set + // Intersect current and new list of targets and find the ones to keep vs dispose previouslyActiveSockets = this.activeSockets; inactiveSockets = new PooledRSocket[previouslyActiveSockets.length]; PooledRSocket[] nextActiveSockets = @@ -141,20 +133,18 @@ public void onNext(List targets) { } } - // going though brightly new rsocket + // The remainder are the brand new targets for (LoadbalanceTarget target : rSocketSuppliersCopy.keySet()) { nextActiveSockets[activeSocketsPosition++] = new PooledRSocket(this, this.connector.connect(target.getTransport()), target); } - // shrank to actual length if (activeSocketsPosition == 0) { - activeSockets = EMPTY; + socketsToUse = EMPTY; } else { - activeSockets = Arrays.copyOf(nextActiveSockets, activeSocketsPosition); + socketsToUse = Arrays.copyOf(nextActiveSockets, activeSocketsPosition); } - - if (ACTIVE_SOCKETS.compareAndSet(this, previouslyActiveSockets, activeSockets)) { + if (ACTIVE_SOCKETS.compareAndSet(this, previouslyActiveSockets, socketsToUse)) { break; } } @@ -169,7 +159,7 @@ public void onNext(List targets) { if (isPending()) { // notifies that upstream is resolved - if (activeSockets != EMPTY) { + if (socketsToUse != EMPTY) { //noinspection ConstantConditions complete(this); } From 9baf974f6048d0dfdc98678433aa5f61b66f1f8a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 17 Feb 2021 19:05:20 +0000 Subject: [PATCH 005/116] Clean up failed loadbalance target Closes gh-958, gh-982 Signed-off-by: Rossen Stoyanchev --- .../java/io/rsocket/loadbalance/PooledRSocket.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java index 44a9334d3..1e7f09ec4 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,8 +85,8 @@ public void onError(Throwable t) { } this.doFinally(); - // terminate upstream which means retryBackoff has exhausted - this.terminate(t); + // terminate upstream (retryBackoff has exhausted) and remove from the parent target list + this.doCleanup(t); } @Override @@ -108,15 +108,15 @@ protected void doSubscribe() { @Override protected void doOnValueResolved(RSocket value) { - value.onClose().subscribe(null, t -> this.doCleanup(), this::doCleanup); + value.onClose().subscribe(null, this::doCleanup, () -> doCleanup(ON_DISPOSE)); } - void doCleanup() { + void doCleanup(Throwable t) { if (isDisposed()) { return; } - this.dispose(); + this.terminate(t); final RSocketPool parent = this.parent; for (; ; ) { From d77df7b4c5a7c77cb2dc12ddfbf87318c8b8c280 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 19 Feb 2021 14:34:09 +0000 Subject: [PATCH 006/116] fixes LoadbalanceTest issues (#983) Signed-off-by: Rossen Stoyanchev --- .../rsocket/loadbalance/LoadbalanceTest.java | 62 +++++++++++-------- .../RoundRobinLoadbalanceStrategyTest.java | 22 ++++--- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java index 966242ed3..52b4e0e13 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java @@ -12,6 +12,8 @@ import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.reactivestreams.Publisher; @@ -26,10 +28,18 @@ public class LoadbalanceTest { - @Test - public void shouldDeliverAllTheRequestsWithRoundRobinStrategy() { + @BeforeEach + void setUp() { Hooks.onErrorDropped((__) -> {}); + } + + @AfterAll + static void afterAll() { + Hooks.resetOnErrorDropped(); + } + @Test + public void shouldDeliverAllTheRequestsWithRoundRobinStrategy() throws Exception { final AtomicInteger counter = new AtomicInteger(); final ClientTransport mockTransport = Mockito.mock(ClientTransport.class); final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); @@ -76,21 +86,28 @@ public Mono fireAndForget(Payload payload) { }); Assertions.assertThat(counter.get()).isEqualTo(1000); - counter.set(0); } } @Test - public void shouldDeliverAllTheRequestsWithWightedStrategy() { - Hooks.onErrorDropped((__) -> {}); - + public void shouldDeliverAllTheRequestsWithWeightedStrategy() throws InterruptedException { final AtomicInteger counter = new AtomicInteger(); - final ClientTransport mockTransport = Mockito.mock(ClientTransport.class); - final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); - Mockito.when(rSocketConnectorMock.connect(Mockito.any(ClientTransport.class))) - .then(im -> Mono.just(new TestRSocket(new WeightedRSocket(counter)))); + final ClientTransport mockTransport1 = Mockito.mock(ClientTransport.class); + final ClientTransport mockTransport2 = Mockito.mock(ClientTransport.class); + + final LoadbalanceTarget target1 = LoadbalanceTarget.from("1", mockTransport1); + final LoadbalanceTarget target2 = LoadbalanceTarget.from("2", mockTransport2); + + final WeightedRSocket weightedRSocket1 = new WeightedRSocket(counter); + final WeightedRSocket weightedRSocket2 = new WeightedRSocket(counter); + + final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + Mockito.when(rSocketConnectorMock.connect(mockTransport1)) + .then(im -> Mono.just(new TestRSocket(weightedRSocket1))); + Mockito.when(rSocketConnectorMock.connect(mockTransport2)) + .then(im -> Mono.just(new TestRSocket(weightedRSocket2))); for (int i = 0; i < 1000; i++) { final TestPublisher> source = TestPublisher.create(); @@ -99,7 +116,11 @@ public void shouldDeliverAllTheRequestsWithWightedStrategy() { rSocketConnectorMock, source, WeightedLoadbalanceStrategy.builder() - .weightedStatsResolver(r -> (WeightedStats) r) + .weightedStatsResolver( + rsocket -> + ((PooledRSocket) rsocket).target() == target1 + ? weightedRSocket1 + : weightedRSocket2) .build()); RaceTestUtils.race( @@ -107,34 +128,27 @@ public void shouldDeliverAllTheRequestsWithWightedStrategy() { for (int j = 0; j < 1000; j++) { Mono.defer(() -> rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE)) .retry() - .subscribe(); + .subscribe(aVoid -> {}, Throwable::printStackTrace); } }, () -> { for (int j = 0; j < 100; j++) { source.next(Collections.emptyList()); - source.next(Collections.singletonList(LoadbalanceTarget.from("1", mockTransport))); - source.next( - Arrays.asList( - LoadbalanceTarget.from("1", mockTransport), - LoadbalanceTarget.from("2", mockTransport))); - source.next(Collections.singletonList(LoadbalanceTarget.from("1", mockTransport))); - source.next(Collections.singletonList(LoadbalanceTarget.from("2", mockTransport))); + source.next(Collections.singletonList(target1)); + source.next(Arrays.asList(target1, target2)).next(Collections.singletonList(target1)); + source.next(Collections.singletonList(target2)); source.next(Collections.emptyList()); - source.next(Collections.singletonList(LoadbalanceTarget.from("2", mockTransport))); + source.next(Collections.singletonList(target2)); } }); Assertions.assertThat(counter.get()).isEqualTo(1000); - counter.set(0); } } @Test public void ensureRSocketIsCleanedFromThePoolIfSourceRSocketIsDisposed() { - Hooks.onErrorDropped((__) -> {}); - final AtomicInteger counter = new AtomicInteger(); final ClientTransport mockTransport = Mockito.mock(ClientTransport.class); final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); @@ -179,8 +193,6 @@ public Mono fireAndForget(Payload payload) { @Test public void ensureContextIsPropagatedCorrectlyForRequestChannel() { - Hooks.onErrorDropped((__) -> {}); - final AtomicInteger counter = new AtomicInteger(); final ClientTransport mockTransport = Mockito.mock(ClientTransport.class); final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategyTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategyTest.java index c4fdcbab9..a177bf2f0 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategyTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategyTest.java @@ -11,6 +11,8 @@ import java.util.concurrent.atomic.AtomicInteger; import org.assertj.core.api.Assertions; import org.assertj.core.data.Offset; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import reactor.core.publisher.Hooks; @@ -19,10 +21,18 @@ public class RoundRobinLoadbalanceStrategyTest { - @Test - public void shouldDeliverValuesProportionally() { + @BeforeEach + void setUp() { Hooks.onErrorDropped((__) -> {}); + } + + @AfterAll + static void afterAll() { + Hooks.resetOnErrorDropped(); + } + @Test + public void shouldDeliverValuesProportionally() { final AtomicInteger counter1 = new AtomicInteger(); final AtomicInteger counter2 = new AtomicInteger(); final ClientTransport mockTransport = Mockito.mock(ClientTransport.class); @@ -71,8 +81,6 @@ public Mono fireAndForget(Payload payload) { @Test public void shouldDeliverValuesToNewlyConnectedSockets() { - Hooks.onErrorDropped((__) -> {}); - final AtomicInteger counter1 = new AtomicInteger(); final AtomicInteger counter2 = new AtomicInteger(); final ClientTransport mockTransport1 = Mockito.mock(ClientTransport.class); @@ -104,7 +112,7 @@ public Mono fireAndForget(Payload payload) { rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); } - source.next(Arrays.asList(LoadbalanceTarget.from("1", mockTransport1))); + source.next(Collections.singletonList(LoadbalanceTarget.from("1", mockTransport1))); Assertions.assertThat(counter1.get()).isCloseTo(1000, Offset.offset(1)); @@ -114,7 +122,7 @@ public Mono fireAndForget(Payload payload) { rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); } - source.next(Arrays.asList(LoadbalanceTarget.from("1", mockTransport1))); + source.next(Collections.singletonList(LoadbalanceTarget.from("1", mockTransport1))); Assertions.assertThat(counter1.get()).isCloseTo(2000, Offset.offset(1)); @@ -130,7 +138,7 @@ public Mono fireAndForget(Payload payload) { Assertions.assertThat(counter1.get()).isCloseTo(2500, Offset.offset(1)); Assertions.assertThat(counter2.get()).isCloseTo(500, Offset.offset(1)); - source.next(Arrays.asList(LoadbalanceTarget.from("2", mockTransport1))); + source.next(Collections.singletonList(LoadbalanceTarget.from("2", mockTransport1))); for (int j = 0; j < 1000; j++) { rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); From 0601f8816259a5f5bb6a3cb38a7106255a2dcab2 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 25 Feb 2021 17:15:13 +0200 Subject: [PATCH 007/116] fixes OverflowException if UnicstProcessr request and onNext race (#985) --- .../java/io/rsocket/core/RequestOperator.java | 10 ++++- .../io/rsocket/core/RSocketRequesterTest.java | 45 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java b/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java index 38c392408..09eeadb6c 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java @@ -93,7 +93,6 @@ public Context currentContext() { @Override public void request(long n) { - this.s.request(n); if (!firstRequest) { try { this.hookOnRemainingRequests(n); @@ -115,6 +114,15 @@ public void request(long n) { if (firstLoop) { firstLoop = false; try { + // since in all the scenarios where RequestOperator is used, the + // CorePublisher is either UnicastProcessor or UnicastProcessor.next() + // we are free to propagate unbounded demand to that publisher right after + // the first request happens. UnicastProcessor is only there to allow sending signals from + // the + // connection to a real subscriber and does not have to check the real demand + // For more info see + // https://github.com/rsocket/rsocket/blob/master/Protocol.md#handling-the-unexpected + this.s.request(Long.MAX_VALUE); this.hookOnFirstRequest(n); } catch (Throwable throwable) { onError(throwable); 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 45770d375..1ce68cfeb 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -1142,6 +1142,51 @@ public void testWorkaround858() { rule.assertHasNoLeaks(); } + @ParameterizedTest + @ValueSource(strings = {"stream", "channel"}) + // see https://github.com/rsocket/rsocket-java/issues/959 + public void testWorkaround959(String type) { + for (int i = 1; i < 20000; i += 2) { + ByteBuf buffer = rule.alloc().buffer(); + buffer.writeCharSequence("test", CharsetUtil.UTF_8); + + final AssertSubscriber assertSubscriber = new AssertSubscriber<>(3); + if (type.equals("stream")) { + rule.socket.requestStream(ByteBufPayload.create(buffer)).subscribe(assertSubscriber); + } else if (type.equals("channel")) { + rule.socket + .requestChannel(Flux.just(ByteBufPayload.create(buffer))) + .subscribe(assertSubscriber); + } + + final ByteBuf payloadFrame = + PayloadFrameCodec.encode( + rule.alloc(), i, false, false, true, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); + + RaceTestUtils.race( + () -> { + rule.connection.addToReceivedBuffer(payloadFrame.copy()); + rule.connection.addToReceivedBuffer(payloadFrame.copy()); + rule.connection.addToReceivedBuffer(payloadFrame); + }, + () -> { + assertSubscriber.request(1); + assertSubscriber.request(1); + assertSubscriber.request(1); + }); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); + + Assertions.assertThat(rule.socket.isDisposed()).isFalse(); + + assertSubscriber.values().forEach(ReferenceCountUtil::safeRelease); + assertSubscriber.assertNoError(); + + rule.connection.clearSendReceiveBuffers(); + rule.assertHasNoLeaks(); + } + } + public static class ClientSocketRule extends AbstractSocketRule { @Override protected RSocketRequester newRSocket() { From 645c1f6bb2024d1dcd0499fb90c0fce1a2cc95d6 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 26 Feb 2021 20:14:48 +0200 Subject: [PATCH 008/116] fixes deadlock on multiconsumer clear/poll in UnboundedProcessor (#990) --- .../rsocket/internal/UnboundedProcessor.java | 70 ++++-- .../internal/UnboundedProcessorTest.java | 218 +++++++++++------- .../internal/subscriber/AssertSubscriber.java | 85 +++++-- 3 files changed, 263 insertions(+), 110 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 cb8b5d63d..d2a438dfd 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -20,6 +20,7 @@ import io.rsocket.internal.jctools.queues.MpscUnboundedArrayQueue; import java.util.Objects; import java.util.Queue; +import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Subscriber; @@ -55,6 +56,8 @@ public final class UnboundedProcessor extends FluxProcessor volatile boolean cancelled; + volatile boolean terminated; + volatile int once; @SuppressWarnings("rawtypes") @@ -124,6 +127,9 @@ void drainRegular(Subscriber a) { } if (checkTerminated(d, empty, a)) { + if (!empty) { + release(t); + } return; } @@ -159,7 +165,9 @@ void drainFused(Subscriber a) { for (; ; ) { if (cancelled) { - this.clear(); + if (terminated) { + this.clear(); + } hasDownstream = false; return; } @@ -189,7 +197,7 @@ void drainFused(Subscriber a) { public void drain() { if (WIP.getAndIncrement(this) != 0) { - if (cancelled) { + if ((!outputFused && cancelled) || terminated) { this.clear(); } return; @@ -350,7 +358,9 @@ public void cancel() { cancelled = true; if (WIP.getAndIncrement(this) == 0) { - this.clear(); + if (!outputFused || terminated) { + this.clear(); + } hasDownstream = false; } } @@ -377,6 +387,7 @@ public boolean isEmpty() { @Override public void clear() { + terminated = true; if (DISCARD_GUARD.getAndIncrement(this) != 0) { return; } @@ -384,17 +395,12 @@ public void clear() { int missed = 1; for (; ; ) { - while (!queue.isEmpty()) { - T t = queue.poll(); - if (t != null) { - release(t); - } + T t; + while ((t = queue.poll()) != null) { + release(t); } - while (!priorityQueue.isEmpty()) { - T t = priorityQueue.poll(); - if (t != null) { - release(t); - } + while ((t = priorityQueue.poll()) != null) { + release(t); } missed = DISCARD_GUARD.addAndGet(this, -missed); @@ -415,7 +421,43 @@ public int requestFusion(int requestedMode) { @Override public void dispose() { - cancel(); + if (cancelled) { + return; + } + + error = new CancellationException("Disposed"); + done = true; + + boolean once = true; + if (WIP.getAndIncrement(this) == 0) { + cancelled = true; + int m = 1; + for (; ; ) { + final CoreSubscriber a = this.actual; + + if (!outputFused || terminated) { + clear(); + } + + if (a != null && once) { + try { + a.onError(error); + } catch (Throwable ignored) { + } + } + + cancelled = true; + once = false; + + int wip = this.wip; + if (wip == m) { + break; + } + m = wip; + } + + hasDownstream = false; + } } @Override 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 7bf975543..552afb70c 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,115 +16,173 @@ 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; -import org.junit.Test; +import static org.assertj.core.api.Assertions.assertThat; + +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.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.internal.subscriber.AssertSubscriber; +import java.time.Duration; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.Fuseable; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Operators; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; +import reactor.test.util.RaceTestUtils; public class UnboundedProcessorTest { - @Test - public void testOnNextBeforeSubscribe_10() { - testOnNextBeforeSubscribeN(10); - } - - @Test - public void testOnNextBeforeSubscribe_100() { - testOnNextBeforeSubscribeN(100); - } - @Test - public void testOnNextBeforeSubscribe_10_000() { - testOnNextBeforeSubscribeN(10_000); + @BeforeAll + public static void setup() { + Hooks.onErrorDropped(__ -> {}); } - @Test - public void testOnNextBeforeSubscribe_100_000() { - testOnNextBeforeSubscribeN(100_000); - } - - @Test - public void testOnNextBeforeSubscribe_1_000_000() { - testOnNextBeforeSubscribeN(1_000_000); - } - - @Test - public void testOnNextBeforeSubscribe_10_000_000() { - testOnNextBeforeSubscribeN(10_000_000); + public static void teardown() { + Hooks.resetOnErrorDropped(); } + @ParameterizedTest( + name = + "Test that emitting {0} onNext before subscribe and requestN should deliver all the signals once the subscriber is available") + @ValueSource(ints = {10, 100, 10_000, 100_000, 1_000_000, 10_000_000}) public void testOnNextBeforeSubscribeN(int n) { - UnboundedProcessor processor = new UnboundedProcessor<>(); + UnboundedProcessor processor = new UnboundedProcessor<>(); for (int i = 0; i < n; i++) { - processor.onNext(EmptyPayload.INSTANCE); + processor.onNext(Unpooled.EMPTY_BUFFER); } processor.onComplete(); - long count = processor.count().block(); - - Assert.assertEquals(n, count); - } - - @Test - public void testOnNextAfterSubscribe_10() throws Exception { - testOnNextAfterSubscribeN(10); - } - - @Test - public void testOnNextAfterSubscribe_100() throws Exception { - testOnNextAfterSubscribeN(100); + StepVerifier.create(processor.count()).expectNext(Long.valueOf(n)).verifyComplete(); } - @Test - public void testOnNextAfterSubscribe_1000() throws Exception { - testOnNextAfterSubscribeN(1000); - } + @ParameterizedTest( + name = + "Test that emitting {0} onNext after subscribe and requestN should deliver all the signals") + @ValueSource(ints = {10, 100, 10_000}) + public void testOnNextAfterSubscribeN(int n) { + UnboundedProcessor processor = new UnboundedProcessor<>(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(); - @Test - public void testPrioritizedSending() { - UnboundedProcessor processor = new UnboundedProcessor<>(); + processor.subscribe(assertSubscriber); - for (int i = 0; i < 1000; i++) { - processor.onNext(EmptyPayload.INSTANCE); + for (int i = 0; i < n; i++) { + processor.onNext(Unpooled.EMPTY_BUFFER); } - processor.onNextPrioritized(ByteBufPayload.create("test")); - - Payload closestPayload = processor.next().block(); - - Assert.assertEquals(closestPayload.getDataUtf8(), "test"); + assertSubscriber.awaitAndAssertNextValueCount(n); } - @Test - public void testPrioritizedFused() { - UnboundedProcessor processor = new UnboundedProcessor<>(); + @ParameterizedTest( + name = + "Test that prioritized value sending deliver prioritized signals before the others mode[fusionEnabled={0}]") + @ValueSource(booleans = {true, false}) + public void testPrioritizedSending(boolean fusedCase) { + UnboundedProcessor processor = new UnboundedProcessor<>(); for (int i = 0; i < 1000; i++) { - processor.onNext(EmptyPayload.INSTANCE); + processor.onNext(Unpooled.EMPTY_BUFFER); } - processor.onNextPrioritized(ByteBufPayload.create("test")); + processor.onNextPrioritized(Unpooled.copiedBuffer("test", CharsetUtil.UTF_8)); - Payload closestPayload = processor.poll(); - - Assert.assertEquals(closestPayload.getDataUtf8(), "test"); + assertThat(fusedCase ? processor.poll() : processor.next().block()) + .isNotNull() + .extracting(bb -> bb.toString(CharsetUtil.UTF_8)) + .isEqualTo("test"); } - public void testOnNextAfterSubscribeN(int n) throws Exception { - CountDownLatch latch = new CountDownLatch(n); - UnboundedProcessor processor = new UnboundedProcessor<>(); - processor.log().doOnNext(integer -> latch.countDown()).subscribe(); - - for (int i = 0; i < n; i++) { - System.out.println("onNexting -> " + i); - processor.onNext(EmptyPayload.INSTANCE); + @ParameterizedTest( + name = + "Ensures that racing between onNext | dispose | cancel | request(n) will not cause any issues and leaks; mode[fusionEnabled={0}]") + @ValueSource(booleans = {true, false}) + public void ensureUnboundedProcessorDisposesQueueProperly(boolean withFusionEnabled) { + final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + for (int i = 0; i < 100000; i++) { + final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); + + final ByteBuf buffer1 = allocator.buffer(1); + final ByteBuf buffer2 = allocator.buffer(2); + + final AssertSubscriber assertSubscriber = + new AssertSubscriber(0) + .requestedFusionMode(withFusionEnabled ? Fuseable.ANY : Fuseable.NONE); + + unboundedProcessor.subscribe(assertSubscriber); + + RaceTestUtils.race( + () -> + RaceTestUtils.race( + () -> + RaceTestUtils.race( + () -> { + unboundedProcessor.onNext(buffer1); + unboundedProcessor.onNext(buffer2); + }, + unboundedProcessor::dispose, + Schedulers.elastic()), + assertSubscriber::cancel, + Schedulers.elastic()), + () -> { + assertSubscriber.request(1); + assertSubscriber.request(1); + }, + Schedulers.elastic()); + + assertSubscriber.values().forEach(ReferenceCountUtil::safeRelease); + + allocator.assertHasNoLeaks(); } + } - processor.drain(); - - latch.await(); + @RepeatedTest( + name = + "Ensures that racing between onNext + dispose | downstream async drain should not cause any issues and leaks", + value = 100000) + @Timeout(60) + public void ensuresAsyncFusionAndDisposureHasNoDeadlock() { + // TODO: enable leaks tracking + // final LeaksTrackingByteBufAllocator allocator = + // LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); + + // final ByteBuf buffer1 = allocator.buffer(1); + // final ByteBuf buffer2 = allocator.buffer(2); + + final AssertSubscriber assertSubscriber = + new AssertSubscriber<>(Operators.enableOnDiscard(null, ReferenceCountUtil::safeRelease)); + + unboundedProcessor.publishOn(Schedulers.parallel()).subscribe(assertSubscriber); + + RaceTestUtils.race( + () -> { + // unboundedProcessor.onNext(buffer1); + // unboundedProcessor.onNext(buffer2); + unboundedProcessor.onNext(Unpooled.EMPTY_BUFFER); + unboundedProcessor.onNext(Unpooled.EMPTY_BUFFER); + unboundedProcessor.onNext(Unpooled.EMPTY_BUFFER); + unboundedProcessor.onNext(Unpooled.EMPTY_BUFFER); + unboundedProcessor.onNext(Unpooled.EMPTY_BUFFER); + unboundedProcessor.onNext(Unpooled.EMPTY_BUFFER); + unboundedProcessor.dispose(); + }, + unboundedProcessor::dispose); + + assertSubscriber + .await(Duration.ofSeconds(50)) + .values() + .forEach(ReferenceCountUtil::safeRelease); + + // allocator.assertHasNoLeaks(); } } 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 index 84a589a8d..83d420d90 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/subscriber/AssertSubscriber.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/subscriber/AssertSubscriber.java @@ -26,6 +26,7 @@ import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.BooleanSupplier; @@ -86,6 +87,10 @@ public class AssertSubscriber implements CoreSubscriber, Subscription { private static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(AssertSubscriber.class, "requested"); + @SuppressWarnings("rawtypes") + private static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(AssertSubscriber.class, "wip"); + @SuppressWarnings("rawtypes") private static final AtomicReferenceFieldUpdater NEXT_VALUES = AtomicReferenceFieldUpdater.newUpdater(AssertSubscriber.class, List.class, "values"); @@ -100,10 +105,14 @@ public class AssertSubscriber implements CoreSubscriber, Subscription { private final CountDownLatch cdl = new CountDownLatch(1); + volatile boolean done; + volatile Subscription s; volatile long requested; + volatile int wip; + volatile List values = new LinkedList<>(); /** The fusion mode to request. */ @@ -377,7 +386,7 @@ public final AssertSubscriber assertError(Class clazz) { } } if (s > 1) { - throw new AssertionError("Multiple errors: " + s, null); + throw new AssertionError("Multiple errors: " + errors, null); } return this; } @@ -854,6 +863,10 @@ public void cancel() { a = S.getAndSet(this, Operators.cancelledSubscription()); if (a != null && a != Operators.cancelledSubscription()) { a.cancel(); + + if (establishedFusionMode == Fuseable.ASYNC && WIP.getAndIncrement(this) == 0) { + qs.clear(); + } } } } @@ -868,37 +881,77 @@ public final boolean isTerminated() { @Override public void onComplete() { + done = true; completionCount++; + + if (establishedFusionMode == Fuseable.ASYNC) { + drain(); + return; + } + cdl.countDown(); } @Override public void onError(Throwable t) { + done = true; errors.add(t); + + if (establishedFusionMode == Fuseable.ASYNC) { + drain(); + return; + } + 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; - } + drain(); + } else { + valueCount++; + if (valuesStorage) { + List nextValuesSnapshot; + for (; ; ) { + nextValuesSnapshot = values; + nextValuesSnapshot.add(t); + if (NEXT_VALUES.compareAndSet(this, nextValuesSnapshot, nextValuesSnapshot)) { + break; } } } - } else { + } + } + + void drain() { + if (this.wip != 0 || WIP.getAndIncrement(this) != 0) { + if (isCancelled()) { + qs.clear(); + } + return; + } + + T t; + int m = 1; + for (; ; ) { + if (isCancelled()) { + qs.clear(); + break; + } + boolean done = this.done; + t = qs.poll(); + if (t == null) { + if (done) { + cdl.countDown(); + return; + } + m = WIP.addAndGet(this, -m); + if (m == 0) { + break; + } + continue; + } valueCount++; if (valuesStorage) { List nextValuesSnapshot; From cad188e72990b1d3f7ac491dc6e8e889100dbc8e Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Fri, 26 Feb 2021 13:46:32 -0500 Subject: [PATCH 009/116] adds proxy RSocket for WeightedStatsRequestInterceptor (#976) --- .../io/rsocket/loadbalance/WeightedStats.java | 15 +++++- .../WeightedStatsRSocketProxy.java | 47 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRSocketProxy.java diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java index 7f2891bba..372d7a77e 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java @@ -1,7 +1,9 @@ package io.rsocket.loadbalance; +import io.rsocket.RSocket; + /** - * Representation of stats used by the {@link WeightedLoadbalanceStrategy} + * Representation of stats used by the {@link WeightedLoadbalanceStrategy}. * * @since 1.1 */ @@ -16,4 +18,15 @@ public interface WeightedStats { double predictedLatency(); double weightedAvailability(); + + /** + * Wraps an RSocket with a proxy that implements WeightedStats. + * + * @param rsocket the RSocket to proxy. + * @return the wrapped RSocket. + * @since 1.1.1 + */ + default RSocket wrap(RSocket rsocket) { + return new WeightedStatsRSocketProxy(rsocket, this); + } } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRSocketProxy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRSocketProxy.java new file mode 100644 index 000000000..1103d2185 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRSocketProxy.java @@ -0,0 +1,47 @@ +package io.rsocket.loadbalance; + +import io.rsocket.RSocket; +import io.rsocket.util.RSocketProxy; + +/** + * {@link RSocketProxy} that implements {@link WeightedStats} and delegates to an existing {@link + * WeightedStats} instance. + */ +class WeightedStatsRSocketProxy extends RSocketProxy implements WeightedStats { + + private final WeightedStats weightedStats; + + public WeightedStatsRSocketProxy(RSocket source, WeightedStats weightedStats) { + super(source); + this.weightedStats = weightedStats; + } + + @Override + public double higherQuantileLatency() { + return this.weightedStats.higherQuantileLatency(); + } + + @Override + public double lowerQuantileLatency() { + return this.weightedStats.lowerQuantileLatency(); + } + + @Override + public int pending() { + return this.weightedStats.pending(); + } + + @Override + public double predictedLatency() { + return this.weightedStats.predictedLatency(); + } + + @Override + public double weightedAvailability() { + return this.weightedStats.weightedAvailability(); + } + + public WeightedStats getDelegate() { + return this.weightedStats; + } +} From 61652c30d6473ddd133fecea01b03d0844d16727 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 26 Feb 2021 20:14:48 +0200 Subject: [PATCH 010/116] fixes deadlock on multiconsumer clear/poll in UnboundedProcessor (#990) Signed-off-by: Oleh Dokuka --- .../test/java/io/rsocket/internal/UnboundedProcessorTest.java | 2 ++ 1 file changed, 2 insertions(+) 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 552afb70c..271c08664 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java @@ -26,6 +26,7 @@ import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.internal.subscriber.AssertSubscriber; import java.time.Duration; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Timeout; @@ -45,6 +46,7 @@ public static void setup() { Hooks.onErrorDropped(__ -> {}); } + @AfterAll public static void teardown() { Hooks.resetOnErrorDropped(); } From 11c6a4a614e857b0e19d3902ca0b06d9d6e0c127 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 27 Feb 2021 14:45:48 +0200 Subject: [PATCH 011/116] improves UnboundedProcessor impl to ensure no leaks and deadlocks Signed-off-by: Oleh Dokuka --- .../rsocket/internal/UnboundedProcessor.java | 267 +++++++++++++----- .../internal/UnboundedProcessorTest.java | 6 +- 2 files changed, 204 insertions(+), 69 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 61a96f915..0df8f1d34 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -20,6 +20,7 @@ import io.rsocket.internal.jctools.queues.MpscUnboundedArrayQueue; import java.util.Objects; import java.util.Queue; +import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Subscriber; @@ -50,12 +51,14 @@ public final class UnboundedProcessor extends FluxProcessor static final long STATE_TERMINATED = 0b1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; - static final long FLAG_CANCELLED = + static final long FLAG_DISPOSED = 0b0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; - static final long FLAG_SUBSCRIBED_ONCE = + static final long FLAG_CANCELLED = 0b0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; - static final long MAX_VALUE = - 0b0001_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111L; + static final long FLAG_SUBSCRIBED_ONCE = + 0b0001_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long MAX_WIP_VALUE = + 0b0000_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111L; volatile long state; @@ -86,18 +89,18 @@ public int getBufferSize() { @Override public Object scanUnsafe(Attr key) { - if (Attr.BUFFERED == key) return queue.size(); + if (Attr.BUFFERED == key) return this.queue.size() + this.priorityQueue.size(); if (Attr.PREFETCH == key) return Integer.MAX_VALUE; return super.scanUnsafe(key); } public void onNextPrioritized(ByteBuf t) { - if (done) { + if (this.done) { release(t); return; } - if (!priorityQueue.offer(t)) { + if (!this.priorityQueue.offer(t)) { Throwable ex = Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); onError(Operators.onOperatorError(null, ex, t, currentContext())); @@ -110,12 +113,12 @@ public void onNextPrioritized(ByteBuf t) { @Override public void onNext(ByteBuf t) { - if (done) { + if (this.done) { release(t); return; } - if (!queue.offer(t)) { + if (!this.queue.offer(t)) { Throwable ex = Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); onError(Operators.onOperatorError(null, ex, t, currentContext())); @@ -128,24 +131,24 @@ public void onNext(ByteBuf t) { @Override public void onError(Throwable t) { - if (done) { + if (this.done) { Operators.onErrorDropped(t, currentContext()); return; } - error = t; - done = true; + this.error = t; + this.done = true; drain(); } @Override public void onComplete() { - if (done) { + if (this.done) { return; } - done = true; + this.done = true; drain(); } @@ -159,8 +162,7 @@ public void subscribe(CoreSubscriber actual) { drain(); } else { Operators.error( - actual, - new IllegalStateException("UnboundedProcessor " + "allows only a single Subscriber")); + actual, new IllegalStateException("UnboundedProcessor allows only a single Subscriber")); } } @@ -175,30 +177,46 @@ void drain() { return; } - final boolean outputFused = this.outputFused; - if (isCancelled(previousState) && !outputFused) { - clearAndTerminate(this); - return; - } - long expectedState = previousState + 1; for (; ; ) { - final Subscriber a = actual; - if (a != null) { + if (isSubscribedOnce(expectedState)) { + final boolean outputFused = this.outputFused; + final Subscriber a = this.actual; + if (outputFused) { + if (isCancelled(expectedState)) { + return; + } + + if (isDisposed(expectedState)) { + a.onError(new CancellationException("Disposed")); + return; + } + drainFused(expectedState, a); } else { + if (isCancelled(expectedState)) { + clearAndTerminate(this); + return; + } + + if (isDisposed(expectedState)) { + clearAndTerminate(this); + a.onError(new CancellationException("Disposed")); + return; + } + drainRegular(expectedState, a); } return; + } else { + if (isCancelled(expectedState) || isDisposed(expectedState)) { + clearAndTerminate(this); + return; + } } expectedState = wipRemoveMissing(this, expectedState); - if (isCancelled(expectedState)) { - clearAndTerminate(this); - return; - } - if (!isWorkInProgress(expectedState)) { return; } @@ -206,15 +224,20 @@ void drain() { } void drainRegular(long expectedState, Subscriber a) { - final Queue q = queue; - final Queue pq = priorityQueue; + final Queue q = this.queue; + final Queue pq = this.priorityQueue; for (; ; ) { - long r = requested; + long r = this.requested; long e = 0L; while (r != e) { + // done has to be read before queue.poll to ensure there was no racing: + // Thread1: <#drain>: queue.poll(null) --------------------> this.done(true) + // Thread2: ------------------> <#onNext(V)> --> <#onComplete()> + boolean done = this.done; + ByteBuf t; boolean empty; @@ -226,7 +249,7 @@ void drainRegular(long expectedState, Subscriber a) { empty = t == null; } - if (checkTerminated(empty, a)) { + if (checkTerminated(done, empty, a)) { if (!empty) { release(t); } @@ -243,7 +266,10 @@ void drainRegular(long expectedState, Subscriber a) { } if (r == e) { - if (checkTerminated(q.isEmpty() && pq.isEmpty(), a)) { + // done has to be read before queue.isEmpty to ensure there was no racing: + // Thread1: <#drain>: queue.isEmpty(true) --------------------> this.done(true) + // Thread2: --------------------> <#onNext(V)> ---> <#onComplete()> + if (checkTerminated(this.done, q.isEmpty() && pq.isEmpty(), a)) { return; } } @@ -258,6 +284,12 @@ void drainRegular(long expectedState, Subscriber a) { return; } + if (isDisposed(expectedState)) { + clearAndTerminate(this); + a.onError(new CancellationException("Disposed")); + return; + } + if (!isWorkInProgress(expectedState)) { break; } @@ -266,12 +298,14 @@ void drainRegular(long expectedState, Subscriber a) { void drainFused(long expectedState, Subscriber a) { for (; ; ) { - boolean d = done; + // done has to be read before queue.poll to ensure there was no racing: + // Thread1: <#drain>: queue.poll(null) --------------------> this.done(true) + boolean d = this.done; a.onNext(null); if (d) { - Throwable ex = error; + Throwable ex = this.error; if (ex != null) { a.onError(ex); } else { @@ -285,21 +319,32 @@ void drainFused(long expectedState, Subscriber a) { return; } + if (isDisposed(expectedState)) { + a.onError(new CancellationException("Disposed")); + return; + } + if (!isWorkInProgress(expectedState)) { break; } } } - boolean checkTerminated(boolean empty, Subscriber a) { + boolean checkTerminated(boolean done, boolean empty, Subscriber a) { final long state = this.state; if (isCancelled(state)) { clearAndTerminate(this); return true; } + if (isDisposed(state)) { + clearAndTerminate(this); + a.onError(new CancellationException("Disposed")); + return true; + } + if (done && empty) { - Throwable e = error; + Throwable e = this.error; if (e != null) { a.onError(e); } else { @@ -315,7 +360,7 @@ boolean checkTerminated(boolean empty, Subscriber a) { @Override public void onSubscribe(Subscription s) { final long state = this.state; - if (done || isTerminated(state) || isCancelled(state)) { + if (this.done || isTerminated(state) || isCancelled(state) || isDisposed(state)) { s.cancel(); } else { s.request(Long.MAX_VALUE); @@ -352,16 +397,28 @@ public void cancel() { return; } - if (outputFused) { - return; + if (!this.outputFused) { + clearAndTerminate(this); } + } - final long state = wipIncrement(this); - if (isWorkInProgress(state)) { + @Override + public void dispose() { + final long previousState = markDisposed(this); + if (isTerminated(previousState) + || isCancelled(previousState) + || isDisposed(previousState) + || isWorkInProgress(previousState)) { return; } - clearAndTerminate(this); + if (!this.outputFused) { + clearAndTerminate(this); + } + + if (isSubscribedOnce(previousState)) { + this.actual.onError(new CancellationException("Disposed")); + } } @Override @@ -371,9 +428,16 @@ public ByteBuf poll() { if (!pq.isEmpty()) { return pq.poll(); } - return queue.poll(); + return this.queue.poll(); } + /** + * Clears all elements from queues and set state to terminate. This method MUST be called only by + * the downstream subscriber which has enabled {@link Fuseable#ASYNC} fusion with the given {@link + * UnboundedProcessor} and is and indicator that the downstream is done with draining, it has + * observed any terminal signal (ON_COMPLETE or ON_ERROR or CANCEL) and will never be interacting + * with SingleConsumer queue anymore. + */ @Override public void clear() { clearAndTerminate(this); @@ -411,46 +475,41 @@ void clearUnsafely() { @Override public int size() { - return priorityQueue.size() + queue.size(); + return this.priorityQueue.size() + this.queue.size(); } @Override public boolean isEmpty() { - return priorityQueue.isEmpty() && queue.isEmpty(); + return this.priorityQueue.isEmpty() && this.queue.isEmpty(); } @Override public int requestFusion(int requestedMode) { if ((requestedMode & Fuseable.ASYNC) != 0) { - outputFused = true; + this.outputFused = true; return Fuseable.ASYNC; } return Fuseable.NONE; } - @Override - public void dispose() { - cancel(); - } - @Override public boolean isDisposed() { final long state = this.state; - return isTerminated(state) || isCancelled(state) || done; + return isTerminated(state) || isCancelled(state) || isDisposed(state) || this.done; } @Override public boolean isTerminated() { final long state = this.state; - return isTerminated(state) || done; + return isTerminated(state) || this.done; } @Override @Nullable public Throwable getError() { final long state = this.state; - if (isTerminated(state) || done) { - return error; + if (isTerminated(state) || this.done) { + return this.error; } else { return null; } @@ -463,7 +522,7 @@ public long downstreamCount() { @Override public boolean hasDownstreams() { - return (state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE && actual != null; + return (this.state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE && this.actual != null; } static void release(ByteBuf byteBuf) { @@ -476,6 +535,12 @@ static void release(ByteBuf byteBuf) { } } + /** + * Tries to set {@link #FLAG_SUBSCRIBED_ONCE} flag if it was not set before and if state is not + * {@link #STATE_TERMINATED} and flags {@link #FLAG_CANCELLED} or {@link #FLAG_DISPOSED} are unset + * + * @return {@code true} if {@link #FLAG_SUBSCRIBED_ONCE} was successfully set + */ static boolean markSubscribedOnce(UnboundedProcessor instance) { for (; ; ) { long state = instance.state; @@ -485,7 +550,8 @@ static boolean markSubscribedOnce(UnboundedProcessor instance) { } if ((state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE - || (state & FLAG_CANCELLED) == FLAG_CANCELLED) { + || (state & FLAG_CANCELLED) == FLAG_CANCELLED + || (state & FLAG_DISPOSED) == FLAG_DISPOSED) { return false; } @@ -495,6 +561,12 @@ static boolean markSubscribedOnce(UnboundedProcessor instance) { } } + /** + * Tries to set {@link #FLAG_CANCELLED} flag if it was not set before and if state is not {@link + * #STATE_TERMINATED}. Also, this method increments number of work in progress (WIP) + * + * @return {@code true} if {@link #FLAG_CANCELLED} was successfully set + */ static boolean markCancelled(UnboundedProcessor instance) { for (; ; ) { long state = instance.state; @@ -507,12 +579,53 @@ static boolean markCancelled(UnboundedProcessor instance) { return false; } - if (STATE.compareAndSet(instance, state, state | FLAG_CANCELLED)) { - return true; + long nextState = state + 1; + if ((nextState & MAX_WIP_VALUE) == 0) { + nextState = state; + } + + if (STATE.compareAndSet(instance, state, nextState | FLAG_CANCELLED)) { + return !isWorkInProgress(state); + } + } + } + + /** + * Tries to set {@link #FLAG_DISPOSED} flag if it was not set before and if state is not {@link + * #STATE_TERMINATED} and flags {@link #FLAG_CANCELLED} are unset. Also, this method increments + * number of work in progress (WIP) + * + * @return previous state + */ + static long markDisposed(UnboundedProcessor instance) { + for (; ; ) { + long state = instance.state; + + if (state == STATE_TERMINATED) { + return STATE_TERMINATED; + } + + if ((state & FLAG_CANCELLED) == FLAG_CANCELLED || (state & FLAG_DISPOSED) == FLAG_DISPOSED) { + return state; + } + + long nextState = state + 1; + if ((nextState & MAX_WIP_VALUE) == 0) { + nextState = state; + } + + if (STATE.compareAndSet(instance, state, nextState | FLAG_DISPOSED)) { + return state; } } } + /** + * Tries to increment the amount of work in progress (max value is {@link #MAX_WIP_VALUE} on the + * given state. Fails if state is {@link #STATE_TERMINATED}. + * + * @return previous state + */ static long wipIncrement(UnboundedProcessor instance) { for (; ; ) { long state = instance.state; @@ -522,7 +635,7 @@ static long wipIncrement(UnboundedProcessor instance) { } final long nextState = state + 1; - if ((nextState & MAX_VALUE) == 0) { + if ((nextState & MAX_WIP_VALUE) == 0) { return state; } @@ -532,8 +645,15 @@ static long wipIncrement(UnboundedProcessor instance) { } } + /** + * Tries to decrement the amount of work in progress by the given amount on the given state. Fails + * if state is {@link #STATE_TERMINATED} or it has flags {@link #FLAG_CANCELLED} or {@link + * #FLAG_DISPOSED} set. + * + * @return state after changing WIP or current state if update failed + */ static long wipRemoveMissing(UnboundedProcessor instance, long previousState) { - long missed = previousState & MAX_VALUE; + long missed = previousState & MAX_WIP_VALUE; boolean outputFused = instance.outputFused; for (; ; ) { long state = instance.state; @@ -542,7 +662,9 @@ static long wipRemoveMissing(UnboundedProcessor instance, long previousState) { return STATE_TERMINATED; } - if (!outputFused && (state & FLAG_CANCELLED) == FLAG_CANCELLED) { + if (!outputFused + && ((state & FLAG_CANCELLED) == FLAG_CANCELLED + || (state & FLAG_DISPOSED) == FLAG_DISPOSED)) { return state; } @@ -553,11 +675,20 @@ static long wipRemoveMissing(UnboundedProcessor instance, long previousState) { } } + /** + * Set state {@link #STATE_TERMINATED} and {@link #release(ByteBuf)} all the elements from {@link + * #queue} and {@link #priorityQueue}. + * + *

This method may be called concurrently only if the given {@link UnboundedProcessor} has no + * output fusion ({@link #outputFused} {@code == true}). Otherwise this method MUST be called once + * and only by the downstream calling method {@link #clear()} + */ static void clearAndTerminate(UnboundedProcessor instance) { + final boolean outputFused = instance.outputFused; for (; ; ) { long state = instance.state; - if (instance.outputFused) { + if (outputFused) { instance.clearSafely(); } else { instance.clearUnsafely(); @@ -577,8 +708,12 @@ static boolean isCancelled(long state) { return (state & FLAG_CANCELLED) == FLAG_CANCELLED; } + static boolean isDisposed(long state) { + return (state & FLAG_DISPOSED) == FLAG_DISPOSED; + } + static boolean isWorkInProgress(long state) { - return (state & MAX_VALUE) != 0; + return (state & MAX_WIP_VALUE) != 0; } static boolean isTerminated(long state) { 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 1975c4ad9..f90607957 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java @@ -132,14 +132,14 @@ public void ensureUnboundedProcessorDisposesQueueProperly(boolean withFusionEnab unboundedProcessor.onNext(buffer2); }, unboundedProcessor::dispose, - Schedulers.elastic()), + Schedulers.boundedElastic()), assertSubscriber::cancel, - Schedulers.elastic()), + Schedulers.boundedElastic()), () -> { assertSubscriber.request(1); assertSubscriber.request(1); }, - Schedulers.elastic()); + Schedulers.boundedElastic()); assertSubscriber.values().forEach(ReferenceCountUtil::safeRelease); From 7ceb7aa50c2fe604736ca193b04fc5cf1298ae90 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 27 Feb 2021 19:55:14 +0200 Subject: [PATCH 012/116] updates CI budge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b7ced77e..085a9bd5e 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=develop)](https://travis-ci.org/rsocket/rsocket-java) +[![Build Status](https://github.com/rsocket/rsocket-java/actions/workflows/gradle-main.yml/badge.svg?branch=master)](https://github.com/rsocket/rsocket-java/actions/workflows/gradle-main.yml) ⚠️ The `master` branch is now dedicated to development of the `1.1.x` line. From ce62903f3fe6ae16a75aec205cd6ace197b47e36 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 27 Feb 2021 21:06:19 +0200 Subject: [PATCH 013/116] fixes AssertSubscriber to terminate upstream if ASYNC fusion Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/internal/subscriber/AssertSubscriber.java | 1 + 1 file changed, 1 insertion(+) 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 index 83d420d90..28206b4ff 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/subscriber/AssertSubscriber.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/subscriber/AssertSubscriber.java @@ -943,6 +943,7 @@ void drain() { t = qs.poll(); if (t == null) { if (done) { + qs.clear(); // clear upstream to terminated it due to the contract cdl.countDown(); return; } From da0c73f7a0cdb7507b3018a62b817d379c158289 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sun, 28 Feb 2021 17:12:47 +0200 Subject: [PATCH 014/116] enables previously disabled tests (#991) Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .../src/test/java/io/rsocket/core/RSocketRequesterTest.java | 1 - .../src/test/java/io/rsocket/core/RSocketResponderTest.java | 3 +-- .../rsocket/transport/netty/server/CloseableChannelTest.java | 4 ---- 3 files changed, 1 insertion(+), 7 deletions(-) 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 d3c27f090..9476be4d8 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -1201,7 +1201,6 @@ public void shouldTerminateAllStreamsIfThereRacingBetweenDisposeAndRequests( } @Test - @Disabled("Reactor 3.4.0 should fix that. No need to do anything on our side") // see https://github.com/rsocket/rsocket-java/issues/858 public void testWorkaround858() { ByteBuf buffer = rule.alloc().buffer(); 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 d796d45e5..50db22826 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -167,9 +167,9 @@ public Mono requestResponse(Payload payload) { @Test @Timeout(2_000) - @Disabled public void testHandlerEmitsError() throws Exception { final int streamId = 4; + rule.prefetch = 1; rule.sendRequest(streamId, FrameType.REQUEST_STREAM); assertThat( "Unexpected frame sent.", frameType(rule.connection.awaitFrame()), is(FrameType.ERROR)); @@ -839,7 +839,6 @@ private static Stream refCntCases() { } @Test - @Disabled("Reactor 3.4.0 should fix that. No need to do anything on our side") // see https://github.com/rsocket/rsocket-java/issues/858 public void testWorkaround858() { ByteBuf buffer = rule.alloc().buffer(); diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/CloseableChannelTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/CloseableChannelTest.java index 308118955..bd53a9b3f 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/CloseableChannelTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/CloseableChannelTest.java @@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -57,9 +56,6 @@ void constructorNullContext() { .withMessage("channel must not be null"); } - @Disabled( - "NettyContext isDisposed() is not accurate\n" - + "https://github.com/reactor/reactor-netty/issues/360") @DisplayName("disposes context") @Test void dispose() { From 01f0f5371b7c915a9a32d72eda788661b579f13a Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 2 Mar 2021 19:20:29 +0200 Subject: [PATCH 015/116] improves UnboundedProcessor Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .../rsocket/internal/UnboundedProcessor.java | 264 +++++++++++------- 1 file changed, 156 insertions(+), 108 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 0df8f1d34..d84546944 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -23,11 +23,12 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; -import org.reactivestreams.Subscriber; +import java.util.stream.Stream; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Exceptions; import reactor.core.Fuseable; +import reactor.core.Scannable; import reactor.core.publisher.FluxProcessor; import reactor.core.publisher.Operators; import reactor.util.annotation.Nullable; @@ -45,20 +46,23 @@ public final class UnboundedProcessor extends FluxProcessor final Queue queue; final Queue priorityQueue; + boolean cancelled; boolean done; Throwable error; CoreSubscriber actual; - static final long STATE_TERMINATED = + static final long FLAG_TERMINATED = 0b1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; static final long FLAG_DISPOSED = 0b0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; static final long FLAG_CANCELLED = 0b0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; - static final long FLAG_SUBSCRIBED_ONCE = + static final long FLAG_SUBSCRIBER_READY = 0b0001_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long FLAG_SUBSCRIBED_ONCE = + 0b0000_1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; static final long MAX_WIP_VALUE = - 0b0000_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111L; + 0b0000_0111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111L; volatile long state; @@ -87,10 +91,21 @@ public int getBufferSize() { return Integer.MAX_VALUE; } + @Override + public Stream inners() { + return hasDownstreams() ? Stream.of(Scannable.from(this.actual)) : Stream.empty(); + } + @Override public Object scanUnsafe(Attr key) { + if (Attr.ACTUAL == key) return isSubscriberReady(this.state) ? this.actual : null; if (Attr.BUFFERED == key) return this.queue.size() + this.priorityQueue.size(); if (Attr.PREFETCH == key) return Integer.MAX_VALUE; + if (Attr.CANCELLED == key) { + final long state = this.state; + return isCancelled(state) || isDisposed(state); + } + return super.scanUnsafe(key); } @@ -99,6 +114,10 @@ public void onNextPrioritized(ByteBuf t) { release(t); return; } + if (this.cancelled) { + release(t); + return; + } if (!this.priorityQueue.offer(t)) { Throwable ex = @@ -117,6 +136,10 @@ public void onNext(ByteBuf t) { release(t); return; } + if (this.cancelled) { + release(t); + return; + } if (!this.queue.offer(t)) { Throwable ex = @@ -135,6 +158,9 @@ public void onError(Throwable t) { Operators.onErrorDropped(t, currentContext()); return; } + if (this.cancelled) { + return; + } this.error = t; this.done = true; @@ -153,19 +179,6 @@ public void onComplete() { drain(); } - @Override - public void subscribe(CoreSubscriber actual) { - Objects.requireNonNull(actual, "subscribe"); - if (markSubscribedOnce(this)) { - this.actual = actual; - actual.onSubscribe(this); - drain(); - } else { - Operators.error( - actual, new IllegalStateException("UnboundedProcessor allows only a single Subscriber")); - } - } - void drain() { long previousState = wipIncrement(this); if (isTerminated(previousState)) { @@ -179,20 +192,11 @@ void drain() { long expectedState = previousState + 1; for (; ; ) { - if (isSubscribedOnce(expectedState)) { + if (isSubscriberReady(expectedState)) { final boolean outputFused = this.outputFused; - final Subscriber a = this.actual; + final CoreSubscriber a = this.actual; if (outputFused) { - if (isCancelled(expectedState)) { - return; - } - - if (isDisposed(expectedState)) { - a.onError(new CancellationException("Disposed")); - return; - } - drainFused(expectedState, a); } else { if (isCancelled(expectedState)) { @@ -223,7 +227,7 @@ void drain() { } } - void drainRegular(long expectedState, Subscriber a) { + void drainRegular(long expectedState, CoreSubscriber a) { final Queue q = this.queue; final Queue pq = this.priorityQueue; @@ -238,13 +242,10 @@ void drainRegular(long expectedState, Subscriber a) { // Thread2: ------------------> <#onNext(V)> --> <#onComplete()> boolean done = this.done; - ByteBuf t; - boolean empty; + ByteBuf t = pq.poll(); + boolean empty = t == null; - if (!pq.isEmpty()) { - t = pq.poll(); - empty = false; - } else { + if (empty) { t = q.poll(); empty = t == null; } @@ -296,7 +297,7 @@ void drainRegular(long expectedState, Subscriber a) { } } - void drainFused(long expectedState, Subscriber a) { + void drainFused(long expectedState, CoreSubscriber a) { for (; ; ) { // done has to be read before queue.poll to ensure there was no racing: // Thread1: <#drain>: queue.poll(null) --------------------> this.done(true) @@ -330,7 +331,7 @@ void drainFused(long expectedState, Subscriber a) { } } - boolean checkTerminated(boolean done, boolean empty, Subscriber a) { + boolean checkTerminated(boolean done, boolean empty, CoreSubscriber a) { final long state = this.state; if (isCancelled(state)) { clearAndTerminate(this); @@ -344,13 +345,13 @@ boolean checkTerminated(boolean done, boolean empty, Subscriber } if (done && empty) { + clearAndTerminate(this); Throwable e = this.error; if (e != null) { a.onError(e); } else { a.onComplete(); } - clearAndTerminate(this); return true; } @@ -374,13 +375,31 @@ public int getPrefetch() { @Override public Context currentContext() { - final long state = this.state; - if (isSubscribedOnce(state) || isTerminated(state)) { - CoreSubscriber actual = this.actual; - return actual != null ? actual.currentContext() : Context.empty(); - } + return isSubscriberReady(this.state) ? this.actual.currentContext() : Context.empty(); + } - return Context.empty(); + @Override + public void subscribe(CoreSubscriber actual) { + Objects.requireNonNull(actual, "subscribe"); + if (markSubscribedOnce(this)) { + actual.onSubscribe(this); + this.actual = actual; + long previousState = markSubscriberReady(this); + if (isCancelled(previousState)) { + return; + } + if (isDisposed(previousState)) { + actual.onError(new CancellationException("Disposed")); + return; + } + if (isWorkInProgress(previousState)) { + return; + } + drain(); + } else { + Operators.error( + actual, new IllegalStateException("UnboundedProcessor allows only a single Subscriber")); + } } @Override @@ -393,17 +412,25 @@ public void request(long n) { @Override public void cancel() { - if (!markCancelled(this)) { + this.cancelled = true; + + final long previousState = markCancelled(this); + if (isTerminated(previousState) + || isCancelled(previousState) + || isDisposed(previousState) + || isWorkInProgress(previousState)) { return; } - if (!this.outputFused) { + if (!isSubscriberReady(previousState) || !this.outputFused) { clearAndTerminate(this); } } @Override public void dispose() { + this.cancelled = true; + final long previousState = markDisposed(this); if (isTerminated(previousState) || isCancelled(previousState) @@ -412,25 +439,37 @@ public void dispose() { return; } - if (!this.outputFused) { + if (!isSubscriberReady(previousState)) { clearAndTerminate(this); + return; } - if (isSubscribedOnce(previousState)) { - this.actual.onError(new CancellationException("Disposed")); + if (!this.outputFused) { + clearAndTerminate(this); } + this.actual.onError(new CancellationException("Disposed")); } @Override @Nullable public ByteBuf poll() { - Queue pq = this.priorityQueue; - if (!pq.isEmpty()) { - return pq.poll(); + ByteBuf t = this.priorityQueue.poll(); + if (t != null) { + return t; } return this.queue.poll(); } + @Override + public int size() { + return this.priorityQueue.size() + this.queue.size(); + } + + @Override + public boolean isEmpty() { + return this.priorityQueue.isEmpty() && this.queue.isEmpty(); + } + /** * Clears all elements from queues and set state to terminate. This method MUST be called only by * the downstream subscriber which has enabled {@link Fuseable#ASYNC} fusion with the given {@link @@ -473,16 +512,6 @@ void clearUnsafely() { } } - @Override - public int size() { - return this.priorityQueue.size() + this.queue.size(); - } - - @Override - public boolean isEmpty() { - return this.priorityQueue.isEmpty() && this.queue.isEmpty(); - } - @Override public int requestFusion(int requestedMode) { if ((requestedMode & Fuseable.ASYNC) != 0) { @@ -500,15 +529,17 @@ public boolean isDisposed() { @Override public boolean isTerminated() { + //noinspection unused final long state = this.state; - return isTerminated(state) || this.done; + return this.done; } @Override @Nullable public Throwable getError() { + //noinspection unused final long state = this.state; - if (isTerminated(state) || this.done) { + if (this.done) { return this.error; } else { return null; @@ -522,7 +553,8 @@ public long downstreamCount() { @Override public boolean hasDownstreams() { - return (this.state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE && this.actual != null; + final long state = this.state; + return !isTerminated(state) && isSubscriberReady(state); } static void release(ByteBuf byteBuf) { @@ -536,8 +568,8 @@ static void release(ByteBuf byteBuf) { } /** - * Tries to set {@link #FLAG_SUBSCRIBED_ONCE} flag if it was not set before and if state is not - * {@link #STATE_TERMINATED} and flags {@link #FLAG_CANCELLED} or {@link #FLAG_DISPOSED} are unset + * Sets {@link #FLAG_SUBSCRIBED_ONCE} flag if it was not set before and if flags {@link + * #FLAG_TERMINATED}, {@link #FLAG_CANCELLED} or {@link #FLAG_DISPOSED} are unset * * @return {@code true} if {@link #FLAG_SUBSCRIBED_ONCE} was successfully set */ @@ -545,11 +577,8 @@ static boolean markSubscribedOnce(UnboundedProcessor instance) { for (; ; ) { long state = instance.state; - if (state == STATE_TERMINATED) { - return false; - } - - if ((state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE + if ((state & FLAG_TERMINATED) == FLAG_TERMINATED + || (state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE || (state & FLAG_CANCELLED) == FLAG_CANCELLED || (state & FLAG_DISPOSED) == FLAG_DISPOSED) { return false; @@ -562,21 +591,40 @@ static boolean markSubscribedOnce(UnboundedProcessor instance) { } /** - * Tries to set {@link #FLAG_CANCELLED} flag if it was not set before and if state is not {@link - * #STATE_TERMINATED}. Also, this method increments number of work in progress (WIP) + * Sets {@link #FLAG_SUBSCRIBER_READY} flag if flags {@link #FLAG_TERMINATED}, {@link + * #FLAG_CANCELLED} or {@link #FLAG_DISPOSED} are unset * - * @return {@code true} if {@link #FLAG_CANCELLED} was successfully set + * @return previous state */ - static boolean markCancelled(UnboundedProcessor instance) { + static long markSubscriberReady(UnboundedProcessor instance) { for (; ; ) { long state = instance.state; - if (state == STATE_TERMINATED) { - return false; + if ((state & FLAG_TERMINATED) == FLAG_TERMINATED + || (state & FLAG_CANCELLED) == FLAG_CANCELLED + || (state & FLAG_DISPOSED) == FLAG_DISPOSED) { + return state; } - if ((state & FLAG_CANCELLED) == FLAG_CANCELLED) { - return false; + if (STATE.compareAndSet(instance, state, state | FLAG_SUBSCRIBER_READY)) { + return state; + } + } + } + + /** + * Sets {@link #FLAG_CANCELLED} flag if it was not set before and if flag {@link #FLAG_TERMINATED} + * is unset. Also, this method increments number of work in progress (WIP) + * + * @return previous state + */ + static long markCancelled(UnboundedProcessor instance) { + for (; ; ) { + long state = instance.state; + + if ((state & FLAG_TERMINATED) == FLAG_TERMINATED + || (state & FLAG_CANCELLED) == FLAG_CANCELLED) { + return state; } long nextState = state + 1; @@ -585,15 +633,15 @@ static boolean markCancelled(UnboundedProcessor instance) { } if (STATE.compareAndSet(instance, state, nextState | FLAG_CANCELLED)) { - return !isWorkInProgress(state); + return state; } } } /** - * Tries to set {@link #FLAG_DISPOSED} flag if it was not set before and if state is not {@link - * #STATE_TERMINATED} and flags {@link #FLAG_CANCELLED} are unset. Also, this method increments - * number of work in progress (WIP) + * Sets {@link #FLAG_DISPOSED} flag if it was not set before and if flags {@link + * #FLAG_TERMINATED}, {@link #FLAG_CANCELLED} are unset. Also, this method increments number of + * work in progress (WIP) * * @return previous state */ @@ -601,11 +649,9 @@ static long markDisposed(UnboundedProcessor instance) { for (; ; ) { long state = instance.state; - if (state == STATE_TERMINATED) { - return STATE_TERMINATED; - } - - if ((state & FLAG_CANCELLED) == FLAG_CANCELLED || (state & FLAG_DISPOSED) == FLAG_DISPOSED) { + if ((state & FLAG_TERMINATED) == FLAG_TERMINATED + || (state & FLAG_CANCELLED) == FLAG_CANCELLED + || (state & FLAG_DISPOSED) == FLAG_DISPOSED) { return state; } @@ -621,8 +667,8 @@ static long markDisposed(UnboundedProcessor instance) { } /** - * Tries to increment the amount of work in progress (max value is {@link #MAX_WIP_VALUE} on the - * given state. Fails if state is {@link #STATE_TERMINATED}. + * Increments the amount of work in progress (max value is {@link #MAX_WIP_VALUE} on the given + * state. Fails if flag {@link #FLAG_TERMINATED} is set. * * @return previous state */ @@ -630,8 +676,8 @@ static long wipIncrement(UnboundedProcessor instance) { for (; ; ) { long state = instance.state; - if (state == STATE_TERMINATED) { - return STATE_TERMINATED; + if ((state & FLAG_TERMINATED) == FLAG_TERMINATED) { + return state; } final long nextState = state + 1; @@ -646,23 +692,26 @@ static long wipIncrement(UnboundedProcessor instance) { } /** - * Tries to decrement the amount of work in progress by the given amount on the given state. Fails - * if state is {@link #STATE_TERMINATED} or it has flags {@link #FLAG_CANCELLED} or {@link - * #FLAG_DISPOSED} set. + * Decrements the amount of work in progress by the given amount on the given state. Fails if flag + * is {@link #FLAG_TERMINATED} is set or if fusion disabled and flags {@link #FLAG_CANCELLED} or + * {@link #FLAG_DISPOSED} are set. + * + *

Note, if fusion is enabled, the decrement should work if flags {@link #FLAG_CANCELLED} or + * {@link #FLAG_DISPOSED} are set, since, while the operator was not terminate by the downstream, + * we still have to propagate notifications that new elements are enqueued * * @return state after changing WIP or current state if update failed */ static long wipRemoveMissing(UnboundedProcessor instance, long previousState) { long missed = previousState & MAX_WIP_VALUE; - boolean outputFused = instance.outputFused; for (; ; ) { long state = instance.state; - if (state == STATE_TERMINATED) { - return STATE_TERMINATED; + if ((state & FLAG_TERMINATED) == FLAG_TERMINATED) { + return state; } - if (!outputFused + if (((state & FLAG_SUBSCRIBER_READY) != FLAG_SUBSCRIBER_READY || !instance.outputFused) && ((state & FLAG_CANCELLED) == FLAG_CANCELLED || (state & FLAG_DISPOSED) == FLAG_DISPOSED)) { return state; @@ -676,7 +725,7 @@ static long wipRemoveMissing(UnboundedProcessor instance, long previousState) { } /** - * Set state {@link #STATE_TERMINATED} and {@link #release(ByteBuf)} all the elements from {@link + * Set flag {@link #FLAG_TERMINATED} and {@link #release(ByteBuf)} all the elements from {@link * #queue} and {@link #priorityQueue}. * *

This method may be called concurrently only if the given {@link UnboundedProcessor} has no @@ -684,21 +733,20 @@ static long wipRemoveMissing(UnboundedProcessor instance, long previousState) { * and only by the downstream calling method {@link #clear()} */ static void clearAndTerminate(UnboundedProcessor instance) { - final boolean outputFused = instance.outputFused; for (; ; ) { long state = instance.state; - if (outputFused) { + if (!isSubscriberReady(state) || !instance.outputFused) { instance.clearSafely(); } else { instance.clearUnsafely(); } - if (state == STATE_TERMINATED) { + if ((state & FLAG_TERMINATED) == FLAG_TERMINATED) { return; } - if (STATE.compareAndSet(instance, state, STATE_TERMINATED)) { + if (STATE.compareAndSet(instance, state, (state & ~MAX_WIP_VALUE) | FLAG_TERMINATED)) { break; } } @@ -717,10 +765,10 @@ static boolean isWorkInProgress(long state) { } static boolean isTerminated(long state) { - return state == STATE_TERMINATED; + return (state & FLAG_TERMINATED) == FLAG_TERMINATED; } - static boolean isSubscribedOnce(long state) { - return (state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE; + static boolean isSubscriberReady(long state) { + return (state & FLAG_SUBSCRIBER_READY) == FLAG_SUBSCRIBER_READY; } } From 765abb7be933046333cf922a8b8d8d6bc50ac2de Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 3 Mar 2021 10:23:11 +0000 Subject: [PATCH 016/116] DefaultPayload#create properly copies ByteBuf content Closes gh-970 Signed-off-by: Rossen Stoyanchev --- .../java/io/rsocket/util/DefaultPayload.java | 13 +++++++++-- .../io/rsocket/core/RSocketConnectorTest.java | 22 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) 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 d59b9fe97..08b8b2fb7 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java @@ -100,7 +100,7 @@ public static Payload create(ByteBuf data) { public static Payload create(ByteBuf data, @Nullable ByteBuf metadata) { try { - return create(data.nioBuffer(), metadata == null ? null : metadata.nioBuffer()); + return create(toBytes(data), metadata != null ? toBytes(metadata) : null); } finally { data.release(); if (metadata != null) { @@ -110,7 +110,16 @@ public static Payload create(ByteBuf data, @Nullable ByteBuf metadata) { } public static Payload create(Payload payload) { - return create(payload.getData(), payload.hasMetadata() ? payload.getMetadata() : null); + return create( + toBytes(payload.data()), payload.hasMetadata() ? toBytes(payload.metadata()) : null); + } + + private static byte[] toBytes(ByteBuf byteBuf) { + byte[] bytes = new byte[byteBuf.readableBytes()]; + byteBuf.markReaderIndex(); + byteBuf.readBytes(bytes); + byteBuf.resetReaderIndex(); + return bytes; } @Override diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java index 468a13505..ec4adb8ad 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java @@ -73,9 +73,14 @@ public void ensuresThatSetupPayloadCanBeRetained() { @Test public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions() { Payload setupPayload = ByteBufPayload.create("TestData", "TestMetadata"); - Assertions.assertThat(setupPayload.refCnt()).isOne(); + // Keep the data and metadata around so we can try changing them independently + ByteBuf dataBuf = setupPayload.data(); + ByteBuf metadataBuf = setupPayload.metadata(); + dataBuf.retain(); + metadataBuf.retain(); + TestClientTransport testClientTransport = new TestClientTransport(); Mono connectionMono = RSocketConnector.create().setupPayload(setupPayload).connect(testClientTransport); @@ -92,6 +97,15 @@ public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions .expectComplete() .verify(Duration.ofMillis(100)); + // Changing the original data and metadata should not impact the SetupPayload + dataBuf.writerIndex(dataBuf.readerIndex()); + dataBuf.writeChar('d'); + dataBuf.release(); + + metadataBuf.writerIndex(metadataBuf.readerIndex()); + metadataBuf.writeChar('m'); + metadataBuf.release(); + Assertions.assertThat(testClientTransport.testConnection().getSent()) .hasSize(2) .allMatch( @@ -100,7 +114,11 @@ public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions return payload.getDataUtf8().equals("TestData") && payload.getMetadataUtf8().equals("TestMetadata"); }) - .allMatch(ReferenceCounted::release); + .allMatch( + byteBuf -> { + System.out.println("calling release " + byteBuf.refCnt()); + return byteBuf.release(); + }); Assertions.assertThat(setupPayload.refCnt()).isZero(); } From e4d62b677b8a0e709f845d77964d1f9b6a05b167 Mon Sep 17 00:00:00 2001 From: Tomas Kolda Date: Thu, 4 Mar 2021 15:25:45 +0100 Subject: [PATCH 017/116] fixes performance degradation when fragmentation is used (#995) --- .../FragmentationDuplexConnection.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 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 6eebd676c..84338d1df 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java @@ -84,7 +84,18 @@ public static int assertMtu(int mtu) { @Override public Mono send(Publisher frames) { - return Flux.from(frames).concatMap(this::sendOne).then(); + return delegate.send( + Flux.from(frames) + .concatMap( + frame -> { + FrameType frameType = FrameHeaderCodec.frameType(frame); + int readableBytes = frame.readableBytes(); + if (!shouldFragment(frameType, readableBytes)) { + return Flux.just(frame); + } + + return logFragments(Flux.from(fragmentFrame(alloc(), mtu, frame, frameType))); + })); } @Override @@ -95,6 +106,11 @@ public Mono sendOne(ByteBuf frame) { return delegate.sendOne(frame); } Flux fragments = Flux.from(fragmentFrame(alloc(), mtu, frame, frameType)); + fragments = logFragments(fragments); + return delegate.send(fragments); + } + + protected Flux logFragments(Flux fragments) { if (logger.isDebugEnabled()) { fragments = fragments.doOnNext( @@ -107,6 +123,6 @@ public Mono sendOne(ByteBuf frame) { ByteBufUtil.prettyHexDump(byteBuf)); }); } - return delegate.send(fragments); + return fragments; } } From 76865fe32c55426e1906f1d655a20b5dbae99e98 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 4 Mar 2021 17:27:42 +0200 Subject: [PATCH 018/116] updates versions Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka 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 f28185a44..3c7e87976 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ repositories { maven { url 'https://repo.spring.io/milestone' } // Reactor milestones (if needed) } dependencies { - implementation 'io.rsocket:rsocket-core:1.0.3' - implementation 'io.rsocket:rsocket-transport-netty:1.0.3' + implementation 'io.rsocket:rsocket-core:1.0.4' + implementation 'io.rsocket:rsocket-transport-netty:1.0.4' } ``` @@ -42,8 +42,8 @@ repositories { maven { url 'https://repo.spring.io/snapshot' } // Reactor snapshots (if needed) } dependencies { - implementation 'io.rsocket:rsocket-core:1.0.4-SNAPSHOT' - implementation 'io.rsocket:rsocket-transport-netty:1.0.4-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:1.0.5-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-netty:1.0.5-SNAPSHOT' } ``` diff --git a/gradle.properties b/gradle.properties index 71389f0c1..f75f86589 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.4 -perfBaselineVersion=1.0.3 +version=1.0.5 +perfBaselineVersion=1.0.4 From 14c6f0c3a9737f72fbb3492897c3cc4cc12be7e5 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Wed, 24 Mar 2021 20:29:16 +0200 Subject: [PATCH 019/116] refactors Lease API (#885) Co-authored-by: Rossen Stoyanchev --- build.gradle | 2 + .../java/io/rsocket/DuplexConnection.java | 2 +- .../io/rsocket/core/LeasePermitHandler.java | 20 ++ .../main/java/io/rsocket/core/LeaseSpec.java | 44 +++ .../io/rsocket/core/RSocketConnector.java | 99 ++++-- .../io/rsocket/core/RSocketRequester.java | 43 ++- .../io/rsocket/core/RSocketResponder.java | 52 ++- .../java/io/rsocket/core/RSocketServer.java | 58 ++-- .../core/RequestChannelRequesterFlux.java | 313 +++++++++++++----- .../core/RequestResponseRequesterMono.java | 52 ++- .../core/RequestStreamRequesterFlux.java | 47 ++- .../rsocket/core/RequesterLeaseTracker.java | 134 ++++++++ .../core/RequesterResponderSupport.java | 11 + .../rsocket/core/ResponderLeaseTracker.java | 112 +++++++ .../core/SlowFireAndForgetRequesterMono.java | 255 ++++++++++++++ .../main/java/io/rsocket/core/StateUtils.java | 134 +++++++- .../src/main/java/io/rsocket/lease/Lease.java | 109 +++--- .../main/java/io/rsocket/lease/LeaseImpl.java | 125 ------- .../java/io/rsocket/lease/LeaseSender.java | 8 + .../java/io/rsocket/lease/LeaseStats.java | 28 -- .../main/java/io/rsocket/lease/Leases.java | 65 ---- .../rsocket/lease/MissingLeaseException.java | 23 +- .../rsocket/lease/RequesterLeaseHandler.java | 113 ------- .../rsocket/lease/ResponderLeaseHandler.java | 146 -------- .../io/rsocket/lease/TrackingLeaseSender.java | 5 + .../io/rsocket/loadbalance/RSocketPool.java | 7 +- .../plugins/CompositeRequestInterceptor.java | 12 +- .../InitializingInterceptorRegistry.java | 17 +- .../core/DefaultRSocketClientTests.java | 3 +- .../java/io/rsocket/core/KeepAliveTest.java | 2 +- .../io/rsocket/core/RSocketLeaseTest.java | 93 +++--- .../core/RSocketRequesterSubscribersTest.java | 3 +- .../io/rsocket/core/RSocketRequesterTest.java | 3 +- .../io/rsocket/core/RSocketResponderTest.java | 3 +- .../java/io/rsocket/core/RSocketTest.java | 6 +- .../io/rsocket/core/SetupRejectionTest.java | 5 +- .../java/io/rsocket/lease/LeaseImplTest.java | 122 ++++--- rsocket-examples/build.gradle | 3 + .../lease/advanced/common/LeaseManager.java | 144 ++++++++ .../common/LimitBasedLeaseSender.java | 54 +++ .../common/LimitBasedStatsCollector.java | 73 ++++ .../tcp/lease/advanced/controller/Task.java | 27 ++ .../controller/TasksHandlingRSocket.java | 44 +++ .../advanced/invertmulticlient/README.MD | 0 .../invertmulticlient/RequestingServer.java | 78 +++++ .../invertmulticlient/RespondingClient.java | 67 ++++ .../tcp/lease/advanced/multiclient/README.MD | 0 .../multiclient/RequestingClient.java | 41 +++ .../multiclient/RespondingServer.java | 81 +++++ .../tcp/lease/{ => simple}/LeaseExample.java | 73 +--- 50 files changed, 1994 insertions(+), 967 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/core/LeasePermitHandler.java create mode 100644 rsocket-core/src/main/java/io/rsocket/core/LeaseSpec.java create mode 100644 rsocket-core/src/main/java/io/rsocket/core/RequesterLeaseTracker.java create mode 100644 rsocket-core/src/main/java/io/rsocket/core/ResponderLeaseTracker.java create mode 100644 rsocket-core/src/main/java/io/rsocket/core/SlowFireAndForgetRequesterMono.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java create mode 100644 rsocket-core/src/main/java/io/rsocket/lease/LeaseSender.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/lease/LeaseStats.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/lease/Leases.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java create mode 100644 rsocket-core/src/main/java/io/rsocket/lease/TrackingLeaseSender.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/common/LeaseManager.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/common/LimitBasedLeaseSender.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/common/LimitBasedStatsCollector.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/controller/Task.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/controller/TasksHandlingRSocket.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/invertmulticlient/README.MD create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/invertmulticlient/RequestingServer.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/invertmulticlient/RespondingClient.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/multiclient/README.MD create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/multiclient/RequestingClient.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/multiclient/RespondingServer.java rename rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/{ => simple}/LeaseExample.java (66%) diff --git a/build.gradle b/build.gradle index 40d97949a..f665350c3 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,7 @@ subprojects { ext['hamcrest.version'] = '1.3' ext['micrometer.version'] = '1.0.6' ext['assertj.version'] = '3.11.1' + ext['netflix.limits.version'] = '0.3.6' group = "io.rsocket" @@ -69,6 +70,7 @@ subprojects { } dependencies { + dependency "com.netflix.concurrency-limits:concurrency-limits-core:${ext['netflix.limits.version']}" dependency "ch.qos.logback:logback-classic:${ext['logback.version']}" dependency "io.netty:netty-tcnative-boringssl-static:${ext['netty-boringssl.version']}" dependency "io.micrometer:micrometer-core:${ext['micrometer.version']}" diff --git a/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java index 497edf123..fe91f4bf0 100644 --- a/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java @@ -49,7 +49,7 @@ public interface DuplexConnection extends Availability, Closeable { *

Completion * *

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

Error * diff --git a/rsocket-core/src/main/java/io/rsocket/core/LeasePermitHandler.java b/rsocket-core/src/main/java/io/rsocket/core/LeasePermitHandler.java new file mode 100644 index 000000000..03ab7c257 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/LeasePermitHandler.java @@ -0,0 +1,20 @@ +package io.rsocket.core; + +/** Handler which enables async lease permits issuing */ +interface LeasePermitHandler { + + /** + * Called by {@link RequesterLeaseTracker} when there is an available lease + * + * @return {@code true} to indicate that lease permit was consumed successfully + */ + boolean handlePermit(); + + /** + * Called by {@link RequesterLeaseTracker} when there are no lease permit available at the moment + * and the list of awaiting {@link LeasePermitHandler} reached the configured limit + * + * @param t associated lease permit rejection exception + */ + void handlePermitError(Throwable t); +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/LeaseSpec.java b/rsocket-core/src/main/java/io/rsocket/core/LeaseSpec.java new file mode 100644 index 000000000..3947f296a --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/LeaseSpec.java @@ -0,0 +1,44 @@ +/* + * 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.rsocket.lease.LeaseSender; +import reactor.core.publisher.Flux; + +public final class LeaseSpec { + + LeaseSender sender = Flux::never; + int maxPendingRequests = 256; + + LeaseSpec() {} + + public LeaseSpec sender(LeaseSender sender) { + this.sender = sender; + return this; + } + + /** + * Setup the maximum queued requests waiting for lease to be available. The default value is 256 + * + * @param maxPendingRequests if set to 0 the requester will terminate the request immediately if + * no leases is available + */ + public LeaseSpec maxPendingRequests(int maxPendingRequests) { + this.maxPendingRequests = 0; + 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 1c4e66477..fe91cdb6f 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -29,13 +29,11 @@ import io.rsocket.frame.SetupFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; 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.lease.TrackingLeaseSender; import io.rsocket.plugins.DuplexConnectionInterceptor; import io.rsocket.plugins.InitializingInterceptorRegistry; import io.rsocket.plugins.InterceptorRegistry; +import io.rsocket.plugins.RequestInterceptor; import io.rsocket.resume.ClientRSocketSession; import io.rsocket.resume.ResumableDuplexConnection; import io.rsocket.resume.ResumableFramesStore; @@ -94,7 +92,8 @@ public class RSocketConnector { private Retry retrySpec; private Resume resume; - private Supplier> leasesSupplier; + + @Nullable private Consumer leaseConfigurer; private int mtu = 0; private int maxInboundPayloadSize = Integer.MAX_VALUE; @@ -403,18 +402,43 @@ public RSocketConnector resume(Resume resume) { * *

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

By default this is not enabled. * - * @param supplier supplier for a {@link Leases} * @return the same instance for method chaining * @see Lease * Semantics */ - public RSocketConnector lease(Supplier> supplier) { - this.leasesSupplier = supplier; + public RSocketConnector lease() { + return lease((config -> {})); + } + + /** + * 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(spec -> spec.maxPendingRequests(128))
+   *                         .connect(transport);
+   * }
+ * + *

By default this is not enabled. + * + * @param leaseConfigurer consumer which accepts {@link LeaseSpec} and use it for configuring + * @return the same instance for method chaining + * @see Lease + * Semantics + */ + public RSocketConnector lease(Consumer leaseConfigurer) { + this.leaseConfigurer = leaseConfigurer; return this; } @@ -543,7 +567,7 @@ public Mono connect(Supplier transportSupplier) { tuple2 -> { DuplexConnection sourceConnection = tuple2.getT1(); Payload setupPayload = tuple2.getT2(); - boolean leaseEnabled = leasesSupplier != null; + boolean leaseEnabled = leaseConfigurer != null; boolean resumeEnabled = resume != null; // TODO: add LeaseClientSetup ClientSetup clientSetup = new DefaultClientSetup(); @@ -575,10 +599,12 @@ public Mono connect(Supplier transportSupplier) { // should be used if lease setup sequence; // See: // https://github.com/rsocket/rsocket/blob/master/Protocol.md#sequences-with-lease - ByteBuf serverResponse = tuple.getT1(); - DuplexConnection clientServerConnection = tuple.getT2(); - KeepAliveHandler keepAliveHandler; - DuplexConnection wrappedConnection; + final ByteBuf serverResponse = tuple.getT1(); + final DuplexConnection clientServerConnection = tuple.getT2(); + final KeepAliveHandler keepAliveHandler; + final DuplexConnection wrappedConnection; + final InitializingInterceptorRegistry interceptors = + this.interceptors; if (resumeEnabled) { final ResumableFramesStore resumableFramesStore = @@ -615,12 +641,18 @@ public Mono connect(Supplier transportSupplier) { new ClientServerInputMultiplexer( wrappedConnection, interceptors, true); - Leases leases = leaseEnabled ? leasesSupplier.get() : null; - RequesterLeaseHandler requesterLeaseHandler = - leaseEnabled - ? new RequesterLeaseHandler.Impl( - CLIENT_TAG, leases.receiver()) - : RequesterLeaseHandler.None; + final LeaseSpec leases; + final RequesterLeaseTracker requesterLeaseTracker; + if (leaseEnabled) { + leases = new LeaseSpec(); + leaseConfigurer.accept(leases); + requesterLeaseTracker = + new RequesterLeaseTracker( + CLIENT_TAG, leases.maxPendingRequests); + } else { + leases = null; + requesterLeaseTracker = null; + } RSocket rSocketRequester = new RSocketRequester( @@ -634,7 +666,7 @@ public Mono connect(Supplier transportSupplier) { (int) keepAliveMaxLifeTime.toMillis(), keepAliveHandler, interceptors::initRequesterRequestInterceptor, - requesterLeaseHandler); + requesterLeaseTracker); RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); @@ -655,25 +687,34 @@ public Mono connect(Supplier transportSupplier) { RSocket wrappedRSocketHandler = interceptors.initResponder(rSocketHandler); - ResponderLeaseHandler responderLeaseHandler = + ResponderLeaseTracker responderLeaseTracker = leaseEnabled - ? new ResponderLeaseHandler.Impl<>( + ? new ResponderLeaseTracker( CLIENT_TAG, - wrappedConnection.alloc(), - leases.sender(), - leases.stats()) - : ResponderLeaseHandler.None; + wrappedConnection, + leases.sender) + : null; RSocket rSocketResponder = new RSocketResponder( multiplexer.asServerConnection(), wrappedRSocketHandler, payloadDecoder, - responderLeaseHandler, + responderLeaseTracker, mtu, maxFrameLength, maxInboundPayloadSize, - interceptors::initResponderRequestInterceptor); + leaseEnabled + && leases.sender + instanceof TrackingLeaseSender + ? rSocket -> + interceptors + .initResponderRequestInterceptor( + rSocket, + (RequestInterceptor) + leases.sender) + : interceptors + ::initResponderRequestInterceptor); return wrappedRSocketRequester; }) 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 f51c14a6d..89c1500bb 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -32,7 +32,6 @@ import io.rsocket.keepalive.KeepAliveFramesAcceptor; import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.keepalive.KeepAliveSupport; -import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.plugins.RequestInterceptor; import java.nio.channels.ClosedChannelException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; @@ -63,7 +62,7 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { AtomicReferenceFieldUpdater.newUpdater( RSocketRequester.class, Throwable.class, "terminationError"); - private final RequesterLeaseHandler leaseHandler; + @Nullable private final RequesterLeaseTracker requesterLeaseTracker; private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; private final MonoProcessor onClose; @@ -78,7 +77,7 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { int keepAliveAckTimeout, @Nullable KeepAliveHandler keepAliveHandler, Function requestInterceptorFunction, - RequesterLeaseHandler leaseHandler) { + @Nullable RequesterLeaseTracker requesterLeaseTracker) { super( mtu, maxFrameLength, @@ -88,7 +87,7 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { streamIdSupplier, requestInterceptorFunction); - this.leaseHandler = leaseHandler; + this.requesterLeaseTracker = requesterLeaseTracker; this.onClose = MonoProcessor.create(); // DO NOT Change the order here. The Send processor must be subscribed to before receiving @@ -111,7 +110,11 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { @Override public Mono fireAndForget(Payload payload) { - return new FireAndForgetRequesterMono(payload, this); + if (this.requesterLeaseTracker == null) { + return new FireAndForgetRequesterMono(payload, this); + } else { + return new SlowFireAndForgetRequesterMono(payload, this); + } } @Override @@ -141,12 +144,12 @@ public Mono metadataPush(Payload payload) { } @Override - public int getNextStreamId() { - RequesterLeaseHandler leaseHandler = this.leaseHandler; - if (!leaseHandler.useLease()) { - throw reactor.core.Exceptions.propagate(leaseHandler.leaseError()); - } + public RequesterLeaseTracker getRequesterLeaseTracker() { + return this.requesterLeaseTracker; + } + @Override + public int getNextStreamId() { int nextStreamId = super.getNextStreamId(); Throwable terminationError = this.terminationError; @@ -159,11 +162,6 @@ public int getNextStreamId() { @Override public int addAndGetNextStreamId(FrameHandler frameHandler) { - RequesterLeaseHandler leaseHandler = this.leaseHandler; - if (!leaseHandler.useLease()) { - throw reactor.core.Exceptions.propagate(leaseHandler.leaseError()); - } - int nextStreamId = super.addAndGetNextStreamId(frameHandler); Throwable terminationError = this.terminationError; @@ -177,7 +175,12 @@ public int addAndGetNextStreamId(FrameHandler frameHandler) { @Override public double availability() { - return Math.min(getDuplexConnection().availability(), leaseHandler.availability()); + final RequesterLeaseTracker requesterLeaseTracker = this.requesterLeaseTracker; + if (requesterLeaseTracker != null) { + return Math.min(getDuplexConnection().availability(), requesterLeaseTracker.availability()); + } else { + return getDuplexConnection().availability(); + } } @Override @@ -218,7 +221,7 @@ private void handleStreamZero(FrameType type, ByteBuf frame) { tryTerminateOnZeroError(frame); break; case LEASE: - leaseHandler.receive(frame); + requesterLeaseTracker.handleLeaseFrame(frame); break; case KEEPALIVE: if (keepAliveFramesAcceptor != null) { @@ -333,7 +336,11 @@ private void terminate(Throwable e) { if (requestInterceptor != null) { requestInterceptor.dispose(); } - leaseHandler.dispose(); + + final RequesterLeaseTracker requesterLeaseTracker = this.requesterLeaseTracker; + if (requesterLeaseTracker != null) { + requesterLeaseTracker.dispose(e); + } synchronized (this) { activeStreams 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 b8f356493..f0a052b93 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -30,7 +30,6 @@ import io.rsocket.frame.RequestResponseFrameCodec; import io.rsocket.frame.RequestStreamFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.lease.ResponderLeaseHandler; import io.rsocket.plugins.RequestInterceptor; import java.nio.channels.ClosedChannelException; import java.util.concurrent.CancellationException; @@ -40,9 +39,9 @@ import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.annotation.Nullable; /** Responder side of RSocket. Receives {@link ByteBuf}s from a peer's {@link RSocketRequester} */ class RSocketResponder extends RequesterResponderSupport implements RSocket { @@ -53,8 +52,7 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { private final RSocket requestHandler; - private final ResponderLeaseHandler leaseHandler; - private final Disposable leaseHandlerDisposable; + @Nullable private final ResponderLeaseTracker leaseHandler; private volatile Throwable terminationError; private static final AtomicReferenceFieldUpdater TERMINATION_ERROR = @@ -65,7 +63,7 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { DuplexConnection connection, RSocket requestHandler, PayloadDecoder payloadDecoder, - ResponderLeaseHandler leaseHandler, + @Nullable ResponderLeaseTracker leaseHandler, int mtu, int maxFrameLength, int maxInboundPayloadSize, @@ -83,10 +81,7 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { this.leaseHandler = leaseHandler; - // DO NOT Change the order here. The Send processor must be subscribed to before receiving - // connections connection.receive().subscribe(this::handleFrame, e -> {}); - leaseHandlerDisposable = leaseHandler.send(leaseFrame -> connection.sendFrame(0, leaseFrame)); connection .onClose() @@ -178,7 +173,12 @@ final void doOnDispose() { if (requestInterceptor != null) { requestInterceptor.dispose(); } - leaseHandlerDisposable.dispose(); + + final ResponderLeaseTracker handler = leaseHandler; + if (handler != null) { + handler.dispose(); + } + requestHandler.dispose(); } @@ -287,8 +287,9 @@ final void handleFrame(ByteBuf frame) { } final void handleFireAndForget(int streamId, ByteBuf frame) { - if (leaseHandler.useLease()) { - + ResponderLeaseTracker leaseHandler = this.leaseHandler; + Throwable leaseError; + if (leaseHandler == null || (leaseError = leaseHandler.use()) == null) { if (FrameHeaderCodec.hasFollows(frame)) { final RequestInterceptor requestInterceptor = this.getRequestInterceptor(); if (requestInterceptor != null) { @@ -317,15 +318,15 @@ final void handleFireAndForget(int streamId, ByteBuf frame) { final RequestInterceptor requestTracker = this.getRequestInterceptor(); if (requestTracker != null) { requestTracker.onReject( - leaseHandler.leaseError(), - FrameType.REQUEST_FNF, - RequestFireAndForgetFrameCodec.metadata(frame)); + leaseError, FrameType.REQUEST_FNF, RequestFireAndForgetFrameCodec.metadata(frame)); } } } final void handleRequestResponse(int streamId, ByteBuf frame) { - if (leaseHandler.useLease()) { + ResponderLeaseTracker leaseHandler = this.leaseHandler; + Throwable leaseError; + if (leaseHandler == null || (leaseError = leaseHandler.use()) == null) { final RequestInterceptor requestInterceptor = this.getRequestInterceptor(); if (requestInterceptor != null) { requestInterceptor.onStart( @@ -346,7 +347,6 @@ final void handleRequestResponse(int streamId, ByteBuf frame) { } } } else { - final Exception leaseError = leaseHandler.leaseError(); final RequestInterceptor requestInterceptor = this.getRequestInterceptor(); if (requestInterceptor != null) { requestInterceptor.onReject( @@ -357,7 +357,9 @@ final void handleRequestResponse(int streamId, ByteBuf frame) { } final void handleStream(int streamId, ByteBuf frame, long initialRequestN) { - if (leaseHandler.useLease()) { + ResponderLeaseTracker leaseHandler = this.leaseHandler; + Throwable leaseError; + if (leaseHandler == null || (leaseError = leaseHandler.use()) == null) { final RequestInterceptor requestInterceptor = this.getRequestInterceptor(); if (requestInterceptor != null) { requestInterceptor.onStart( @@ -378,7 +380,6 @@ final void handleStream(int streamId, ByteBuf frame, long initialRequestN) { } } } else { - final Exception leaseError = leaseHandler.leaseError(); final RequestInterceptor requestInterceptor = this.getRequestInterceptor(); if (requestInterceptor != null) { requestInterceptor.onReject( @@ -389,7 +390,9 @@ final void handleStream(int streamId, ByteBuf frame, long initialRequestN) { } final void handleChannel(int streamId, ByteBuf frame, long initialRequestN, boolean complete) { - if (leaseHandler.useLease()) { + ResponderLeaseTracker leaseHandler = this.leaseHandler; + Throwable leaseError; + if (leaseHandler == null || (leaseError = leaseHandler.use()) == null) { final RequestInterceptor requestInterceptor = this.getRequestInterceptor(); if (requestInterceptor != null) { requestInterceptor.onStart( @@ -414,7 +417,6 @@ final void handleChannel(int streamId, ByteBuf frame, long initialRequestN, bool } } } else { - final Exception leaseError = leaseHandler.leaseError(); final RequestInterceptor requestTracker = this.getRequestInterceptor(); if (requestTracker != null) { requestTracker.onReject( @@ -433,13 +435,9 @@ private void handleMetadataPush(Mono result) { result.subscribe(MetadataPushResponderSubscriber.INSTANCE); } - private boolean add(int streamId, FrameHandler frameHandler) { - FrameHandler existingHandler; - synchronized (this) { - existingHandler = super.activeStreams.putIfAbsent(streamId, frameHandler); - } - - if (existingHandler != null) { + @Override + public boolean add(int streamId, FrameHandler frameHandler) { + if (!super.add(streamId, frameHandler)) { frameHandler.handleCancel(); return false; } 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 5799d0773..5ec33e76f 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -34,12 +34,11 @@ import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.SetupFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.lease.Leases; -import io.rsocket.lease.RequesterLeaseHandler; -import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.lease.TrackingLeaseSender; import io.rsocket.plugins.DuplexConnectionInterceptor; import io.rsocket.plugins.InitializingInterceptorRegistry; import io.rsocket.plugins.InterceptorRegistry; +import io.rsocket.plugins.RequestInterceptor; import io.rsocket.resume.SessionManager; import io.rsocket.transport.ServerTransport; import java.util.Objects; @@ -66,7 +65,7 @@ public final class RSocketServer { private InitializingInterceptorRegistry interceptors = new InitializingInterceptorRegistry(); private Resume resume; - private Supplier> leasesSupplier = null; + private Consumer leaseConfigurer = null; private int mtu = 0; private int maxInboundPayloadSize = Integer.MAX_VALUE; @@ -184,21 +183,23 @@ public RSocketServer resume(Resume resume) { * *

{@code
    * RSocketServer.create(SocketAcceptor.with(new RSocket() {...}))
-   *         .lease(Leases::new)
+   *         .lease(spec ->
+   *            spec.sender(() -> Flux.interval(ofSeconds(1))
+   *                                  .map(__ -> Lease.create(ofSeconds(1), 1)))
+   *         )
    *         .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 + * @param leaseConfigurer consumer which accepts {@link LeaseSpec} and use it for configuring * @return the same instance for method chaining * @see Lease * Semantics */ - public RSocketServer lease(Supplier> supplier) { - this.leasesSupplier = supplier; + public RSocketServer lease(Consumer leaseConfigurer) { + this.leaseConfigurer = leaseConfigurer; return this; } @@ -388,7 +389,7 @@ private Mono acceptSetup( return clientServerConnection.onClose(); } - boolean leaseEnabled = leasesSupplier != null; + boolean leaseEnabled = leaseConfigurer != null; if (SetupFrameCodec.honorLease(setupFrame) && !leaseEnabled) { serverSetup.sendError( clientServerConnection, new InvalidSetupException("lease is not supported")); @@ -401,14 +402,21 @@ private Mono acceptSetup( (keepAliveHandler, wrappedDuplexConnection) -> { ConnectionSetupPayload setupPayload = new DefaultConnectionSetupPayload(setupFrame.retain()); + final InitializingInterceptorRegistry interceptors = this.interceptors; final ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(wrappedDuplexConnection, interceptors, false); - Leases leases = leaseEnabled ? leasesSupplier.get() : null; - RequesterLeaseHandler requesterLeaseHandler = - leaseEnabled - ? new RequesterLeaseHandler.Impl(SERVER_TAG, leases.receiver()) - : RequesterLeaseHandler.None; + final LeaseSpec leases; + final RequesterLeaseTracker requesterLeaseTracker; + if (leaseEnabled) { + leases = new LeaseSpec(); + leaseConfigurer.accept(leases); + requesterLeaseTracker = + new RequesterLeaseTracker(SERVER_TAG, leases.maxPendingRequests); + } else { + leases = null; + requesterLeaseTracker = null; + } RSocket rSocketRequester = new RSocketRequester( @@ -422,7 +430,7 @@ private Mono acceptSetup( setupPayload.keepAliveMaxLifetime(), keepAliveHandler, interceptors::initRequesterRequestInterceptor, - requesterLeaseHandler); + requesterLeaseTracker); RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); @@ -436,25 +444,25 @@ private Mono acceptSetup( RSocket wrappedRSocketHandler = interceptors.initResponder(rSocketHandler); DuplexConnection clientConnection = multiplexer.asClientConnection(); - ResponderLeaseHandler responderLeaseHandler = + ResponderLeaseTracker responderLeaseTracker = leaseEnabled - ? new ResponderLeaseHandler.Impl<>( - SERVER_TAG, - clientConnection.alloc(), - leases.sender(), - leases.stats()) - : ResponderLeaseHandler.None; + ? new ResponderLeaseTracker(SERVER_TAG, clientConnection, leases.sender) + : null; RSocket rSocketResponder = new RSocketResponder( clientConnection, wrappedRSocketHandler, payloadDecoder, - responderLeaseHandler, + responderLeaseTracker, mtu, maxFrameLength, maxInboundPayloadSize, - interceptors::initResponderRequestInterceptor); + leaseEnabled && leases.sender instanceof TrackingLeaseSender + ? rSocket -> + interceptors.initResponderRequestInterceptor( + rSocket, (RequestInterceptor) leases.sender) + : interceptors::initResponderRequestInterceptor); }) .doFinally(signalType -> setupPayload.release()) .then(); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java index 9b2936444..eee1346eb 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java @@ -50,7 +50,11 @@ import reactor.util.context.Context; final class RequestChannelRequesterFlux extends Flux - implements RequesterFrameHandler, CoreSubscriber, Subscription, Scannable { + implements RequesterFrameHandler, + LeasePermitHandler, + CoreSubscriber, + Subscription, + Scannable { final ByteBufAllocator allocator; final int mtu; @@ -62,6 +66,7 @@ final class RequestChannelRequesterFlux extends Flux final Publisher payloadsPublisher; + @Nullable final RequesterLeaseTracker requesterLeaseTracker; @Nullable final RequestInterceptor requestInterceptor; volatile long state; @@ -70,14 +75,16 @@ final class RequestChannelRequesterFlux extends Flux int streamId; - Context cachedContext; + boolean isFirstSignal = true; + Payload firstPayload; - boolean isFirstPayload = true; + Subscription outboundSubscription; + boolean outboundDone; + Throwable outboundError; + Context cachedContext; CoreSubscriber inboundSubscriber; - Subscription outboundSubscription; boolean inboundDone; - boolean outboundDone; CompositeByteBuf frames; @@ -91,6 +98,7 @@ final class RequestChannelRequesterFlux extends Flux this.requesterResponderSupport = requesterResponderSupport; this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); + this.requesterLeaseTracker = requesterResponderSupport.getRequesterLeaseTracker(); this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); } @@ -129,7 +137,7 @@ public final void request(long n) { return; } - long previousState = addRequestN(STATE, this, n); + long previousState = addRequestN(STATE, this, n, this.requesterLeaseTracker == null); if (isTerminated(previousState)) { return; } @@ -155,27 +163,66 @@ public void onNext(Payload p) { return; } - if (this.isFirstPayload) { - this.isFirstPayload = false; + if (this.isFirstSignal) { + this.isFirstSignal = false; - long state = this.state; - if (isTerminated(state)) { - p.release(); - return; + final RequesterLeaseTracker requesterLeaseTracker = this.requesterLeaseTracker; + final boolean leaseEnabled = requesterLeaseTracker != null; + + if (leaseEnabled) { + this.firstPayload = p; + + final long previousState = markFirstPayloadReceived(STATE, this); + if (isTerminated(previousState)) { + this.firstPayload = null; + p.release(); + return; + } + + requesterLeaseTracker.issue(this); + } else { + final long state = this.state; + if (isTerminated(state)) { + p.release(); + return; + } + // TODO: check if source is Scalar | Callable | Mono + sendFirstPayload(p, extractRequestN(state), false); } - sendFirstPayload(p, extractRequestN(state)); } else { sendFollowingPayload(p); } } - void sendFirstPayload(Payload firstPayload, long initialRequestN) { + @Override + public boolean handlePermit() { + final long previousState = markReadyToSendFirstFrame(STATE, this); + + if (isTerminated(previousState)) { + return false; + } + + final Payload firstPayload = this.firstPayload; + this.firstPayload = null; + + sendFirstPayload( + firstPayload, extractRequestN(previousState), isOutboundTerminated(previousState)); + return true; + } + + void sendFirstPayload(Payload firstPayload, long initialRequestN, boolean completed) { int mtu = this.mtu; try { if (!isValid(mtu, this.maxFrameLength, firstPayload, true)) { - lazyTerminate(STATE, this); + final long previousState = markTerminated(STATE, this); - this.outboundSubscription.cancel(); + if (isTerminated(previousState)) { + return; + } + + if (!isOutboundTerminated(previousState)) { + this.outboundSubscription.cancel(); + } final IllegalArgumentException e = new IllegalArgumentException( @@ -192,9 +239,16 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { return; } } catch (IllegalReferenceCountException e) { - lazyTerminate(STATE, this); + final long previousState = markTerminated(STATE, this); - this.outboundSubscription.cancel(); + if (isTerminated(previousState)) { + Operators.onErrorDropped(e, this.inboundSubscriber.currentContext()); + return; + } + + if (!isOutboundTerminated(previousState)) { + this.outboundSubscription.cancel(); + } final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { @@ -215,10 +269,18 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { streamId = sm.addAndGetNextStreamId(this); this.streamId = streamId; } catch (Throwable t) { - this.inboundDone = true; final long previousState = markTerminated(STATE, this); - this.outboundSubscription.cancel(); + firstPayload.release(); + + if (isTerminated(previousState)) { + Operators.onErrorDropped(t, this.inboundSubscriber.currentContext()); + return; + } + + if (!isOutboundTerminated(previousState)) { + this.outboundSubscription.cancel(); + } final Throwable ut = Exceptions.unwrap(t); final RequestInterceptor requestInterceptor = this.requestInterceptor; @@ -226,11 +288,9 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { requestInterceptor.onReject(ut, FrameType.REQUEST_CHANNEL, firstPayload.metadata()); } - firstPayload.release(); + this.inboundDone = true; + this.inboundSubscriber.onError(ut); - if (!isTerminated(previousState)) { - this.inboundSubscriber.onError(ut); - } return; } @@ -248,44 +308,80 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { firstPayload, connection, allocator, - // TODO: Should be a different flag in case of the scalar - // source or if we know in advance upstream is mono - false); + completed); } catch (Throwable t) { - lazyTerminate(STATE, this); + final long previousState = markTerminated(STATE, this); + + firstPayload.release(); + + if (isTerminated(previousState)) { + Operators.onErrorDropped(t, this.inboundSubscriber.currentContext()); + return; + } sm.remove(streamId, this); - this.outboundSubscription.cancel(); - this.inboundDone = true; + if (!isOutboundTerminated(previousState)) { + this.outboundSubscription.cancel(); + } if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, t); } + this.inboundDone = true; this.inboundSubscriber.onError(t); return; } long previousState = markFirstFrameSent(STATE, this); if (isTerminated(previousState)) { + // now, this can be terminated in case of the following scenarios: + // + // 1) SendFirst is called synchronously from onNext, thus we can have + // handleError called before we marked first frame sent, thus we may check if + // inboundDone flag is true and exit execution without any further actions: if (this.inboundDone) { return; } sm.remove(streamId, this); - ReassemblyUtils.synchronizedRelease(this, previousState); + // 2) SendFirst is called asynchronously on the connection event-loop. Thus, we + // need to check if outbound error is present. Note, we check outboundError since + // in the last scenario, cancellation may terminate the state and async + // onComplete may set outboundDone to true. Thus, we explicitly check for + // outboundError + final Throwable outboundError = this.outboundError; + if (outboundError != null) { + final ByteBuf errorFrame = ErrorFrameCodec.encode(allocator, streamId, outboundError); + connection.sendFrame(streamId, errorFrame); - final ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, streamId); - connection.sendFrame(streamId, cancelFrame); + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, outboundError); + } - if (requestInterceptor != null) { - requestInterceptor.onCancel(streamId, FrameType.REQUEST_CHANNEL); + this.inboundDone = true; + this.inboundSubscriber.onError(outboundError); + } else { + // 3) SendFirst is interleaving with cancel. Thus, we need to generate cancel + // frame + final ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, streamId); + connection.sendFrame(streamId, cancelFrame); + + if (requestInterceptor != null) { + requestInterceptor.onCancel(streamId, FrameType.REQUEST_CHANNEL); + } } + return; } + if (!completed && isOutboundTerminated(previousState)) { + final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(this.allocator, streamId); + connection.sendFrame(streamId, completeFrame); + } + if (isMaxAllowedRequestN(initialRequestN)) { return; } @@ -396,22 +492,30 @@ boolean tryCancel() { return false; } - this.outboundSubscription.cancel(); + if (!isOutboundTerminated(previousState)) { + this.outboundSubscription.cancel(); + } - if (!isFirstFrameSent(previousState)) { + if (!isReadyToSendFirstFrame(previousState) && isFirstPayloadReceived(previousState)) { + final Payload firstPayload = this.firstPayload; + this.firstPayload = null; + firstPayload.release(); // no need to send anything, since we have not started a stream yet (no logical wire) return false; } - final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); - ReassemblyUtils.synchronizedRelease(this, previousState); - final ByteBuf cancelFrame = CancelFrameCodec.encode(this.allocator, streamId); - this.connection.sendFrame(streamId, cancelFrame); + final boolean firstFrameSent = isFirstFrameSent(previousState); + if (firstFrameSent) { + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); - return true; + final ByteBuf cancelFrame = CancelFrameCodec.encode(this.allocator, streamId); + this.connection.sendFrame(streamId, cancelFrame); + } + + return firstFrameSent; } @Override @@ -421,6 +525,7 @@ public void onError(Throwable t) { return; } + this.outboundError = t; this.outboundDone = true; long previousState = markTerminated(STATE, this); @@ -429,34 +534,49 @@ public void onError(Throwable t) { return; } - if (!isFirstFrameSent(previousState)) { - // first signal, thus, just propagates error to actual subscriber + if (this.isFirstSignal) { + this.inboundDone = true; this.inboundSubscriber.onError(t); + return; + } else if (!isReadyToSendFirstFrame(previousState)) { + // first signal is received but we are still waiting for lease permit to be issued, + // thus, just propagates error to actual subscriber + + final Payload firstPayload = this.firstPayload; + this.firstPayload = null; + + firstPayload.release(); + + this.inboundDone = true; + this.inboundSubscriber.onError(t); + return; } ReassemblyUtils.synchronizedRelease(this, previousState); - final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); - // propagates error to remote responder - final ByteBuf errorFrame = ErrorFrameCodec.encode(this.allocator, streamId, t); - this.connection.sendFrame(streamId, errorFrame); + if (isFirstFrameSent(previousState)) { + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); + // propagates error to remote responder + final ByteBuf errorFrame = ErrorFrameCodec.encode(this.allocator, streamId, t); + this.connection.sendFrame(streamId, errorFrame); + + if (!isInboundTerminated(previousState)) { + // FIXME: must be scheduled on the connection event-loop to achieve serial + // behaviour on the inbound subscriber + synchronized (this) { + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, t); + } - if (!isInboundTerminated(previousState)) { - // FIXME: must be scheduled on the connection event-loop to achieve serial - // behaviour on the inbound subscriber - synchronized (this) { - final RequestInterceptor interceptor = requestInterceptor; - if (interceptor != null) { - interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, t); + this.inboundDone = true; + this.inboundSubscriber.onError(t); } - - this.inboundDone = true; - this.inboundSubscriber.onError(t); + } else { + Operators.onErrorDropped(t, this.inboundSubscriber.currentContext()); } - } else { - Operators.onErrorDropped(t, this.inboundSubscriber.currentContext()); } } @@ -474,22 +594,21 @@ public void onComplete() { } if (!isFirstFrameSent(previousState)) { - // first signal, thus, just propagates error to actual subscriber - this.inboundSubscriber.onError(new CancellationException("Empty Source")); + if (!isFirstPayloadReceived(previousState)) { + // first signal, thus, just propagates error to actual subscriber + this.inboundSubscriber.onError(new CancellationException("Empty Source")); + } return; } final int streamId = this.streamId; - - final boolean isInboundTerminated = isInboundTerminated(previousState); - if (isInboundTerminated) { - this.requesterResponderSupport.remove(streamId, this); - } - final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(this.allocator, streamId); + this.connection.sendFrame(streamId, completeFrame); - if (isInboundTerminated) { + if (isInboundTerminated(previousState)) { + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor interceptor = requestInterceptor; if (interceptor != null) { interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, null); @@ -513,15 +632,39 @@ public final void handleComplete() { if (isOutboundTerminated(previousState)) { this.requesterResponderSupport.remove(this.streamId, this); - final RequestInterceptor interceptor = requestInterceptor; + final RequestInterceptor interceptor = this.requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, null); + interceptor.onTerminate(this.streamId, FrameType.REQUEST_CHANNEL, null); } } this.inboundSubscriber.onComplete(); } + @Override + public final void handlePermitError(Throwable cause) { + this.inboundDone = true; + + long previousState = markTerminated(STATE, this); + if (isTerminated(previousState) || isInboundTerminated(previousState)) { + Operators.onErrorDropped(cause, this.inboundSubscriber.currentContext()); + return; + } + + if (!isOutboundTerminated(previousState)) { + this.outboundSubscription.cancel(); + } + + final Payload p = this.firstPayload; + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onReject(cause, FrameType.REQUEST_CHANNEL, p.metadata()); + } + p.release(); + + this.inboundSubscriber.onError(cause); + } + @Override public final void handleError(Throwable cause) { if (this.inboundDone) { @@ -532,27 +675,20 @@ public final void handleError(Throwable cause) { this.inboundDone = true; long previousState = markTerminated(STATE, this); - if (isTerminated(previousState)) { + if (isTerminated(previousState) || isInboundTerminated(previousState)) { Operators.onErrorDropped(cause, this.inboundSubscriber.currentContext()); return; - } else if (isInboundTerminated(previousState)) { - final RequestInterceptor interceptor = this.requestInterceptor; - if (interceptor != null) { - interceptor.onTerminate(this.streamId, FrameType.REQUEST_CHANNEL, cause); - } + } - Operators.onErrorDropped(cause, this.inboundSubscriber.currentContext()); - return; + if (!isOutboundTerminated(previousState)) { + this.outboundSubscription.cancel(); } ReassemblyUtils.release(this, previousState); final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); - this.outboundSubscription.cancel(); - final RequestInterceptor interceptor = requestInterceptor; if (interceptor != null) { interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, cause); @@ -625,9 +761,12 @@ public Context currentContext() { long state = this.state; if (isSubscribedOrTerminated(state)) { - Context contextWithDiscard = this.inboundSubscriber.currentContext().putAll(DISCARD_CONTEXT); - cachedContext = contextWithDiscard; - return contextWithDiscard; + Context cachedContext = this.cachedContext; + if (cachedContext == null) { + cachedContext = this.inboundSubscriber.currentContext().putAll(DISCARD_CONTEXT); + this.cachedContext = cachedContext; + } + return cachedContext; } return Context.empty(); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java index 850298a2a..a13b105b5 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java @@ -42,7 +42,7 @@ import reactor.util.annotation.Nullable; final class RequestResponseRequesterMono extends Mono - implements RequesterFrameHandler, Subscription, Scannable { + implements RequesterFrameHandler, LeasePermitHandler, Subscription, Scannable { final ByteBufAllocator allocator; final Payload payload; @@ -53,6 +53,7 @@ final class RequestResponseRequesterMono extends Mono final DuplexConnection connection; final PayloadDecoder payloadDecoder; + @Nullable final RequesterLeaseTracker requesterLeaseTracker; @Nullable final RequestInterceptor requestInterceptor; volatile long state; @@ -75,6 +76,7 @@ final class RequestResponseRequesterMono extends Mono this.requesterResponderSupport = requesterResponderSupport; this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); + this.requesterLeaseTracker = requesterResponderSupport.getRequesterLeaseTracker(); this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); } @@ -134,15 +136,35 @@ public final void request(long n) { return; } - long previousState = addRequestN(STATE, this, n); + final RequesterLeaseTracker requesterLeaseTracker = this.requesterLeaseTracker; + final boolean leaseEnabled = requesterLeaseTracker != null; + final long previousState = addRequestN(STATE, this, n, !leaseEnabled); + if (isTerminated(previousState) || hasRequested(previousState)) { return; } - sendFirstPayload(this.payload, n); + if (leaseEnabled) { + requesterLeaseTracker.issue(this); + return; + } + + sendFirstPayload(this.payload); + } + + @Override + public boolean handlePermit() { + final long previousState = markReadyToSendFirstFrame(STATE, this); + + if (isTerminated(previousState)) { + return false; + } + + sendFirstPayload(this.payload); + return true; } - void sendFirstPayload(Payload payload, long initialRequestN) { + void sendFirstPayload(Payload payload) { final RequesterResponderSupport sm = this.requesterResponderSupport; final DuplexConnection connection = this.connection; @@ -228,7 +250,7 @@ public final void cancel() { if (requestInterceptor != null) { requestInterceptor.onCancel(streamId, FrameType.REQUEST_RESPONSE); } - } else if (!hasRequested(previousState)) { + } else if (!isReadyToSendFirstFrame(previousState)) { this.payload.release(); } } @@ -285,6 +307,26 @@ public final void handleComplete() { this.actual.onComplete(); } + @Override + public final void handlePermitError(Throwable cause) { + this.done = true; + + long previousState = markTerminated(STATE, this); + if (isTerminated(previousState)) { + Operators.onErrorDropped(cause, this.actual.currentContext()); + return; + } + + final Payload p = this.payload; + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(cause, FrameType.REQUEST_RESPONSE, p.metadata()); + } + p.release(); + + this.actual.onError(cause); + } + @Override public final void handleError(Throwable cause) { if (this.done) { diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java index 47e8c1610..424451a58 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java @@ -43,7 +43,7 @@ import reactor.util.annotation.Nullable; final class RequestStreamRequesterFlux extends Flux - implements RequesterFrameHandler, Subscription, Scannable { + implements RequesterFrameHandler, LeasePermitHandler, Subscription, Scannable { final ByteBufAllocator allocator; final Payload payload; @@ -54,6 +54,7 @@ final class RequestStreamRequesterFlux extends Flux final DuplexConnection connection; final PayloadDecoder payloadDecoder; + @Nullable final RequesterLeaseTracker requesterLeaseTracker; @Nullable final RequestInterceptor requestInterceptor; volatile long state; @@ -74,6 +75,7 @@ final class RequestStreamRequesterFlux extends Flux this.requesterResponderSupport = requesterResponderSupport; this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); + this.requesterLeaseTracker = requesterResponderSupport.getRequesterLeaseTracker(); this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); } @@ -132,7 +134,9 @@ public final void request(long n) { return; } - long previousState = addRequestN(STATE, this, n); + final RequesterLeaseTracker requesterLeaseTracker = this.requesterLeaseTracker; + final boolean leaseEnabled = requesterLeaseTracker != null; + final long previousState = addRequestN(STATE, this, n, !leaseEnabled); if (isTerminated(previousState)) { return; } @@ -147,9 +151,26 @@ public final void request(long n) { return; } + if (leaseEnabled) { + requesterLeaseTracker.issue(this); + return; + } + sendFirstPayload(this.payload, n); } + @Override + public boolean handlePermit() { + final long previousState = markReadyToSendFirstFrame(STATE, this); + + if (isTerminated(previousState)) { + return false; + } + + sendFirstPayload(this.payload, extractRequestN(previousState)); + return true; + } + void sendFirstPayload(Payload payload, long initialRequestN) { final RequesterResponderSupport sm = this.requesterResponderSupport; @@ -261,7 +282,7 @@ public final void cancel() { if (requestInterceptor != null) { requestInterceptor.onCancel(streamId, FrameType.REQUEST_STREAM); } - } else if (!hasRequested(previousState)) { + } else if (!isReadyToSendFirstFrame(previousState)) { // no need to send anything, since the first request has not happened this.payload.release(); } @@ -301,6 +322,26 @@ public final void handleComplete() { this.inboundSubscriber.onComplete(); } + @Override + public final void handlePermitError(Throwable cause) { + this.done = true; + + long previousState = markTerminated(STATE, this); + if (isTerminated(previousState)) { + Operators.onErrorDropped(cause, this.inboundSubscriber.currentContext()); + return; + } + + final Payload p = this.payload; + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(cause, FrameType.REQUEST_STREAM, p.metadata()); + } + p.release(); + + this.inboundSubscriber.onError(cause); + } + @Override public final void handleError(Throwable cause) { if (this.done) { diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequesterLeaseTracker.java b/rsocket-core/src/main/java/io/rsocket/core/RequesterLeaseTracker.java new file mode 100644 index 000000000..6e7a822f1 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/RequesterLeaseTracker.java @@ -0,0 +1,134 @@ +/* + * 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.core; + +import io.netty.buffer.ByteBuf; +import io.rsocket.Availability; +import io.rsocket.frame.LeaseFrameCodec; +import io.rsocket.lease.Lease; +import io.rsocket.lease.MissingLeaseException; +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.Queue; + +final class RequesterLeaseTracker implements Availability { + + final String tag; + final int maximumAllowedAwaitingPermitHandlersNumber; + final Queue awaitingPermitHandlersQueue; + + Lease currentLease = null; + int availableRequests; + + boolean isDisposed; + Throwable t; + + RequesterLeaseTracker(String tag, int maximumAllowedAwaitingPermitHandlersNumber) { + this.tag = tag; + this.maximumAllowedAwaitingPermitHandlersNumber = maximumAllowedAwaitingPermitHandlersNumber; + this.awaitingPermitHandlersQueue = new ArrayDeque<>(); + } + + synchronized void issue(LeasePermitHandler leasePermitHandler) { + if (this.isDisposed) { + leasePermitHandler.handlePermitError(this.t); + return; + } + + final int availableRequests = this.availableRequests; + final Lease l = this.currentLease; + final boolean leaseReceived = l != null; + final boolean isExpired = leaseReceived && isExpired(l); + + if (leaseReceived && availableRequests > 0 && !isExpired) { + leasePermitHandler.handlePermit(); + this.availableRequests = availableRequests - 1; + } else { + final Queue queue = this.awaitingPermitHandlersQueue; + if (this.maximumAllowedAwaitingPermitHandlersNumber > queue.size()) { + queue.offer(leasePermitHandler); + } else { + final String tag = this.tag; + final String message; + if (!leaseReceived) { + message = String.format("[%s] Lease was not received yet", tag); + } else if (isExpired) { + message = String.format("[%s] Missing leases. Lease is expired", tag); + } else { + message = + String.format( + "[%s] Missing leases. Issued [%s] request allowance is used", + tag, availableRequests); + } + + final Throwable t = new MissingLeaseException(message); + leasePermitHandler.handlePermitError(t); + } + } + } + + void handleLeaseFrame(ByteBuf leaseFrame) { + final int numberOfRequests = LeaseFrameCodec.numRequests(leaseFrame); + final int timeToLiveMillis = LeaseFrameCodec.ttl(leaseFrame); + final ByteBuf metadata = LeaseFrameCodec.metadata(leaseFrame); + + synchronized (this) { + final Lease lease = + Lease.create(Duration.ofMillis(timeToLiveMillis), numberOfRequests, metadata); + final Queue queue = this.awaitingPermitHandlersQueue; + + int availableRequests = lease.numberOfRequests(); + + this.currentLease = lease; + if (queue.size() > 0) { + do { + final LeasePermitHandler handler = queue.poll(); + if (handler.handlePermit()) { + availableRequests--; + } + } while (availableRequests > 0 && queue.size() > 0); + } + + this.availableRequests = availableRequests; + } + } + + public synchronized void dispose(Throwable t) { + this.isDisposed = true; + this.t = t; + + final Queue queue = this.awaitingPermitHandlersQueue; + final int size = queue.size(); + + for (int i = 0; i < size; i++) { + final LeasePermitHandler leasePermitHandler = queue.poll(); + + //noinspection ConstantConditions + leasePermitHandler.handlePermitError(t); + } + } + + @Override + public synchronized double availability() { + final Lease lease = this.currentLease; + return lease != null ? this.availableRequests / (double) lease.numberOfRequests() : 0.0d; + } + + static boolean isExpired(Lease currentLease) { + return System.currentTimeMillis() >= currentLease.expirationTime(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java b/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java index 2272ceb5f..52db6e198 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java @@ -67,6 +67,11 @@ public DuplexConnection getDuplexConnection() { return connection; } + @Nullable + public RequesterLeaseTracker getRequesterLeaseTracker() { + return null; + } + @Nullable public RequestInterceptor getRequestInterceptor() { return requestInterceptor; @@ -112,6 +117,12 @@ public int addAndGetNextStreamId(FrameHandler frameHandler) { } } + public synchronized boolean add(int streamId, FrameHandler frameHandler) { + final FrameHandler previousHandler = this.activeStreams.putIfAbsent(streamId, frameHandler); + + return previousHandler == null; + } + /** * Resolves {@link FrameHandler} by {@code streamId} * diff --git a/rsocket-core/src/main/java/io/rsocket/core/ResponderLeaseTracker.java b/rsocket-core/src/main/java/io/rsocket/core/ResponderLeaseTracker.java new file mode 100644 index 000000000..fc7442f4a --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/ResponderLeaseTracker.java @@ -0,0 +1,112 @@ +/* + * 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.core; + +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Availability; +import io.rsocket.DuplexConnection; +import io.rsocket.frame.LeaseFrameCodec; +import io.rsocket.lease.Lease; +import io.rsocket.lease.LeaseSender; +import io.rsocket.lease.MissingLeaseException; +import reactor.core.Disposable; +import reactor.core.publisher.BaseSubscriber; +import reactor.util.annotation.Nullable; + +final class ResponderLeaseTracker extends BaseSubscriber + implements Disposable, Availability { + + final String tag; + final ByteBufAllocator allocator; + final DuplexConnection connection; + + @Nullable volatile MutableLease currentLease; + + ResponderLeaseTracker(String tag, DuplexConnection connection, LeaseSender leaseSender) { + this.tag = tag; + this.connection = connection; + this.allocator = connection.alloc(); + + leaseSender.send().subscribe(this); + } + + @Nullable + Throwable use() { + final MutableLease lease = this.currentLease; + final String tag = this.tag; + + if (lease == null) { + return new MissingLeaseException(String.format("[%s] Lease was not issued yet", tag)); + } + + if (isExpired(lease)) { + return new MissingLeaseException(String.format("[%s] Missing leases. Lease is expired", tag)); + } + + final int allowedRequests = lease.allowedRequests; + final int remainingRequests = lease.remainingRequests; + if (remainingRequests <= 0) { + return new MissingLeaseException( + String.format( + "[%s] Missing leases. Issued [%s] request allowance is used", tag, allowedRequests)); + } + + lease.remainingRequests = remainingRequests - 1; + + return null; + } + + @Override + protected void hookOnNext(Lease lease) { + final int allowedRequests = lease.numberOfRequests(); + final int ttl = lease.timeToLiveInMillis(); + final long expireAt = lease.expirationTime(); + + this.currentLease = new MutableLease(allowedRequests, expireAt); + this.connection.sendFrame( + 0, LeaseFrameCodec.encode(this.allocator, ttl, allowedRequests, lease.metadata())); + } + + @Override + public double availability() { + final MutableLease lease = this.currentLease; + + if (lease == null || isExpired(lease)) { + return 0; + } + + return lease.remainingRequests / (double) lease.allowedRequests; + } + + static boolean isExpired(MutableLease currentLease) { + return System.currentTimeMillis() >= currentLease.expireAt; + } + + static final class MutableLease { + final int allowedRequests; + final long expireAt; + + int remainingRequests; + + MutableLease(int allowedRequests, long expireAt) { + this.allowedRequests = allowedRequests; + this.expireAt = expireAt; + + this.remainingRequests = allowedRequests; + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/SlowFireAndForgetRequesterMono.java b/rsocket-core/src/main/java/io/rsocket/core/SlowFireAndForgetRequesterMono.java new file mode 100644 index 000000000..3035696b3 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/SlowFireAndForgetRequesterMono.java @@ -0,0 +1,255 @@ +/* + * 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 static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; +import static io.rsocket.core.PayloadValidationUtils.isValid; +import static io.rsocket.core.SendUtils.sendReleasingPayload; +import static io.rsocket.core.StateUtils.isReadyToSendFirstFrame; +import static io.rsocket.core.StateUtils.isSubscribedOrTerminated; +import static io.rsocket.core.StateUtils.isTerminated; +import static io.rsocket.core.StateUtils.lazyTerminate; +import static io.rsocket.core.StateUtils.markReadyToSendFirstFrame; +import static io.rsocket.core.StateUtils.markSubscribed; +import static io.rsocket.core.StateUtils.markTerminated; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.IllegalReferenceCountException; +import io.rsocket.DuplexConnection; +import io.rsocket.Payload; +import io.rsocket.frame.FrameType; +import io.rsocket.plugins.RequestInterceptor; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Exceptions; +import reactor.core.Scannable; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.util.annotation.NonNull; +import reactor.util.annotation.Nullable; + +final class SlowFireAndForgetRequesterMono extends Mono + implements LeasePermitHandler, Subscription, Scannable { + + volatile long state; + + static final AtomicLongFieldUpdater STATE = + AtomicLongFieldUpdater.newUpdater(SlowFireAndForgetRequesterMono.class, "state"); + + final Payload payload; + + final ByteBufAllocator allocator; + final int mtu; + final int maxFrameLength; + final RequesterResponderSupport requesterResponderSupport; + final DuplexConnection connection; + + @Nullable final RequesterLeaseTracker requesterLeaseTracker; + @Nullable final RequestInterceptor requestInterceptor; + + CoreSubscriber actual; + + SlowFireAndForgetRequesterMono( + Payload payload, RequesterResponderSupport requesterResponderSupport) { + this.allocator = requesterResponderSupport.getAllocator(); + this.payload = payload; + this.mtu = requesterResponderSupport.getMtu(); + this.maxFrameLength = requesterResponderSupport.getMaxFrameLength(); + this.requesterResponderSupport = requesterResponderSupport; + this.connection = requesterResponderSupport.getDuplexConnection(); + this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); + this.requesterLeaseTracker = requesterResponderSupport.getRequesterLeaseTracker(); + } + + @Override + public void subscribe(CoreSubscriber actual) { + final RequesterLeaseTracker requesterLeaseTracker = this.requesterLeaseTracker; + final boolean leaseEnabled = requesterLeaseTracker != null; + long previousState = markSubscribed(STATE, this, !leaseEnabled); + if (isSubscribedOrTerminated(previousState)) { + final IllegalStateException e = + new IllegalStateException("FireAndForgetMono allows only a single Subscriber"); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_FNF, null); + } + + Operators.error(actual, e); + return; + } + + final Payload p = this.payload; + int mtu = this.mtu; + try { + if (!isValid(mtu, this.maxFrameLength, p, false)) { + lazyTerminate(STATE, this); + + final IllegalArgumentException e = + new IllegalArgumentException( + String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_FNF, p.metadata()); + } + + p.release(); + + Operators.error(actual, e); + return; + } + } catch (IllegalReferenceCountException e) { + lazyTerminate(STATE, this); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_FNF, null); + } + + Operators.error(actual, e); + return; + } + + this.actual = actual; + actual.onSubscribe(this); + + if (leaseEnabled) { + requesterLeaseTracker.issue(this); + return; + } + + sendFirstFrame(p); + } + + @Override + public boolean handlePermit() { + final long previousState = markReadyToSendFirstFrame(STATE, this); + + if (isTerminated(previousState)) { + return false; + } + + sendFirstFrame(this.payload); + return true; + } + + void sendFirstFrame(Payload p) { + final CoreSubscriber actual = this.actual; + final int streamId; + try { + streamId = this.requesterResponderSupport.getNextStreamId(); + } catch (Throwable t) { + lazyTerminate(STATE, this); + + final Throwable ut = Exceptions.unwrap(t); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(ut, FrameType.REQUEST_FNF, p.metadata()); + } + + p.release(); + + actual.onError(ut); + return; + } + + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onStart(streamId, FrameType.REQUEST_FNF, p.metadata()); + } + + try { + if (isTerminated(this.state)) { + p.release(); + + if (interceptor != null) { + interceptor.onCancel(streamId, FrameType.REQUEST_FNF); + } + + return; + } + + sendReleasingPayload( + streamId, FrameType.REQUEST_FNF, mtu, p, this.connection, this.allocator, true); + } catch (Throwable e) { + lazyTerminate(STATE, this); + + if (interceptor != null) { + interceptor.onTerminate(streamId, FrameType.REQUEST_FNF, e); + } + + actual.onError(e); + return; + } + + lazyTerminate(STATE, this); + + if (interceptor != null) { + interceptor.onTerminate(streamId, FrameType.REQUEST_FNF, null); + } + + actual.onComplete(); + } + + @Override + public void request(long n) { + // no ops + } + + @Override + public void cancel() { + final long previousState = markTerminated(STATE, this); + + if (isTerminated(previousState)) { + return; + } + + if (!isReadyToSendFirstFrame(previousState)) { + this.payload.release(); + } + } + + @Override + public final void handlePermitError(Throwable cause) { + long previousState = markTerminated(STATE, this); + if (isTerminated(previousState)) { + Operators.onErrorDropped(cause, this.actual.currentContext()); + return; + } + + final Payload p = this.payload; + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(cause, FrameType.REQUEST_RESPONSE, p.metadata()); + } + + p.release(); + + this.actual.onError(cause); + } + + @Override + public Object scanUnsafe(Attr key) { + return null; // no particular key to be represented, still useful in hooks + } + + @Override + @NonNull + public String stepName() { + return "source(FireAndForgetMono)"; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/StateUtils.java b/rsocket-core/src/main/java/io/rsocket/core/StateUtils.java index b3857bc12..2b6a0e09a 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/StateUtils.java +++ b/rsocket-core/src/main/java/io/rsocket/core/StateUtils.java @@ -13,27 +13,36 @@ final class StateUtils { /** Bit Flag that indicates Requester Producer has been subscribed once */ static final long SUBSCRIBED_FLAG = 0b000000000000000000000000000000001_0000000000000000000000000000000L; + /** Bit Flag that indicates that the first payload in RequestChannel scenario is received */ + static final long FIRST_PAYLOAD_RECEIVED_FLAG = + 0b000000000000000000000000000000010_0000000000000000000000000000000L; + /** + * Bit Flag that indicates that the logical stream is ready to send the first initial frame + * (applicable for requester only) + */ + static final long READY_TO_SEND_FIRST_FRAME_FLAG = + 0b000000000000000000000000000000100_0000000000000000000000000000000L; /** * Bit Flag that indicates that sent first initial frame was sent (in case of requester) or * consumed (if responder) */ static final long FIRST_FRAME_SENT_FLAG = - 0b000000000000000000000000000000010_0000000000000000000000000000000L; + 0b000000000000000000000000000001000_0000000000000000000000000000000L; /** Bit Flag that indicates that there is a frame being reassembled */ static final long REASSEMBLING_FLAG = - 0b000000000000000000000000000000100_0000000000000000000000000000000L; + 0b000000000000000000000000000010000_0000000000000000000000000000000L; /** * Bit Flag that indicates requestChannel stream is half terminated. In this case flag indicates * that the inbound is terminated */ static final long INBOUND_TERMINATED_FLAG = - 0b000000000000000000000000000001000_0000000000000000000000000000000L; + 0b000000000000000000000000000100000_0000000000000000000000000000000L; /** * Bit Flag that indicates requestChannel stream is half terminated. In this case flag indicates * that the outbound is terminated */ static final long OUTBOUND_TERMINATED_FLAG = - 0b000000000000000000000000000010000_0000000000000000000000000000000L; + 0b000000000000000000000000001000000_0000000000000000000000000000000L; /** Initial state for any request operator */ static final long UNSUBSCRIBED_STATE = 0b000000000000000000000000000000000_0000000000000000000000000000000L; @@ -54,6 +63,24 @@ final class StateUtils { * @return return previous state before setting the new one */ static long markSubscribed(AtomicLongFieldUpdater updater, T instance) { + return markSubscribed(updater, instance, false); + } + + /** + * Adds (if possible) to the given state the {@link #SUBSCRIBED_FLAG} flag which indicates that + * the given stream has already been subscribed once + * + *

Note, the flag will not be added if the stream has already been terminated or if the stream + * has already been subscribed once + * + * @param updater of the volatile state field + * @param instance instance holder of the volatile state + * @param markPrepared indicates whether the given instance should be marked as prepared + * @param generic type of the instance + * @return return previous state before setting the new one + */ + static long markSubscribed( + AtomicLongFieldUpdater updater, T instance, boolean markPrepared) { for (; ; ) { long state = updater.get(instance); @@ -65,7 +92,10 @@ static long markSubscribed(AtomicLongFieldUpdater updater, T instance) { return state; } - if (updater.compareAndSet(instance, state, state | SUBSCRIBED_FLAG)) { + if (updater.compareAndSet( + instance, + state, + state | SUBSCRIBED_FLAG | (markPrepared ? READY_TO_SEND_FIRST_FRAME_FLAG : 0))) { return state; } } @@ -121,6 +151,86 @@ static boolean isFirstFrameSent(long state) { return (state & FIRST_FRAME_SENT_FLAG) == FIRST_FRAME_SENT_FLAG; } + /** + * Adds (if possible) to the given state the {@link #READY_TO_SEND_FIRST_FRAME_FLAG} flag which + * indicates that the logical stream is ready for initial frame sending. + * + *

Note, the flag will not be added if the stream has already been terminated or if the stream + * has already been marked as prepared + * + * @param updater of the volatile state field + * @param instance instance holder of the volatile state + * @param generic type of the instance + * @return return previous state before setting the new one + */ + static long markReadyToSendFirstFrame(AtomicLongFieldUpdater updater, T instance) { + for (; ; ) { + long state = updater.get(instance); + + if (state == TERMINATED_STATE) { + return TERMINATED_STATE; + } + + if ((state & READY_TO_SEND_FIRST_FRAME_FLAG) == READY_TO_SEND_FIRST_FRAME_FLAG) { + return state; + } + + if (updater.compareAndSet(instance, state, state | READY_TO_SEND_FIRST_FRAME_FLAG)) { + return state; + } + } + } + + /** + * Indicates that the logical stream is ready for initial frame sending + * + * @param state to check whether stream is prepared for initial frame sending + * @return true if the {@link #READY_TO_SEND_FIRST_FRAME_FLAG} flag is set + */ + static boolean isReadyToSendFirstFrame(long state) { + return (state & READY_TO_SEND_FIRST_FRAME_FLAG) == READY_TO_SEND_FIRST_FRAME_FLAG; + } + + /** + * Adds (if possible) to the given state the {@link #FIRST_PAYLOAD_RECEIVED_FLAG} flag which + * indicates that the logical stream is ready for initial frame sending. + * + *

Note, the flag will not be added if the stream has already been terminated or if the stream + * has already been marked as prepared + * + * @param updater of the volatile state field + * @param instance instance holder of the volatile state + * @param generic type of the instance + * @return return previous state before setting the new one + */ + static long markFirstPayloadReceived(AtomicLongFieldUpdater updater, T instance) { + for (; ; ) { + long state = updater.get(instance); + + if (state == TERMINATED_STATE) { + return TERMINATED_STATE; + } + + if ((state & FIRST_PAYLOAD_RECEIVED_FLAG) == FIRST_PAYLOAD_RECEIVED_FLAG) { + return state; + } + + if (updater.compareAndSet(instance, state, state | FIRST_PAYLOAD_RECEIVED_FLAG)) { + return state; + } + } + } + + /** + * Indicates that the logical stream is ready for initial frame sending + * + * @param state to check whether stream is established + * @return true if the {@link #FIRST_PAYLOAD_RECEIVED_FLAG} flag is set + */ + static boolean isFirstPayloadReceived(long state) { + return (state & FIRST_PAYLOAD_RECEIVED_FLAG) == FIRST_PAYLOAD_RECEIVED_FLAG; + } + /** * Adds (if possible) to the given state the {@link #REASSEMBLING_FLAG} flag which indicates that * there is a payload reassembling in progress. @@ -327,14 +437,12 @@ static boolean isSubscribedOrTerminated(long state) { return state == TERMINATED_STATE || (state & SUBSCRIBED_FLAG) == SUBSCRIBED_FLAG; } - /** - * @param updater - * @param instance - * @param toAdd - * @param - * @return - */ static long addRequestN(AtomicLongFieldUpdater updater, T instance, long toAdd) { + return addRequestN(updater, instance, toAdd, false); + } + + static long addRequestN( + AtomicLongFieldUpdater updater, T instance, long toAdd, boolean markPrepared) { long currentState, flags, requestN, nextRequestN; for (; ; ) { currentState = updater.get(instance); @@ -348,7 +456,7 @@ static long addRequestN(AtomicLongFieldUpdater updater, T instance, long return currentState; } - flags = currentState & FLAGS_MASK; + flags = (currentState & FLAGS_MASK) | (markPrepared ? READY_TO_SEND_FIRST_FRAME_FLAG : 0); nextRequestN = addRequestN(requestN, toAdd); if (updater.compareAndSet(instance, currentState, nextRequestN | flags)) { 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 673b4a480..9e76d176d 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/Lease.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/Lease.java @@ -18,51 +18,62 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -import io.rsocket.Availability; +import java.time.Duration; 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 { +public final class Lease { - static Lease create(int timeToLiveMillis, int numberOfRequests, @Nullable ByteBuf metadata) { - return LeaseImpl.create(timeToLiveMillis, numberOfRequests, metadata); + public static Lease create( + Duration timeToLive, int numberOfRequests, @Nullable ByteBuf metadata) { + return new Lease(timeToLive, numberOfRequests, metadata); } - static Lease create(int timeToLiveMillis, int numberOfRequests) { - return create(timeToLiveMillis, numberOfRequests, Unpooled.EMPTY_BUFFER); + public static Lease create(Duration timeToLive, int numberOfRequests) { + return create(timeToLive, numberOfRequests, Unpooled.EMPTY_BUFFER); } - /** - * Number of requests allowed by this lease. - * - * @return The number of requests allowed by this lease. - */ - int getAllowedRequests(); + public static Lease unbounded() { + return unbounded(null); + } - /** - * 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"); + public static Lease unbounded(@Nullable ByteBuf metadata) { + return create(Duration.ofMillis(Integer.MAX_VALUE), Integer.MAX_VALUE, metadata); + } + + public static Lease empty() { + return create(Duration.ZERO, 0); + } + + final int timeToLiveMillis; + final int numberOfRequests; + final ByteBuf metadata; + final long expirationTime; + + Lease(Duration timeToLive, int numberOfRequests, @Nullable ByteBuf metadata) { + this.numberOfRequests = numberOfRequests; + this.timeToLiveMillis = (int) Math.min(timeToLive.toMillis(), Integer.MAX_VALUE); + this.metadata = metadata == null ? Unpooled.EMPTY_BUFFER : metadata; + this.expirationTime = + timeToLive.isZero() ? 0 : System.currentTimeMillis() + timeToLive.toMillis(); } /** - * Number of milliseconds that this lease is valid from the time it is received. + * Number of requests allowed by this lease. * - * @return Number of milliseconds that this lease is valid from the time it is received. + * @return The number of requests allowed by this lease. */ - int getTimeToLiveMillis(); + public int numberOfRequests() { + return numberOfRequests; + } /** - * Number of milliseconds that this lease is still valid from now. + * Time to live for the given lease * - * @param now millis since epoch - * @return Number of milliseconds that this lease is still valid from now, or 0 if expired. + * @return relative duration in milliseconds */ - default int getRemainingTimeToLiveMillis(long now) { - return isEmpty() ? 0 : (int) Math.max(0, expiry() - now); + public int timeToLiveInMillis() { + return this.timeToLiveMillis; } /** @@ -70,41 +81,29 @@ default int getRemainingTimeToLiveMillis(long now) { * * @return Absolute time since epoch at which this lease will expire. */ - long expiry(); + public long expirationTime() { + return expirationTime; + } /** * Metadata for the lease. * * @return Metadata for the lease. */ - ByteBuf getMetadata(); - - /** - * Checks if the lease is expired now. - * - * @return {@code true} if the lease has expired. - */ - default boolean isExpired() { - return isExpired(System.currentTimeMillis()); - } - - /** - * Checks if the lease is expired for the passed {@code now}. - * - * @param now current time in millis. - * @return {@code true} if the lease has expired. - */ - 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; + @Nullable + public ByteBuf metadata() { + return metadata; } - /** Checks if the lease is empty(default value if no lease was received yet) */ - default boolean isEmpty() { - return getAllowedRequests() == 0 && getTimeToLiveMillis() == 0; + @Override + public String toString() { + return "Lease{" + + "timeToLiveMillis=" + + timeToLiveMillis + + ", numberOfRequests=" + + numberOfRequests + + ", expirationTime=" + + expirationTime + + '}'; } } diff --git a/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java b/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java deleted file mode 100644 index 7abb8aab9..000000000 --- a/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java +++ /dev/null @@ -1,125 +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.lease; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import java.util.concurrent.atomic.AtomicInteger; -import reactor.util.annotation.Nullable; - -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; - - 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); - } - - 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 int getTimeToLiveMillis() { - return timeToLiveMillis; - } - - @Override - public int getAllowedRequests() { - return Math.max(0, allowedRequests.get()); - } - - @Override - public int getStartingAllowedRequests() { - return startingAllowedRequests; - } - - @Override - public ByteBuf getMetadata() { - return metadata; - } - - @Override - public long expiry() { - return expiry; - } - - @Override - 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{" - + "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/LeaseSender.java b/rsocket-core/src/main/java/io/rsocket/lease/LeaseSender.java new file mode 100644 index 000000000..48bd38494 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/lease/LeaseSender.java @@ -0,0 +1,8 @@ +package io.rsocket.lease; + +import reactor.core.publisher.Flux; + +public interface LeaseSender { + + Flux send(); +} diff --git a/rsocket-core/src/main/java/io/rsocket/lease/LeaseStats.java b/rsocket-core/src/main/java/io/rsocket/lease/LeaseStats.java deleted file mode 100644 index 791f5a023..000000000 --- a/rsocket-core/src/main/java/io/rsocket/lease/LeaseStats.java +++ /dev/null @@ -1,28 +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.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 deleted file mode 100644 index 4c90e38ce..000000000 --- a/rsocket-core/src/main/java/io/rsocket/lease/Leases.java +++ /dev/null @@ -1,65 +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.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/MissingLeaseException.java b/rsocket-core/src/main/java/io/rsocket/lease/MissingLeaseException.java index 3b6cec62c..84af91b1b 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/MissingLeaseException.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/MissingLeaseException.java @@ -16,35 +16,16 @@ package io.rsocket.lease; import io.rsocket.exceptions.RejectedException; -import java.util.Objects; -import reactor.util.annotation.Nullable; public class MissingLeaseException extends RejectedException { private static final long serialVersionUID = -6169748673403858959L; - public MissingLeaseException(Lease lease, String tag) { - super(leaseMessage(Objects.requireNonNull(lease), Objects.requireNonNull(tag))); - } - - public MissingLeaseException(String tag) { - super(leaseMessage(null, Objects.requireNonNull(tag))); + public MissingLeaseException(String message) { + super(message); } @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/lease/RequesterLeaseHandler.java b/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java deleted file mode 100644 index fd569a2c8..000000000 --- a/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java +++ /dev/null @@ -1,113 +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.lease; - -import io.netty.buffer.ByteBuf; -import io.rsocket.Availability; -import io.rsocket.frame.LeaseFrameCodec; -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 = 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); - } - - @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 deleted file mode 100644 index df8787cb7..000000000 --- a/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java +++ /dev/null @@ -1,146 +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.lease; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.rsocket.Availability; -import io.rsocket.frame.LeaseFrameCodec; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Function; -import reactor.core.Disposable; -import reactor.core.Disposables; -import reactor.core.publisher.Flux; -import reactor.util.annotation.Nullable; - -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 Optional leaseStatsOption; - private final T leaseStats; - - public Impl( - String tag, - ByteBufAllocator allocator, - Function, Flux> leaseSender, - Optional leaseStatsOption) { - this.tag = tag; - this.allocator = allocator; - this.leaseSender = leaseSender; - 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)); - }); - } - - @Override - public double availability() { - return currentLease.availability(); - } - - private ByteBuf createLeaseFrame(Lease lease) { - return LeaseFrameCodec.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/lease/TrackingLeaseSender.java b/rsocket-core/src/main/java/io/rsocket/lease/TrackingLeaseSender.java new file mode 100644 index 000000000..3e6f68321 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/lease/TrackingLeaseSender.java @@ -0,0 +1,5 @@ +package io.rsocket.lease; + +import io.rsocket.plugins.RequestInterceptor; + +public interface TrackingLeaseSender extends LeaseSender, RequestInterceptor {} diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java index 24ed2933c..bf6f53830 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java @@ -387,8 +387,13 @@ static List wrap(PooledRSocket[] activeSockets) { @Override public RSocket get(int index) { final PooledRSocket socket = activeSockets[index]; - final RSocket realValue = socket.valueIfResolved(); + RSocket realValue = socket.value; + if (realValue != null) { + return realValue; + } + + realValue = socket.valueIfResolved(); if (realValue != null) { return realValue; } diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/CompositeRequestInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/CompositeRequestInterceptor.java index d455c79ba..9a134153d 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/CompositeRequestInterceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/CompositeRequestInterceptor.java @@ -1,10 +1,8 @@ package io.rsocket.plugins; import io.netty.buffer.ByteBuf; -import io.rsocket.RSocket; import io.rsocket.frame.FrameType; import java.util.List; -import java.util.function.Function; import reactor.core.publisher.Operators; import reactor.util.annotation.Nullable; import reactor.util.context.Context; @@ -13,7 +11,7 @@ class CompositeRequestInterceptor implements RequestInterceptor { final RequestInterceptor[] requestInterceptors; - public CompositeRequestInterceptor(RequestInterceptor[] requestInterceptors) { + CompositeRequestInterceptor(RequestInterceptor[] requestInterceptors) { this.requestInterceptors = requestInterceptors; } @@ -80,16 +78,14 @@ public void onReject( } @Nullable - static RequestInterceptor create( - RSocket rSocket, List> interceptors) { + static RequestInterceptor create(List interceptors) { switch (interceptors.size()) { case 0: return null; case 1: - return new SafeRequestInterceptor(interceptors.get(0).apply(rSocket)); + return new SafeRequestInterceptor(interceptors.get(0)); default: - return new CompositeRequestInterceptor( - interceptors.stream().map(f -> f.apply(rSocket)).toArray(RequestInterceptor[]::new)); + return new CompositeRequestInterceptor(interceptors.toArray(new RequestInterceptor[0])); } } 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 2c53fb6b2..7c9a90f54 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java @@ -18,6 +18,8 @@ import io.rsocket.DuplexConnection; import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; +import java.util.stream.Collectors; +import java.util.stream.Stream; import reactor.util.annotation.Nullable; /** @@ -29,13 +31,22 @@ public class InitializingInterceptorRegistry extends InterceptorRegistry { @Nullable public RequestInterceptor initRequesterRequestInterceptor(RSocket rSocketRequester) { return CompositeRequestInterceptor.create( - rSocketRequester, getRequestInterceptorsForRequester()); + getRequestInterceptorsForRequester() + .stream() + .map(factory -> factory.apply(rSocketRequester)) + .collect(Collectors.toList())); } @Nullable - public RequestInterceptor initResponderRequestInterceptor(RSocket rSocketResponder) { + public RequestInterceptor initResponderRequestInterceptor( + RSocket rSocketResponder, RequestInterceptor... perConnectionInterceptors) { return CompositeRequestInterceptor.create( - rSocketResponder, getRequestInterceptorsForResponder()); + Stream.concat( + Stream.of(perConnectionInterceptors), + getRequestInterceptorsForResponder() + .stream() + .map(inteptorFactory -> inteptorFactory.apply(rSocketResponder))) + .collect(Collectors.toList())); } public DuplexConnection initConnection( diff --git a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java index b77e51537..1e3f86a7c 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -32,7 +32,6 @@ import io.rsocket.frame.PayloadFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.subscriber.AssertSubscriber; -import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.util.ByteBufPayload; import java.time.Duration; import java.util.ArrayList; @@ -544,7 +543,7 @@ protected RSocketRequester newRSocket() { Integer.MAX_VALUE, null, __ -> null, - RequesterLeaseHandler.None); + null); } public int getStreamIdForRequestType(FrameType expectedFrameType) { 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 a895fc5ad..5bd5f9999 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java @@ -29,7 +29,7 @@ // import io.rsocket.frame.FrameHeaderCodec; // import io.rsocket.frame.FrameType; // import io.rsocket.frame.KeepAliveFrameCodec; -// import io.rsocket.lease.RequesterLeaseHandler; +// import io.rsocket.core.RequesterLeaseHandler; // import io.rsocket.resume.InMemoryResumableFramesStore; //// import io.rsocket.resume.ResumableDuplexConnection; // import io.rsocket.test.util.TestDuplexConnection; 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 a36415cb1..a4978bd4f 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.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; @@ -45,7 +44,7 @@ import io.rsocket.frame.SetupFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.subscriber.AssertSubscriber; -import io.rsocket.lease.*; +import io.rsocket.lease.Lease; import io.rsocket.lease.MissingLeaseException; import io.rsocket.plugins.InitializingInterceptorRegistry; import io.rsocket.test.util.TestClientTransport; @@ -56,9 +55,7 @@ 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.BiFunction; import java.util.stream.Stream; import org.assertj.core.api.Assertions; @@ -81,15 +78,14 @@ class RSocketLeaseTest { private static final String TAG = "test"; private RSocket rSocketRequester; - private ResponderLeaseHandler responderLeaseHandler; + private ResponderLeaseTracker responderLeaseTracker; private LeaksTrackingByteBufAllocator byteBufAllocator; private TestDuplexConnection connection; private RSocketResponder rSocketResponder; private RSocket mockRSocketHandler; private EmitterProcessor leaseSender = EmitterProcessor.create(); - private Flux leaseReceiver; - private RequesterLeaseHandler requesterLeaseHandler; + private RequesterLeaseTracker requesterLeaseTracker; @BeforeEach void setUp() { @@ -97,10 +93,8 @@ void setUp() { byteBufAllocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); connection = new TestDuplexConnection(byteBufAllocator); - requesterLeaseHandler = new RequesterLeaseHandler.Impl(TAG, leases -> leaseReceiver = leases); - responderLeaseHandler = - new ResponderLeaseHandler.Impl<>( - TAG, byteBufAllocator, stats -> leaseSender, Optional.empty()); + requesterLeaseTracker = new RequesterLeaseTracker(TAG, 0); + responderLeaseTracker = new ResponderLeaseTracker(TAG, connection, () -> leaseSender); ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(connection, new InitializingInterceptorRegistry(), true); @@ -116,7 +110,7 @@ void setUp() { 0, null, __ -> null, - requesterLeaseHandler); + requesterLeaseTracker); mockRSocketHandler = mock(RSocket.class); when(mockRSocketHandler.metadataPush(any())) @@ -179,7 +173,7 @@ protected void hookOnError(Throwable throwable) { multiplexer.asServerConnection(), mockRSocketHandler, payloadDecoder, - responderLeaseHandler, + responderLeaseTracker, 0, FRAME_LENGTH_MASK, Integer.MAX_VALUE, @@ -216,7 +210,7 @@ public void serverRSocketFactoryRejectsUnsupportedLease() { @Test public void clientRSocketFactorySetsLeaseFlag() { TestClientTransport clientTransport = new TestClientTransport(); - RSocketConnector.create().lease(Leases::new).connect(clientTransport).block(); + RSocketConnector.create().lease().connect(clientTransport).block(); Collection sent = clientTransport.testConnection().getSent(); Assertions.assertThat(sent).hasSize(1); @@ -245,7 +239,7 @@ void requesterMissingLeaseRequestsAreRejected( void requesterPresentLeaseRequestsAreAccepted( BiFunction> interaction, FrameType frameType) { ByteBuf frame = leaseFrame(5_000, 2, Unpooled.EMPTY_BUFFER); - requesterLeaseHandler.receive(frame); + requesterLeaseTracker.handleLeaseFrame(frame); Assertions.assertThat(rSocketRequester.availability()).isCloseTo(1.0, offset(1e-2)); ByteBuf buffer = byteBufAllocator.buffer(); @@ -297,13 +291,13 @@ void requesterDepletedAllowedLeaseRequestsAreRejected( buffer.writeCharSequence("test", CharsetUtil.UTF_8); Payload payload1 = ByteBufPayload.create(buffer); ByteBuf leaseFrame = leaseFrame(5_000, 1, Unpooled.EMPTY_BUFFER); - requesterLeaseHandler.receive(leaseFrame); + requesterLeaseTracker.handleLeaseFrame(leaseFrame); - double initialAvailability = requesterLeaseHandler.availability(); + double initialAvailability = requesterLeaseTracker.availability(); Publisher request = interaction.apply(rSocketRequester, payload1); // ensures that lease is not used until the frame is sent - Assertions.assertThat(initialAvailability).isEqualTo(requesterLeaseHandler.availability()); + Assertions.assertThat(initialAvailability).isEqualTo(requesterLeaseTracker.availability()); Assertions.assertThat(connection.getSent()).hasSize(0); AssertSubscriber assertSubscriber = AssertSubscriber.create(0); @@ -312,7 +306,7 @@ void requesterDepletedAllowedLeaseRequestsAreRejected( // if request is FNF, then request frame is sent on subscribe // otherwise we need to make request(1) if (interactionType != REQUEST_FNF) { - Assertions.assertThat(initialAvailability).isEqualTo(requesterLeaseHandler.availability()); + Assertions.assertThat(initialAvailability).isEqualTo(requesterLeaseTracker.availability()); Assertions.assertThat(connection.getSent()).hasSize(0); assertSubscriber.request(1); @@ -357,7 +351,7 @@ void requesterDepletedAllowedLeaseRequestsAreRejected( void requesterExpiredLeaseRequestsAreRejected( BiFunction> interaction) { ByteBuf frame = leaseFrame(50, 1, Unpooled.EMPTY_BUFFER); - requesterLeaseHandler.receive(frame); + requesterLeaseTracker.handleLeaseFrame(frame); ByteBuf buffer = byteBufAllocator.buffer(); buffer.writeCharSequence("test", CharsetUtil.UTF_8); @@ -376,7 +370,7 @@ void requesterExpiredLeaseRequestsAreRejected( @Test void requesterAvailabilityRespectsTransport() { - requesterLeaseHandler.receive(leaseFrame(5_000, 1, Unpooled.EMPTY_BUFFER)); + requesterLeaseTracker.handleLeaseFrame(leaseFrame(5_000, 1, Unpooled.EMPTY_BUFFER)); double unavailable = 0.0; connection.setAvailability(unavailable); Assertions.assertThat(rSocketRequester.availability()).isCloseTo(unavailable, offset(1e-2)); @@ -431,7 +425,7 @@ void responderMissingLeaseRequestsAreRejected(FrameType frameType) { @ParameterizedTest @MethodSource("responderInteractions") void responderPresentLeaseRequestsAreAccepted(FrameType frameType) { - leaseSender.onNext(Lease.create(5_000, 2)); + leaseSender.onNext(Lease.create(Duration.ofMillis(5_000), 2)); ByteBuf buffer = byteBufAllocator.buffer(); buffer.writeCharSequence("test", CharsetUtil.UTF_8); @@ -498,7 +492,7 @@ void responderPresentLeaseRequestsAreAccepted(FrameType frameType) { @ParameterizedTest @MethodSource("responderInteractions") void responderDepletedAllowedLeaseRequestsAreRejected(FrameType frameType) { - leaseSender.onNext(Lease.create(5_000, 1)); + leaseSender.onNext(Lease.create(Duration.ofMillis(5_000), 1)); ByteBuf buffer = byteBufAllocator.buffer(); buffer.writeCharSequence("test", CharsetUtil.UTF_8); @@ -592,7 +586,7 @@ void responderDepletedAllowedLeaseRequestsAreRejected(FrameType frameType) { @ParameterizedTest @MethodSource("interactions") void expiredLeaseRequestsAreRejected(BiFunction> interaction) { - leaseSender.onNext(Lease.create(50, 1)); + leaseSender.onNext(Lease.create(Duration.ofMillis(50), 1)); ByteBuf buffer = byteBufAllocator.buffer(); buffer.writeCharSequence("test", CharsetUtil.UTF_8); @@ -621,7 +615,7 @@ void sendLease() { metadata.writeCharSequence(metadataContent, utf8); int ttl = 5_000; int numberOfRequests = 2; - leaseSender.onNext(Lease.create(5_000, 2, metadata)); + leaseSender.onNext(Lease.create(Duration.ofMillis(5_000), 2, metadata)); ByteBuf leaseFrame = connection @@ -637,30 +631,31 @@ void sendLease() { .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); - - ReferenceCountUtil.safeRelease(leaseFrame); - } + // @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.metadata().toString(utf8)).isEqualTo(metadataContent); + // + // ReferenceCountUtil.safeRelease(leaseFrame); + // } ByteBuf leaseFrame(int ttl, int requests, ByteBuf metadata) { return LeaseFrameCodec.encode(byteBufAllocator, ttl, requests, metadata); 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 4c7921db1..25d91b25b 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -28,7 +28,6 @@ import io.rsocket.frame.PayloadFrameCodec; 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; import java.util.Arrays; @@ -78,7 +77,7 @@ void setUp() { 0, null, __ -> null, - RequesterLeaseHandler.None); + null); } @ParameterizedTest 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 9476be4d8..0904abe0d 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -62,7 +62,6 @@ import io.rsocket.frame.RequestStreamFrameCodec; 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; @@ -1420,7 +1419,7 @@ protected RSocketRequester newRSocket() { Integer.MAX_VALUE, null, (__) -> null, - RequesterLeaseHandler.None); + null); } public int getStreamIdForRequestType(FrameType expectedFrameType) { 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 50db22826..b82848b91 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -62,7 +62,6 @@ import io.rsocket.frame.RequestStreamFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.subscriber.AssertSubscriber; -import io.rsocket.lease.ResponderLeaseHandler; import io.rsocket.plugins.RequestInterceptor; import io.rsocket.plugins.TestRequestInterceptor; import io.rsocket.test.util.TestDuplexConnection; @@ -1254,7 +1253,7 @@ protected RSocketResponder newRSocket() { connection, acceptingSocket, PayloadDecoder.ZERO_COPY, - ResponderLeaseHandler.None, + null, 0, maxFrameLength, maxInboundPayloadSize, 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 785532bcf..62b449be1 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -27,8 +27,6 @@ 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; import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; @@ -566,7 +564,7 @@ public Flux requestChannel(Publisher payloads) { serverConnection, requestAcceptor, PayloadDecoder.DEFAULT, - ResponderLeaseHandler.None, + null, 0, FRAME_LENGTH_MASK, Integer.MAX_VALUE, @@ -584,7 +582,7 @@ public Flux requestChannel(Publisher payloads) { 0, null, __ -> null, - RequesterLeaseHandler.None); + null); } 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 fe3f75e1b..173385b55 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -14,7 +14,6 @@ import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; import io.rsocket.frame.SetupFrameCodec; -import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.transport.ServerTransport; import io.rsocket.util.DefaultPayload; @@ -62,7 +61,7 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { 0, null, __ -> null, - RequesterLeaseHandler.None); + null); String errorMsg = "error"; @@ -100,7 +99,7 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { 0, null, __ -> null, - RequesterLeaseHandler.None); + null); conn.addToReceivedBuffer( ErrorFrameCodec.encode(ByteBufAllocator.DEFAULT, 0, new RejectedSetupException("error"))); diff --git a/rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java b/rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java index d5b2eeb41..ec8725b1e 100644 --- a/rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java +++ b/rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java @@ -18,69 +18,63 @@ 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()); - } + // + // @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-examples/build.gradle b/rsocket-examples/build.gradle index c86befc92..e2a7ad31a 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -23,6 +23,9 @@ dependencies { implementation project(':rsocket-load-balancer') implementation project(':rsocket-transport-local') implementation project(':rsocket-transport-netty') + + implementation 'com.netflix.concurrency-limits:concurrency-limits-core' + runtimeOnly 'ch.qos.logback:logback-classic' testImplementation project(':rsocket-test') diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/common/LeaseManager.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/common/LeaseManager.java new file mode 100644 index 000000000..272caf7a0 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/common/LeaseManager.java @@ -0,0 +1,144 @@ +package io.rsocket.examples.transport.tcp.lease.advanced.common; + +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +public class LeaseManager implements Runnable { + + static final Logger logger = LoggerFactory.getLogger(LeaseManager.class); + + volatile int activeConnectionsCount; + static final AtomicIntegerFieldUpdater ACTIVE_CONNECTIONS_COUNT = + AtomicIntegerFieldUpdater.newUpdater(LeaseManager.class, "activeConnectionsCount"); + + volatile int stateAndInFlight; + static final AtomicIntegerFieldUpdater STATE_AND_IN_FLIGHT = + AtomicIntegerFieldUpdater.newUpdater(LeaseManager.class, "stateAndInFlight"); + + static final int MASK_PAUSED = 0b1_000_0000_0000_0000_0000_0000_0000_0000; + static final int MASK_IN_FLIGHT = 0b0_111_1111_1111_1111_1111_1111_1111_1111; + + final BlockingDeque sendersQueue = new LinkedBlockingDeque<>(); + final Scheduler worker = Schedulers.newSingle(LeaseManager.class.getName()); + + final int capacity; + final int ttl; + + public LeaseManager(int capacity, int ttl) { + this.capacity = capacity; + this.ttl = ttl; + } + + @Override + public void run() { + try { + LimitBasedLeaseSender leaseSender = sendersQueue.poll(); + + if (leaseSender == null) { + return; + } + + if (leaseSender.isDisposed()) { + logger.debug("Connection[" + leaseSender.connectionId + "]: LeaseSender is Disposed"); + worker.schedule(this); + return; + } + + int limit = leaseSender.limitAlgorithm.getLimit(); + + if (limit == 0) { + throw new IllegalStateException("Limit is 0"); + } + + if (pauseIfNoCapacity()) { + sendersQueue.addFirst(leaseSender); + logger.debug("Pause execution. Not enough capacity"); + return; + } + + leaseSender.sendLease(ttl, limit); + sendersQueue.offer(leaseSender); + + int activeConnections = activeConnectionsCount; + int nextDelay = activeConnections == 0 ? ttl : (ttl / activeConnections); + + logger.debug("Next check happens in " + nextDelay + "ms"); + + worker.schedule(this, nextDelay, TimeUnit.MILLISECONDS); + } catch (Throwable e) { + logger.error("LeaseSender failed to send lease", e); + } + } + + int incrementInFlightAndGet() { + for (; ; ) { + int state = stateAndInFlight; + int paused = state & MASK_PAUSED; + int inFlight = stateAndInFlight & MASK_IN_FLIGHT; + + // assume overflow is impossible due to max concurrency in RSocket it self + int nextInFlight = inFlight + 1; + + if (STATE_AND_IN_FLIGHT.compareAndSet(this, state, nextInFlight | paused)) { + return nextInFlight; + } + } + } + + void decrementInFlight() { + for (; ; ) { + int state = stateAndInFlight; + int paused = state & MASK_PAUSED; + int inFlight = stateAndInFlight & MASK_IN_FLIGHT; + + // assume overflow is impossible due to max concurrency in RSocket it self + int nextInFlight = inFlight - 1; + + if (inFlight == capacity && paused == MASK_PAUSED) { + if (STATE_AND_IN_FLIGHT.compareAndSet(this, state, nextInFlight)) { + logger.debug("Resume execution"); + worker.schedule(this); + return; + } + } else { + if (STATE_AND_IN_FLIGHT.compareAndSet(this, state, nextInFlight | paused)) { + return; + } + } + } + } + + boolean pauseIfNoCapacity() { + int capacity = this.capacity; + for (; ; ) { + int inFlight = stateAndInFlight; + + if (inFlight < capacity) { + return false; + } + + if (STATE_AND_IN_FLIGHT.compareAndSet(this, inFlight, inFlight | MASK_PAUSED)) { + return true; + } + } + } + + void unregister() { + ACTIVE_CONNECTIONS_COUNT.decrementAndGet(this); + } + + void register(LimitBasedLeaseSender sender) { + sendersQueue.offer(sender); + final int activeCount = ACTIVE_CONNECTIONS_COUNT.getAndIncrement(this); + + if (activeCount == 0) { + worker.schedule(this); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/common/LimitBasedLeaseSender.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/common/LimitBasedLeaseSender.java new file mode 100644 index 000000000..8e1b27823 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/common/LimitBasedLeaseSender.java @@ -0,0 +1,54 @@ +package io.rsocket.examples.transport.tcp.lease.advanced.common; + +import com.netflix.concurrency.limits.Limit; +import io.rsocket.lease.Lease; +import io.rsocket.lease.TrackingLeaseSender; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Sinks; +import reactor.util.concurrent.Queues; + +public class LimitBasedLeaseSender extends LimitBasedStatsCollector implements TrackingLeaseSender { + + static final Logger logger = LoggerFactory.getLogger(LimitBasedLeaseSender.class); + + final String connectionId; + final Sinks.Many sink = + Sinks.many().unicast().onBackpressureBuffer(Queues.one().get()); + + public LimitBasedLeaseSender( + String connectionId, LeaseManager leaseManager, Limit limitAlgorithm) { + super(leaseManager, limitAlgorithm); + this.connectionId = connectionId; + } + + @Override + public Flux send() { + logger.info("Received new leased Connection[" + connectionId + "]"); + + leaseManager.register(this); + + return sink.asFlux(); + } + + public void sendLease(int ttl, int amount) { + final Lease nextLease = Lease.create(Duration.ofMillis(ttl), amount); + final Sinks.EmitResult result = sink.tryEmitNext(nextLease); + + if (result.isFailure()) { + logger.warn( + "Connection[" + + connectionId + + "]. Issued Lease: [" + + nextLease + + "] was not sent due to " + + result); + } else { + if (logger.isDebugEnabled()) { + logger.debug("To Connection[" + connectionId + "]: Issued Lease: [" + nextLease + "]"); + } + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/common/LimitBasedStatsCollector.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/common/LimitBasedStatsCollector.java new file mode 100644 index 000000000..7f639ab87 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/common/LimitBasedStatsCollector.java @@ -0,0 +1,73 @@ +package io.rsocket.examples.transport.tcp.lease.advanced.common; + +import com.netflix.concurrency.limits.Limit; +import io.netty.buffer.ByteBuf; +import io.rsocket.frame.FrameType; +import io.rsocket.plugins.RequestInterceptor; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.LongSupplier; +import reactor.util.annotation.Nullable; + +public class LimitBasedStatsCollector extends AtomicBoolean implements RequestInterceptor { + + final LeaseManager leaseManager; + final Limit limitAlgorithm; + + final ConcurrentMap inFlightMap = new ConcurrentHashMap<>(); + final ConcurrentMap timeMap = new ConcurrentHashMap<>(); + + final LongSupplier clock = System::nanoTime; + + public LimitBasedStatsCollector(LeaseManager leaseManager, Limit limitAlgorithm) { + this.leaseManager = leaseManager; + this.limitAlgorithm = limitAlgorithm; + } + + @Override + public void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metadata) { + long startTime = clock.getAsLong(); + + int currentInFlight = leaseManager.incrementInFlightAndGet(); + + inFlightMap.put(streamId, currentInFlight); + timeMap.put(streamId, startTime); + } + + @Override + public void onReject( + Throwable rejectionReason, FrameType requestType, @Nullable ByteBuf metadata) {} + + @Override + public void onTerminate(int streamId, FrameType requestType, @Nullable Throwable t) { + leaseManager.decrementInFlight(); + + Long startTime = timeMap.remove(streamId); + Integer currentInflight = inFlightMap.remove(streamId); + + limitAlgorithm.onSample(startTime, clock.getAsLong() - startTime, currentInflight, t != null); + } + + @Override + public void onCancel(int streamId, FrameType requestType) { + leaseManager.decrementInFlight(); + + Long startTime = timeMap.remove(streamId); + Integer currentInflight = inFlightMap.remove(streamId); + + limitAlgorithm.onSample(startTime, clock.getAsLong() - startTime, currentInflight, true); + } + + @Override + public boolean isDisposed() { + return get(); + } + + @Override + public void dispose() { + if (!getAndSet(true)) { + leaseManager.unregister(); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/controller/Task.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/controller/Task.java new file mode 100644 index 000000000..a18dd9484 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/controller/Task.java @@ -0,0 +1,27 @@ +package io.rsocket.examples.transport.tcp.lease.advanced.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// emulating a worker that process data from the queue +public class Task implements Runnable { + private static final Logger logger = LoggerFactory.getLogger(Task.class); + + final String message; + final int processingTime; + + Task(String message, int processingTime) { + this.message = message; + this.processingTime = processingTime; + } + + @Override + public void run() { + logger.info("Processing Task[{}]", message); + try { + Thread.sleep(processingTime); // emulating processing + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/controller/TasksHandlingRSocket.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/controller/TasksHandlingRSocket.java new file mode 100644 index 000000000..cbecadfc3 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/controller/TasksHandlingRSocket.java @@ -0,0 +1,44 @@ +package io.rsocket.examples.transport.tcp.lease.advanced.controller; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +public class TasksHandlingRSocket implements RSocket { + + private static final Logger logger = LoggerFactory.getLogger(TasksHandlingRSocket.class); + + final Disposable terminatable; + final Scheduler workScheduler; + final int processingTime; + + public TasksHandlingRSocket(Disposable terminatable, Scheduler scheduler, int processingTime) { + this.terminatable = terminatable; + this.workScheduler = scheduler; + this.processingTime = processingTime; + } + + @Override + public Mono fireAndForget(Payload payload) { + + // specifically to show that lease can limit rate of fnf requests in + // that example + String message = payload.getDataUtf8(); + payload.release(); + + return Mono.fromRunnable(new Task(message, processingTime)) + // schedule task on specific, limited in size scheduler + .subscribeOn(workScheduler) + // if errors - terminates server + .doOnError( + t -> { + logger.error("Queue has been overflowed. Terminating server"); + terminatable.dispose(); + System.exit(9); + }); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/invertmulticlient/README.MD b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/invertmulticlient/README.MD new file mode 100644 index 000000000..e69de29bb diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/invertmulticlient/RequestingServer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/invertmulticlient/RequestingServer.java new file mode 100644 index 000000000..30eb0c0e3 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/invertmulticlient/RequestingServer.java @@ -0,0 +1,78 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.examples.transport.tcp.lease.advanced.invertmulticlient; + +import io.rsocket.RSocket; +import io.rsocket.core.RSocketServer; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.ByteBufPayload; +import java.util.Comparator; +import java.util.concurrent.PriorityBlockingQueue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class RequestingServer { + + private static final Logger logger = LoggerFactory.getLogger(RequestingServer.class); + + public static void main(String[] args) { + PriorityBlockingQueue rSockets = + new PriorityBlockingQueue<>( + 16, Comparator.comparingDouble(RSocket::availability).reversed()); + + CloseableChannel server = + RSocketServer.create( + (setup, sendingSocket) -> { + logger.info("Received new connection"); + return Mono.just(new RSocket() {}) + .doAfterTerminate(() -> rSockets.put(sendingSocket)); + }) + .lease(spec -> spec.maxPendingRequests(Integer.MAX_VALUE)) + .bindNow(TcpServerTransport.create("localhost", 7000)); + + logger.info("Server started on port {}", server.address().getPort()); + + // generate stream of fnfs + Flux.generate( + () -> 0L, + (state, sink) -> { + sink.next(state); + return state + 1; + }) + .flatMap( + tick -> { + logger.info("Requesting FireAndForget({})", tick); + + return Mono.fromCallable( + () -> { + RSocket rSocket = rSockets.take(); + rSockets.offer(rSocket); + return rSocket; + }) + .flatMap( + clientRSocket -> + clientRSocket.fireAndForget(ByteBufPayload.create("" + tick))) + .retry(); + }) + .blockLast(); + + server.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/invertmulticlient/RespondingClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/invertmulticlient/RespondingClient.java new file mode 100644 index 000000000..4a06855b2 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/invertmulticlient/RespondingClient.java @@ -0,0 +1,67 @@ +package io.rsocket.examples.transport.tcp.lease.advanced.invertmulticlient; + +import com.netflix.concurrency.limits.limit.VegasLimit; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.examples.transport.tcp.lease.advanced.common.LeaseManager; +import io.rsocket.examples.transport.tcp.lease.advanced.common.LimitBasedLeaseSender; +import io.rsocket.examples.transport.tcp.lease.advanced.controller.TasksHandlingRSocket; +import io.rsocket.transport.netty.client.TcpClientTransport; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +public class RespondingClient { + private static final Logger logger = LoggerFactory.getLogger(RespondingClient.class); + + public static final int PROCESSING_TASK_TIME = 500; + public static final int CONCURRENT_WORKERS_COUNT = 1; + public static final int QUEUE_CAPACITY = 50; + + public static void main(String[] args) { + // Queue for incoming messages represented as Flux + // Imagine that every fireAndForget that is pushed is processed by a worker + BlockingQueue tasksQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY); + + ThreadPoolExecutor threadPoolExecutor = + new ThreadPoolExecutor(1, CONCURRENT_WORKERS_COUNT, 1, TimeUnit.MINUTES, tasksQueue); + + Scheduler workScheduler = Schedulers.fromExecutorService(threadPoolExecutor); + + LeaseManager periodicLeaseSender = + new LeaseManager(CONCURRENT_WORKERS_COUNT, PROCESSING_TASK_TIME); + + Disposable.Composite disposable = Disposables.composite(); + RSocket clientRSocket = + RSocketConnector.create() + .acceptor( + SocketAcceptor.with( + new TasksHandlingRSocket(disposable, workScheduler, PROCESSING_TASK_TIME))) + .lease( + (config) -> + config.sender( + new LimitBasedLeaseSender( + UUID.randomUUID().toString(), + periodicLeaseSender, + VegasLimit.newBuilder() + .initialLimit(CONCURRENT_WORKERS_COUNT) + .maxConcurrency(QUEUE_CAPACITY) + .build()))) + .connect(TcpClientTransport.create("localhost", 7000)) + .block(); + + Objects.requireNonNull(clientRSocket); + disposable.add(clientRSocket); + clientRSocket.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/multiclient/README.MD b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/multiclient/README.MD new file mode 100644 index 000000000..e69de29bb diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/multiclient/RequestingClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/multiclient/RequestingClient.java new file mode 100644 index 000000000..c2fde38e3 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/multiclient/RequestingClient.java @@ -0,0 +1,41 @@ +package io.rsocket.examples.transport.tcp.lease.advanced.multiclient; + +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.util.ByteBufPayload; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +public class RequestingClient { + private static final Logger logger = LoggerFactory.getLogger(RequestingClient.class); + + public static void main(String[] args) { + + RSocket clientRSocket = + RSocketConnector.create() + .lease() + .connect(TcpClientTransport.create("localhost", 7000)) + .block(); + + Objects.requireNonNull(clientRSocket); + + // generate stream of fnfs + Flux.generate( + () -> 0L, + (state, sink) -> { + sink.next(state); + return state + 1; + }) + .concatMap( + tick -> { + logger.info("Requesting FireAndForget({})", tick); + return clientRSocket.fireAndForget(ByteBufPayload.create("" + tick)); + }) + .blockLast(); + + clientRSocket.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/multiclient/RespondingServer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/multiclient/RespondingServer.java new file mode 100644 index 000000000..b54330450 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/advanced/multiclient/RespondingServer.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.examples.transport.tcp.lease.advanced.multiclient; + +import com.netflix.concurrency.limits.limit.VegasLimit; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.tcp.lease.advanced.common.LeaseManager; +import io.rsocket.examples.transport.tcp.lease.advanced.common.LimitBasedLeaseSender; +import io.rsocket.examples.transport.tcp.lease.advanced.controller.TasksHandlingRSocket; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import java.util.UUID; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +public class RespondingServer { + + private static final Logger logger = LoggerFactory.getLogger(RespondingServer.class); + + public static final int TASK_PROCESSING_TIME = 500; + public static final int CONCURRENT_WORKERS_COUNT = 1; + public static final int QUEUE_CAPACITY = 50; + + public static void main(String[] args) { + // Queue for incoming messages represented as Flux + // Imagine that every fireAndForget that is pushed is processed by a worker + BlockingQueue tasksQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY); + + ThreadPoolExecutor threadPoolExecutor = + new ThreadPoolExecutor(1, CONCURRENT_WORKERS_COUNT, 1, TimeUnit.MINUTES, tasksQueue); + + Scheduler workScheduler = Schedulers.fromExecutorService(threadPoolExecutor); + + LeaseManager leaseManager = new LeaseManager(CONCURRENT_WORKERS_COUNT, TASK_PROCESSING_TIME); + + Disposable.Composite disposable = Disposables.composite(); + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.with( + new TasksHandlingRSocket(disposable, workScheduler, TASK_PROCESSING_TIME))) + .lease( + (config) -> + config.sender( + new LimitBasedLeaseSender( + UUID.randomUUID().toString(), + leaseManager, + VegasLimit.newBuilder() + .initialLimit(CONCURRENT_WORKERS_COUNT) + .maxConcurrency(QUEUE_CAPACITY) + .build()))) + .bindNow(TcpServerTransport.create("localhost", 7000)); + + disposable.add(server); + + logger.info("Server started on port {}", server.address().getPort()); + server.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/simple/LeaseExample.java similarity index 66% rename from rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java rename to rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/simple/LeaseExample.java index 49f683204..c54335ccc 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/simple/LeaseExample.java @@ -14,33 +14,26 @@ * limitations under the License. */ -package io.rsocket.examples.transport.tcp.lease; +package io.rsocket.examples.transport.tcp.lease.simple; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.lease.Lease; -import io.rsocket.lease.LeaseStats; -import io.rsocket.lease.Leases; -import io.rsocket.lease.MissingLeaseException; +import io.rsocket.lease.LeaseSender; import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.ByteBufPayload; import java.time.Duration; import java.util.Objects; -import java.util.Optional; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; -import java.util.function.Consumer; -import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.ReplayProcessor; -import reactor.util.retry.Retry; public class LeaseExample { @@ -95,13 +88,12 @@ public Mono fireAndForget(Payload payload) { return Mono.empty(); } })) - .lease(() -> Leases.create().sender(new LeaseCalculator(SERVER_TAG, messagesQueue))) + .lease(leases -> leases.sender(new LeaseCalculator(SERVER_TAG, messagesQueue))) .bindNow(TcpServerTransport.create("localhost", 7000)); - LeaseReceiver receiver = new LeaseReceiver(CLIENT_TAG); RSocket clientRSocket = RSocketConnector.create() - .lease(() -> Leases.create().receiver(receiver)) + .lease((config) -> config.maxPendingRequests(1)) .connect(TcpClientTransport.create(server.address())) .block(); @@ -116,22 +108,10 @@ public Mono fireAndForget(Payload payload) { }) // here we wait for the first lease for the responder side and start execution // on if there is allowance - .delaySubscription(receiver.notifyWhenNewLease().then()) .concatMap( tick -> { logger.info("Requesting FireAndForget({})", tick); - return Mono.defer(() -> clientRSocket.fireAndForget(ByteBufPayload.create("" + tick))) - .retryWhen( - Retry.indefinitely() - // ensures that error is the result of missed lease - .filter(t -> t instanceof MissingLeaseException) - .doBeforeRetryAsync( - rs -> { - // here we create a mechanism to delay the retry until - // the new lease allowance comes in. - logger.info("Ran out of leases {}", rs); - return receiver.notifyWhenNewLease().then(); - })); + return clientRSocket.fireAndForget(ByteBufPayload.create("" + tick)); }) .blockLast(); @@ -146,7 +126,7 @@ public Mono fireAndForget(Payload payload) { * connection.
* In real-world projects this class has to issue leases based on real metrics */ - private static class LeaseCalculator implements Function, Flux> { + private static class LeaseCalculator implements LeaseSender { final String tag; final BlockingQueue queue; @@ -156,8 +136,7 @@ public LeaseCalculator(String tag, BlockingQueue queue) { } @Override - public Flux apply(Optional leaseStats) { - logger.info("{} stats are {}", tag, leaseStats.isPresent() ? "present" : "absent"); + public Flux send() { Duration ttlDuration = Duration.ofSeconds(5); // The interval function is used only for the demo purpose and should not be // considered as the way to issue leases. @@ -173,45 +152,9 @@ public Flux apply(Optional leaseStats) { // reissue new lease only if queue has remaining capacity to // accept more requests if (requests > 0) { - long ttl = ttlDuration.toMillis(); - sink.next(Lease.create((int) ttl, requests)); + sink.next(Lease.create(ttlDuration, requests)); } }); } } - - /** - * Requester-side Lease listener.
- * In the nutshell this class implements mechanism to listen (and do appropriate actions as - * needed) to incoming leases issued by the Responder - */ - private static class LeaseReceiver implements Consumer> { - final String tag; - final ReplayProcessor lastLeaseReplay = ReplayProcessor.cacheLast(); - - public LeaseReceiver(String tag) { - this.tag = tag; - } - - @Override - public void accept(Flux receivedLeases) { - receivedLeases.subscribe( - l -> { - logger.info( - "{} received leases - ttl: {}, requests: {}", - tag, - l.getTimeToLiveMillis(), - l.getAllowedRequests()); - lastLeaseReplay.onNext(l); - }); - } - - /** - * This method allows to listen to new incoming leases and delay some action (e.g . retry) until - * new valid lease has come in - */ - public Mono notifyWhenNewLease() { - return lastLeaseReplay.filter(l -> l.isValid()).next(); - } - } } From 737012821e9dd07ecd3ee0116d3e7b36da16bae0 Mon Sep 17 00:00:00 2001 From: rudy2steiner Date: Fri, 19 Mar 2021 23:55:00 +0800 Subject: [PATCH 020/116] Support for per-stream data MIME type metadata Signed-off-by: rudy2steiner --- .../metadata/MimeTypeMetadataCodec.java | 147 ++++++++++++++++++ .../metadata/MimeMetadataCodecTest.java | 53 +++++++ 2 files changed, 200 insertions(+) create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/MimeTypeMetadataCodec.java create mode 100644 rsocket-core/src/test/java/io/rsocket/metadata/MimeMetadataCodecTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/MimeTypeMetadataCodec.java b/rsocket-core/src/main/java/io/rsocket/metadata/MimeTypeMetadataCodec.java new file mode 100644 index 000000000..16203e9f5 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/MimeTypeMetadataCodec.java @@ -0,0 +1,147 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +/** + * Provides support for encoding and/or decoding the per-stream MIME type to use for payload data. + * + *

For more on the format of the metadata, see the + * Stream Data MIME Types extension specification. + * + * @since 1.1.1 + */ +public class MimeTypeMetadataCodec { + private static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 + private static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 + + private MimeTypeMetadataCodec() {} + + /** + * Encode a {@link WellKnownMimeType} into a newly allocated {@link ByteBuf} and this can then be + * decoded using {@link #decode(ByteBuf)}. + */ + public static ByteBuf encode(ByteBufAllocator allocator, WellKnownMimeType mimeType) { + return allocator.buffer(1, 1).writeByte(mimeType.getIdentifier() | STREAM_METADATA_KNOWN_MASK); + } + + /** + * Encode either a {@link WellKnownMimeType} or a custom mime type into a newly allocated {@link + * ByteBuf}. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer + * @param mimeType mime type + * @return the encoded mime type + */ + public static ByteBuf encode(ByteBufAllocator allocator, String mimeType) { + if (mimeType == null || mimeType.length() == 0) { + throw new IllegalArgumentException("Mime type null or length is zero"); + } + WellKnownMimeType wkn = WellKnownMimeType.fromString(mimeType); + if (wkn == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { + return encodeCustomMimeType(allocator, mimeType); + } else { + return encode(allocator, wkn); + } + } + + /** + * Encode multiple {@link WellKnownMimeType} or custom mime type into a newly allocated {@link + * ByteBuf}. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer + * @param mimeTypes mime types + * @return the encoded mime types + */ + public static ByteBuf encode(ByteBufAllocator allocator, List mimeTypes) { + if (mimeTypes == null || mimeTypes.size() == 0) { + throw new IllegalArgumentException("Mime types empty"); + } + CompositeByteBuf compositeMimeByteBuf = allocator.compositeBuffer(); + for (String mimeType : mimeTypes) { + compositeMimeByteBuf.addComponents(true, encode(allocator, mimeType)); + } + return compositeMimeByteBuf; + } + + /** + * Encode a custom mime type into a newly allocated {@link ByteBuf}. + * + *

This larger representation encodes the mime type representation's length on a single byte, + * then the representation itself. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer + * @param customMimeType a custom mime type to encode + * @return the encoded mime type + */ + private static ByteBuf encodeCustomMimeType(ByteBufAllocator allocator, String customMimeType) { + ByteBuf mime = allocator.buffer(1 + customMimeType.length()); + // reserve 1 byte for the customMime length + // /!\ careful not to read that first byte, which is random at this point + mime.writerIndex(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(mime, customMimeType); + if (!ByteBufUtil.isText(mime, mime.readerIndex() + 1, customMimeLength, CharsetUtil.US_ASCII)) { + mime.release(); + throw new IllegalArgumentException("custom mime type must be US_ASCII characters only"); + } + if (customMimeLength < 1 || customMimeLength > 128) { + mime.release(); + throw new IllegalArgumentException( + "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + mime.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 + mime.writerIndex(0); + mime.writeByte(customMimeLength - 1); + + // go back to post-mime type + mime.resetWriterIndex(); + return mime; + } + + /** + * Decode mime types from a {@link ByteBuf} that contains at least enough bytes for one mime type. + * + * @return decoded mime types + */ + public static List decode(ByteBuf buf) { + List mimeTypes = new ArrayList<>(); + while (buf.isReadable()) { + byte mimeIdOrLength = buf.readByte(); + if ((mimeIdOrLength & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK) { + byte mimeIdentifier = (byte) (mimeIdOrLength & STREAM_METADATA_LENGTH_MASK); + mimeTypes.add(WellKnownMimeType.fromIdentifier(mimeIdentifier).toString()); + } else { + int mimeLen = Byte.toUnsignedInt(mimeIdOrLength) + 1; + mimeTypes.add(buf.readCharSequence(mimeLen, CharsetUtil.US_ASCII).toString()); + } + } + return mimeTypes; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/MimeMetadataCodecTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/MimeMetadataCodecTest.java new file mode 100644 index 000000000..01bc11ab2 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/MimeMetadataCodecTest.java @@ -0,0 +1,53 @@ +/* + * 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 java.util.List; +import org.assertj.core.api.Assertions; +import org.assertj.core.util.Lists; +import org.junit.Test; + +public class MimeMetadataCodecTest { + + @Test + public void customMimeType() { + String customMimeType = "aaa/bb"; + ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, customMimeType); + List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); + Assertions.assertThat(mimeTypes.size()).isEqualTo(1); + Assertions.assertThat(customMimeType).isEqualTo(mimeTypes.get(0)); + } + + @Test + public void wellKnowMimeType() { + WellKnownMimeType wellKnownMimeType = WellKnownMimeType.APPLICATION_HESSIAN; + ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, wellKnownMimeType); + List mimes = MimeTypeMetadataCodec.decode(byteBuf); + Assertions.assertThat(mimes.size()).isEqualTo(1); + Assertions.assertThat(wellKnownMimeType).isEqualTo(WellKnownMimeType.fromString(mimes.get(0))); + } + + @Test + public void multipleAndMixMimeType() { + List mimeTypes = + Lists.newArrayList("aaa/bbb", WellKnownMimeType.APPLICATION_HESSIAN.getString()); + ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, mimeTypes); + List decodedMimeTypes = MimeTypeMetadataCodec.decode(byteBuf); + Assertions.assertThat(decodedMimeTypes).isEqualTo(mimeTypes); + } +} From c80b3cb6437046f3ccc79136a66299448f58c561 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 26 Mar 2021 10:36:07 +0000 Subject: [PATCH 021/116] Polishing contribution Closes gh-998 Signed-off-by: Rossen Stoyanchev --- .../metadata/MimeTypeMetadataCodec.java | 112 ++++++++---------- ...st.java => MimeTypeMetadataCodecTest.java} | 39 +++--- 2 files changed, 72 insertions(+), 79 deletions(-) rename rsocket-core/src/test/java/io/rsocket/metadata/{MimeMetadataCodecTest.java => MimeTypeMetadataCodecTest.java} (55%) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/MimeTypeMetadataCodec.java b/rsocket-core/src/main/java/io/rsocket/metadata/MimeTypeMetadataCodec.java index 16203e9f5..2e03bd754 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/MimeTypeMetadataCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/MimeTypeMetadataCodec.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import java.util.List; /** - * Provides support for encoding and/or decoding the per-stream MIME type to use for payload data. + * Provides support for encoding and decoding the per-stream MIME type to use for payload data. * *

For more on the format of the metadata, see the @@ -33,30 +33,34 @@ * @since 1.1.1 */ public class MimeTypeMetadataCodec { + private static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 + private static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 private MimeTypeMetadataCodec() {} /** - * Encode a {@link WellKnownMimeType} into a newly allocated {@link ByteBuf} and this can then be - * decoded using {@link #decode(ByteBuf)}. + * Encode a {@link WellKnownMimeType} into a newly allocated single byte {@link ByteBuf}. + * + * @param allocator the allocator to create the buffer with + * @param mimeType well-known MIME type to encode + * @return the resulting buffer */ public static ByteBuf encode(ByteBufAllocator allocator, WellKnownMimeType mimeType) { return allocator.buffer(1, 1).writeByte(mimeType.getIdentifier() | STREAM_METADATA_KNOWN_MASK); } /** - * Encode either a {@link WellKnownMimeType} or a custom mime type into a newly allocated {@link - * ByteBuf}. + * Encode the given MIME type into a newly allocated {@link ByteBuf}. * - * @param allocator the {@link ByteBufAllocator} to use to create the buffer - * @param mimeType mime type - * @return the encoded mime type + * @param allocator the allocator to create the buffer with + * @param mimeType MIME type to encode + * @return the resulting buffer */ public static ByteBuf encode(ByteBufAllocator allocator, String mimeType) { if (mimeType == null || mimeType.length() == 0) { - throw new IllegalArgumentException("Mime type null or length is zero"); + throw new IllegalArgumentException("MIME type is required"); } WellKnownMimeType wkn = WellKnownMimeType.fromString(mimeType); if (wkn == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { @@ -67,79 +71,65 @@ public static ByteBuf encode(ByteBufAllocator allocator, String mimeType) { } /** - * Encode multiple {@link WellKnownMimeType} or custom mime type into a newly allocated {@link - * ByteBuf}. + * Encode multiple MIME types into a newly allocated {@link ByteBuf}. * - * @param allocator the {@link ByteBufAllocator} to use to create the buffer - * @param mimeTypes mime types - * @return the encoded mime types + * @param allocator the allocator to create the buffer with + * @param mimeTypes MIME types to encode + * @return the resulting buffer */ public static ByteBuf encode(ByteBufAllocator allocator, List mimeTypes) { if (mimeTypes == null || mimeTypes.size() == 0) { - throw new IllegalArgumentException("Mime types empty"); + throw new IllegalArgumentException("No MIME types provided"); } - CompositeByteBuf compositeMimeByteBuf = allocator.compositeBuffer(); + CompositeByteBuf compositeByteBuf = allocator.compositeBuffer(); for (String mimeType : mimeTypes) { - compositeMimeByteBuf.addComponents(true, encode(allocator, mimeType)); + ByteBuf byteBuf = encode(allocator, mimeType); + compositeByteBuf.addComponents(true, byteBuf); } - return compositeMimeByteBuf; + return compositeByteBuf; } - /** - * Encode a custom mime type into a newly allocated {@link ByteBuf}. - * - *

This larger representation encodes the mime type representation's length on a single byte, - * then the representation itself. - * - * @param allocator the {@link ByteBufAllocator} to use to create the buffer - * @param customMimeType a custom mime type to encode - * @return the encoded mime type - */ private static ByteBuf encodeCustomMimeType(ByteBufAllocator allocator, String customMimeType) { - ByteBuf mime = allocator.buffer(1 + customMimeType.length()); - // reserve 1 byte for the customMime length - // /!\ careful not to read that first byte, which is random at this point - mime.writerIndex(1); + ByteBuf byteBuf = allocator.buffer(1 + customMimeType.length()); + + byteBuf.writerIndex(1); + int length = ByteBufUtil.writeUtf8(byteBuf, customMimeType); - // 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(mime, customMimeType); - if (!ByteBufUtil.isText(mime, mime.readerIndex() + 1, customMimeLength, CharsetUtil.US_ASCII)) { - mime.release(); - throw new IllegalArgumentException("custom mime type must be US_ASCII characters only"); + if (!ByteBufUtil.isText(byteBuf, 1, length, CharsetUtil.US_ASCII)) { + byteBuf.release(); + throw new IllegalArgumentException("MIME type must be ASCII characters only"); } - if (customMimeLength < 1 || customMimeLength > 128) { - mime.release(); + + if (length < 1 || length > 128) { + byteBuf.release(); throw new IllegalArgumentException( - "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + "MIME type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); } - mime.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 - mime.writerIndex(0); - mime.writeByte(customMimeLength - 1); - // go back to post-mime type - mime.resetWriterIndex(); - return mime; + byteBuf.markWriterIndex(); + byteBuf.writerIndex(0); + byteBuf.writeByte(length - 1); + byteBuf.resetWriterIndex(); + + return byteBuf; } /** - * Decode mime types from a {@link ByteBuf} that contains at least enough bytes for one mime type. + * Decode the per-stream MIME type metadata encoded in the given {@link ByteBuf}. * - * @return decoded mime types + * @return the decoded MIME types */ - public static List decode(ByteBuf buf) { + public static List decode(ByteBuf byteBuf) { List mimeTypes = new ArrayList<>(); - while (buf.isReadable()) { - byte mimeIdOrLength = buf.readByte(); - if ((mimeIdOrLength & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK) { - byte mimeIdentifier = (byte) (mimeIdOrLength & STREAM_METADATA_LENGTH_MASK); - mimeTypes.add(WellKnownMimeType.fromIdentifier(mimeIdentifier).toString()); + while (byteBuf.isReadable()) { + byte idOrLength = byteBuf.readByte(); + if ((idOrLength & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK) { + byte id = (byte) (idOrLength & STREAM_METADATA_LENGTH_MASK); + WellKnownMimeType wellKnownMimeType = WellKnownMimeType.fromIdentifier(id); + mimeTypes.add(wellKnownMimeType.toString()); } else { - int mimeLen = Byte.toUnsignedInt(mimeIdOrLength) + 1; - mimeTypes.add(buf.readCharSequence(mimeLen, CharsetUtil.US_ASCII).toString()); + int length = Byte.toUnsignedInt(idOrLength) + 1; + mimeTypes.add(byteBuf.readCharSequence(length, CharsetUtil.US_ASCII).toString()); } } return mimeTypes; diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/MimeMetadataCodecTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java similarity index 55% rename from rsocket-core/src/test/java/io/rsocket/metadata/MimeMetadataCodecTest.java rename to rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java index 01bc11ab2..a39caed2b 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/MimeMetadataCodecTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java @@ -15,39 +15,42 @@ */ package io.rsocket.metadata; +import static org.assertj.core.api.Assertions.assertThat; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import java.util.List; -import org.assertj.core.api.Assertions; import org.assertj.core.util.Lists; import org.junit.Test; -public class MimeMetadataCodecTest { +/** Unit tests for {@link MimeTypeMetadataCodec}. */ +public class MimeTypeMetadataCodecTest { @Test - public void customMimeType() { - String customMimeType = "aaa/bb"; - ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, customMimeType); + public void wellKnownMimeType() { + WellKnownMimeType mimeType = WellKnownMimeType.APPLICATION_HESSIAN; + ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, mimeType); List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); - Assertions.assertThat(mimeTypes.size()).isEqualTo(1); - Assertions.assertThat(customMimeType).isEqualTo(mimeTypes.get(0)); + + assertThat(mimeTypes.size()).isEqualTo(1); + assertThat(WellKnownMimeType.fromString(mimeTypes.get(0))).isEqualTo(mimeType); } @Test - public void wellKnowMimeType() { - WellKnownMimeType wellKnownMimeType = WellKnownMimeType.APPLICATION_HESSIAN; - ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, wellKnownMimeType); - List mimes = MimeTypeMetadataCodec.decode(byteBuf); - Assertions.assertThat(mimes.size()).isEqualTo(1); - Assertions.assertThat(wellKnownMimeType).isEqualTo(WellKnownMimeType.fromString(mimes.get(0))); + public void customMimeType() { + String mimeType = "aaa/bb"; + ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, mimeType); + List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); + + assertThat(mimeTypes.size()).isEqualTo(1); + assertThat(mimeTypes.get(0)).isEqualTo(mimeType); } @Test - public void multipleAndMixMimeType() { - List mimeTypes = - Lists.newArrayList("aaa/bbb", WellKnownMimeType.APPLICATION_HESSIAN.getString()); + public void multipleMimeTypes() { + List mimeTypes = Lists.newArrayList("aaa/bbb", "application/x-hessian"); ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, mimeTypes); - List decodedMimeTypes = MimeTypeMetadataCodec.decode(byteBuf); - Assertions.assertThat(decodedMimeTypes).isEqualTo(mimeTypes); + + assertThat(MimeTypeMetadataCodec.decode(byteBuf)).isEqualTo(mimeTypes); } } From 831d5bf0b89fa3b9169b2b60f9de65928814e4e9 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 30 Mar 2021 14:53:34 +0100 Subject: [PATCH 022/116] Update Javadoc in loadbalance package Closes gh-954 Signed-off-by: Rossen Stoyanchev --- .../io/rsocket/core/ResolvingOperator.java | 17 ++++++ .../loadbalance/BaseWeightedStats.java | 22 ++++++- .../ClientLoadbalanceStrategy.java | 30 +++++++++- .../loadbalance/LoadbalanceRSocketClient.java | 56 +++++++++--------- .../loadbalance/LoadbalanceStrategy.java | 17 +++++- .../loadbalance/LoadbalanceTarget.java | 25 ++++---- .../loadbalance/ResolvingOperator.java | 57 ++----------------- .../RoundRobinLoadbalanceStrategy.java | 4 +- .../WeightedLoadbalanceStrategy.java | 33 +++++++---- .../io/rsocket/loadbalance/WeightedStats.java | 26 +++++++-- .../WeightedStatsRSocketProxy.java | 19 ++++++- .../WeightedStatsRequestInterceptor.java | 23 +++++++- .../io/rsocket/loadbalance/package-info.java | 1 + 13 files changed, 212 insertions(+), 118 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java b/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java index c431b3f3f..e6ceada87 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.core; import java.time.Duration; @@ -15,6 +30,8 @@ import reactor.util.annotation.Nullable; import reactor.util.context.Context; +// A copy of this class exists in io.rsocket.loadbalance + class ResolvingOperator implements Disposable { static final CancellationException ON_DISPOSE = new CancellationException("Disposed"); diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java index bd427da8a..fdbbeb25d 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.loadbalance; import io.rsocket.util.Clock; @@ -5,9 +20,14 @@ import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; /** - * The base implementation of the {@link WeightedStats} interface + * Implementation of {@link WeightedStats} that manages tracking state and exposes the required + * stats. + * + *

A sub-class or a different class (delegation) needs to call {@link #startStream()}, {@link + * #stopStream()}, {@link #startRequest()}, and {@link #stopRequest(long)} to drive state tracking. * * @since 1.1 + * @see WeightedStatsRequestInterceptor */ public class BaseWeightedStats implements WeightedStats { diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/ClientLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/ClientLoadbalanceStrategy.java index a35151fa6..528f4f896 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/ClientLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/ClientLoadbalanceStrategy.java @@ -1,14 +1,40 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.loadbalance; import io.rsocket.core.RSocketConnector; +import io.rsocket.plugins.InterceptorRegistry; /** - * Extension for {@link LoadbalanceStrategy} which allows pre-setup {@link RSocketConnector} for - * {@link LoadbalanceStrategy} needs + * A {@link LoadbalanceStrategy} with an interest in configuring the {@link RSocketConnector} for + * connecting to load-balance targets in order to hook into request lifecycle and track usage + * statistics. + * + *

Currently this callback interface is supported for strategies configured in {@link + * LoadbalanceRSocketClient}. * * @since 1.1 */ public interface ClientLoadbalanceStrategy extends LoadbalanceStrategy { + /** + * Initialize the connector, for example using the {@link InterceptorRegistry}, to intercept + * requests. + * + * @param connector the connector to configure + */ void initialize(RSocketConnector connector); } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java index 4a1625e8a..1b677edba 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import io.rsocket.RSocket; import io.rsocket.core.RSocketClient; import io.rsocket.core.RSocketConnector; +import io.rsocket.transport.ClientTransport; import java.util.List; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -26,8 +27,8 @@ import reactor.util.annotation.Nullable; /** - * {@link RSocketClient} implementation that uses a {@link LoadbalanceStrategy} to select the {@code - * RSocket} to use for a given request from a pool of possible targets. + * An implementation of {@link RSocketClient backed by a pool of {@code RSocket} instances and using a {@link + * LoadbalanceStrategy} to select the {@code RSocket} to use for a given request. * * @since 1.1 */ @@ -39,6 +40,7 @@ private LoadbalanceRSocketClient(RSocketPool rSocketPool) { this.rSocketPool = rSocketPool; } + /** Return {@code Mono} that selects an RSocket from the underlying pool. */ @Override public Mono source() { return Mono.fromSupplier(rSocketPool::select); @@ -75,7 +77,7 @@ public void dispose() { } /** - * Shortcut to create an {@link LoadbalanceRSocketClient} with round robin loadalancing. + * Shortcut to create an {@link LoadbalanceRSocketClient} with round-robin load balancing. * Effectively a shortcut for: * *

@@ -84,8 +86,8 @@ public void dispose() {
    *    .build();
    * 
* - * @param connector the {@link Builder#connector(RSocketConnector) to use - * @param targetPublisher publisher that periodically refreshes the list of targets to loadbalance across. + * @param connector a "template" for connecting to load balance targets + * @param targetPublisher refreshes the list of load balance targets periodically * @return the created client instance */ public static LoadbalanceRSocketClient create( @@ -94,11 +96,10 @@ public static LoadbalanceRSocketClient create( } /** - * Return a builder to create an {@link LoadbalanceRSocketClient} with. + * Return a builder for a {@link LoadbalanceRSocketClient}. * - * @param targetPublisher publisher that periodically refreshes the list of targets to loadbalance - * across. - * @return the builder instance + * @param targetPublisher refreshes the list of load balance targets periodically + * @return the created builder */ public static Builder builder(Publisher> targetPublisher) { return new Builder(targetPublisher); @@ -118,10 +119,11 @@ public static class Builder { } /** - * The given {@link RSocketConnector} is used as a template to produce the {@code Mono} - * source for each {@link LoadbalanceTarget}. This is done by passing the {@code - * ClientTransport} contained in every target to the {@code connect} method of the given - * connector instance. + * Configure the "template" connector to use for connecting to load balance targets. To + * establish a connection, the {@link LoadbalanceTarget#getTransport() ClientTransport} + * contained in each target is passed to the connector's {@link + * RSocketConnector#connect(ClientTransport) connect} method and thus the same connector with + * the same settings applies to all targets. * *

By default this is initialized with {@link RSocketConnector#create()}. * @@ -133,7 +135,7 @@ public Builder connector(RSocketConnector connector) { } /** - * Switch to using a round-robin strategy for selecting a target. + * Configure {@link RoundRobinLoadbalanceStrategy} as the strategy to use to select targets. * *

This is the strategy used by default. */ @@ -143,8 +145,7 @@ public Builder roundRobinLoadbalanceStrategy() { } /** - * Switch to using a strategy that assigns a weight to each pooled {@code RSocket} based on - * actual usage stats, and uses that to make a choice. + * Configure {@link WeightedLoadbalanceStrategy} as the strategy to use to select targets. * *

By default, {@link RoundRobinLoadbalanceStrategy} is used. */ @@ -154,7 +155,7 @@ public Builder weightedLoadbalanceStrategy() { } /** - * Provide the {@link LoadbalanceStrategy} to use. + * Configure the {@link LoadbalanceStrategy} to use. * *

By default, {@link RoundRobinLoadbalanceStrategy} is used. */ @@ -165,8 +166,13 @@ public Builder loadbalanceStrategy(LoadbalanceStrategy strategy) { /** Build the {@link LoadbalanceRSocketClient} instance. */ public LoadbalanceRSocketClient build() { - final RSocketConnector connector = initConnector(); - final LoadbalanceStrategy strategy = initLoadbalanceStrategy(); + final RSocketConnector connector = + (this.connector != null ? this.connector : RSocketConnector.create()); + + final LoadbalanceStrategy strategy = + (this.loadbalanceStrategy != null + ? this.loadbalanceStrategy + : new RoundRobinLoadbalanceStrategy()); if (strategy instanceof ClientLoadbalanceStrategy) { ((ClientLoadbalanceStrategy) strategy).initialize(connector); @@ -175,15 +181,5 @@ public LoadbalanceRSocketClient build() { return new LoadbalanceRSocketClient( new RSocketPool(connector, this.targetPublisher, strategy)); } - - private RSocketConnector initConnector() { - return (this.connector != null ? this.connector : RSocketConnector.create()); - } - - private LoadbalanceStrategy initLoadbalanceStrategy() { - return (this.loadbalanceStrategy != null - ? this.loadbalanceStrategy - : new RoundRobinLoadbalanceStrategy()); - } } } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java index 2a333959b..5662448e7 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,21 @@ import io.rsocket.RSocket; import java.util.List; +/** + * Strategy to select an {@link RSocket} given a list of instances for load-balancing purposes. A + * simple implementation might go in round-robin fashion while a more sophisticated strategy might + * check availability, track usage stats, and so on. + * + * @since 1.1 + */ @FunctionalInterface public interface LoadbalanceStrategy { - RSocket select(List availableRSockets); + /** + * Select an {@link RSocket} from the given non-empty list. + * + * @param sockets the list to choose from + * @return the selected instance + */ + RSocket select(List sockets); } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceTarget.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceTarget.java index e99914caa..3b5d71e4e 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceTarget.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceTarget.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,14 +15,18 @@ */ package io.rsocket.loadbalance; +import io.rsocket.core.RSocketConnector; import io.rsocket.transport.ClientTransport; +import org.reactivestreams.Publisher; /** - * Simple container for a key and a {@link ClientTransport}, representing a specific target for - * loadbalancing purposes. The key is used to compare previous and new targets when refreshing the - * list of target to use. The transport is used to connect to the target. + * Representation for a load-balance target used as input to {@link LoadbalanceRSocketClient} that + * in turn maintains and peridodically updates a list of current load-balance targets. The {@link + * #getKey()} is used to identify a target uniquely while the {@link #getTransport() transport} is + * used to connect to the target server. * * @since 1.1 + * @see LoadbalanceRSocketClient#create(RSocketConnector, Publisher) */ public class LoadbalanceTarget { @@ -34,23 +38,22 @@ private LoadbalanceTarget(String key, ClientTransport transport) { this.transport = transport; } - /** Return the key for this target. */ + /** Return the key that identifies this target uniquely. */ public String getKey() { return key; } - /** Return the transport to use to connect to the target. */ + /** Return the transport to use to connect to the target server. */ public ClientTransport getTransport() { return transport; } /** - * Create a an instance of {@link LoadbalanceTarget} with the given key and {@link - * ClientTransport}. The key can be anything that can be used to identify identical targets, e.g. - * a SocketAddress, URL, etc. + * Create a new {@link LoadbalanceTarget} with the given key and {@link ClientTransport}. The key + * can be anything that identifies the target uniquely, e.g. SocketAddress, URL, and so on. * - * @param key the key to use to identify identical targets - * @param transport the transport to use for connecting to the target + * @param key identifies the load-balance target uniquely + * @param transport for connecting to the target * @return the created instance */ public static LoadbalanceTarget from(String key, ClientTransport transport) { diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/ResolvingOperator.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/ResolvingOperator.java index e03088b7f..a55012a0f 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/ResolvingOperator.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/ResolvingOperator.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,8 @@ import reactor.util.annotation.Nullable; import reactor.util.context.Context; +// This class is a copy of the same class in io.rsocket.core + class ResolvingOperator implements Disposable { static final CancellationException ON_DISPOSE = new CancellationException("Disposed"); @@ -72,7 +74,7 @@ public ResolvingOperator() { } @Override - public void dispose() { + public final void dispose() { this.terminate(ON_DISPOSE); } @@ -559,55 +561,4 @@ public void cancel() { } } } - - static class MonoDeferredResolutionOperator extends Operators.MonoSubscriber - implements BiConsumer { - - final ResolvingOperator parent; - - MonoDeferredResolutionOperator(ResolvingOperator parent, CoreSubscriber actual) { - super(actual); - this.parent = parent; - } - - @Override - public void accept(T t, Throwable throwable) { - if (throwable != null) { - onError(throwable); - return; - } - - complete(t); - } - - @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); - } - } } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java index 98c86d565..f1a9f8c55 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ public class RoundRobinLoadbalanceStrategy implements LoadbalanceStrategy { volatile int nextIndex; - static final AtomicIntegerFieldUpdater NEXT_INDEX = + private static final AtomicIntegerFieldUpdater NEXT_INDEX = AtomicIntegerFieldUpdater.newUpdater(RoundRobinLoadbalanceStrategy.class, "nextIndex"); @Override diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java index 682a808bf..c401818f9 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,10 +27,16 @@ import reactor.util.annotation.Nullable; /** - * {@link LoadbalanceStrategy} that assigns a weight to each {@code RSocket} based on usage - * statistics, and uses this weight to select the {@code RSocket} to use. + * {@link LoadbalanceStrategy} that assigns a weight to each {@code RSocket} based on {@link + * RSocket#availability() availability} and usage statistics. The weight is used to decide which + * {@code RSocket} to select. + * + *

Use {@link #create()} or a {@link #builder() Builder} to create an instance. * * @since 1.1 + * @see Predictive Load-Balancing: Unfair but + * Faster & more Robust + * @see WeightedStatsRequestInterceptor */ public class WeightedLoadbalanceStrategy implements ClientLoadbalanceStrategy { @@ -55,7 +61,6 @@ public void initialize(RSocketConnector connector) { @Override public RSocket select(List sockets) { - final int numberOfAttepmts = this.maxPairSelectionAttempts; final int size = sockets.size(); RSocket weightedRSocket; @@ -83,7 +88,7 @@ public RSocket select(List sockets) { RSocket rsc1 = null; RSocket rsc2 = null; - for (int i = 0; i < numberOfAttepmts; i++) { + for (int i = 0; i < this.maxPairSelectionAttempts; i++) { int i1 = ThreadLocalRandom.current().nextInt(size); int i2 = ThreadLocalRandom.current().nextInt(size - 1); @@ -148,7 +153,10 @@ private static double calculateFactor(final double u, final double l, final doub return Math.pow(1 + alpha, EXP_FACTOR); } - /** Create an instance of {@link WeightedLoadbalanceStrategy} with default settings. */ + /** + * Create an instance of {@link WeightedLoadbalanceStrategy} with default settings, which include + * round-robin load-balancing and 5 {@link #maxPairSelectionAttempts}. + */ public static WeightedLoadbalanceStrategy create() { return new Builder().build(); } @@ -185,17 +193,22 @@ public Builder maxPairSelectionAttempts(int numberOfAttempts) { * Configure how the created {@link WeightedLoadbalanceStrategy} should find the stats for a * given RSocket. * - *

By default {@code WeightedLoadbalanceStrategy} installs a {@code RequestInterceptor} when - * {@link ClientLoadbalanceStrategy#initialize(RSocketConnector)} is called in order to keep - * track of stats. + *

By default this resolver is not set. + * + *

When {@code WeightedLoadbalanceStrategy} is used through the {@link + * LoadbalanceRSocketClient}, the resolver does not need to be set because a {@link + * WeightedStatsRequestInterceptor} is automatically installed through the {@link + * ClientLoadbalanceStrategy} callback. If this strategy is used in any other context however, a + * resolver here must be provided. * - * @param resolver the function to find the stats for an RSocket + * @param resolver to find the stats for an RSocket with */ public Builder weightedStatsResolver(Function resolver) { this.weightedStatsResolver = resolver; return this; } + /** Build the {@code WeightedLoadbalanceStrategy} instance. */ public WeightedLoadbalanceStrategy build() { return new WeightedLoadbalanceStrategy( this.maxPairSelectionAttempts, this.weightedStatsResolver); diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java index 372d7a77e..5ebe668ce 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java @@ -1,9 +1,26 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.loadbalance; import io.rsocket.RSocket; /** - * Representation of stats used by the {@link WeightedLoadbalanceStrategy}. + * Contract to expose the stats required in {@link WeightedLoadbalanceStrategy} to calculate an + * algorithmic weight for an {@code RSocket}. The weight helps to select an {@code RSocket} for + * load-balancing. * * @since 1.1 */ @@ -20,10 +37,11 @@ public interface WeightedStats { double weightedAvailability(); /** - * Wraps an RSocket with a proxy that implements WeightedStats. + * Create a proxy for the given {@code RSocket} that attaches the stats contained in this instance + * and exposes them as {@link WeightedStats}. * - * @param rsocket the RSocket to proxy. - * @return the wrapped RSocket. + * @param rsocket the RSocket to wrap + * @return the wrapped RSocket * @since 1.1.1 */ default RSocket wrap(RSocket rsocket) { diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRSocketProxy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRSocketProxy.java index 1103d2185..f2cf3fbd0 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRSocketProxy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRSocketProxy.java @@ -1,11 +1,26 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.loadbalance; import io.rsocket.RSocket; import io.rsocket.util.RSocketProxy; /** - * {@link RSocketProxy} that implements {@link WeightedStats} and delegates to an existing {@link - * WeightedStats} instance. + * Package private {@code RSocketProxy} used from {@link WeightedStats#wrap(RSocket)} to attach a + * {@link WeightedStats} instance to an {@code RSocket}. */ class WeightedStatsRSocketProxy extends RSocketProxy implements WeightedStats { diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRequestInterceptor.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRequestInterceptor.java index f1e790309..ec2c88b19 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRequestInterceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRequestInterceptor.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.loadbalance; import io.netty.buffer.ByteBuf; @@ -6,9 +21,15 @@ import reactor.util.annotation.Nullable; /** - * A {@link RequestInterceptor} implementation + * {@link RequestInterceptor} that hooks into request lifecycle and calls methods of the parent + * class to manage tracking state and expose {@link WeightedStats}. + * + *

This interceptor the default mechanism for gathering stats when {@link + * WeightedLoadbalanceStrategy} is used with {@link LoadbalanceRSocketClient}. * * @since 1.1 + * @see LoadbalanceRSocketClient + * @see WeightedLoadbalanceStrategy */ public class WeightedStatsRequestInterceptor extends BaseWeightedStats implements RequestInterceptor { diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/package-info.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/package-info.java index f5fd00a52..19668e99c 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/package-info.java @@ -14,6 +14,7 @@ * limitations under the License. */ +/** Support client load-balancing in RSocket Java. */ @NonNullApi package io.rsocket.loadbalance; From 3fdc78f67dad7dbcf9d6792bd30647bbf0fbf545 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 27 Apr 2021 13:30:11 +0100 Subject: [PATCH 023/116] adds Null-safe iteration of active streams (#1004) Closes gh-914 Signed-off-by: Rossen Stoyanchev --- .../io/rsocket/core/RSocketRequester.java | 21 ++++++++++--------- .../io/rsocket/core/RSocketResponder.java | 10 +++++++-- 2 files changed, 19 insertions(+), 12 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 89c1500bb..600b2ab08 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import static io.rsocket.keepalive.KeepAliveSupport.ClientKeepAliveSupport; import io.netty.buffer.ByteBuf; +import io.netty.util.collection.IntObjectMap; import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.RSocket; @@ -343,15 +344,15 @@ private void terminate(Throwable e) { } synchronized (this) { - activeStreams - .values() - .forEach( - receiver -> { - try { - receiver.handleError(e); - } catch (Throwable ignored) { - } - }); + for (IntObjectMap.PrimitiveEntry entry : activeStreams.entries()) { + FrameHandler handler = entry.value(); + if (handler != null) { + try { + handler.handleError(e); + } catch (Throwable ignored) { + } + } + } } if (e == CLOSED_CHANNEL_EXCEPTION) { 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 f0a052b93..969353bd6 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package io.rsocket.core; import io.netty.buffer.ByteBuf; +import io.netty.util.collection.IntObjectMap; import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.RSocket; @@ -183,7 +184,12 @@ final void doOnDispose() { } private synchronized void cleanUpSendingSubscriptions() { - activeStreams.values().forEach(FrameHandler::handleCancel); + for (IntObjectMap.PrimitiveEntry entry : activeStreams.entries()) { + FrameHandler handler = entry.value(); + if (handler != null) { + handler.handleCancel(); + } + } activeStreams.clear(); } From 67f6077f276ea60545b83947868c03758a825029 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 29 Apr 2021 17:03:34 +0100 Subject: [PATCH 024/116] replaces use of deprecated Reactor Processor API (#1003) --- .../io/rsocket/core/RSocketRequester.java | 12 +- .../internal/BaseDuplexConnection.java | 29 +- .../resume/InMemoryResumableFramesStore.java | 10 +- .../resume/ResumableDuplexConnection.java | 21 +- .../core/DefaultRSocketClientTests.java | 11 +- .../io/rsocket/core/RSocketConnectorTest.java | 54 ++-- .../io/rsocket/core/RSocketLeaseTest.java | 23 +- .../io/rsocket/core/RSocketRequesterTest.java | 54 ++-- .../io/rsocket/core/RSocketResponderTest.java | 34 +-- .../io/rsocket/core/RSocketServerTest.java | 26 +- .../java/io/rsocket/core/RSocketTest.java | 12 +- .../io/rsocket/core/ReconnectMonoTests.java | 256 +++++++++--------- .../io/rsocket/core/SetupRejectionTest.java | 31 ++- .../internal/subscriber/AssertSubscriber.java | 25 +- .../rsocket/loadbalance/LoadbalanceTest.java | 23 +- .../test/util/LocalDuplexConnection.java | 32 ++- .../test/util/TestServerTransport.java | 38 ++- .../integration/TcpIntegrationTest.java | 20 +- .../io/rsocket/client/TestingRSocket.java | 12 +- .../transport/local/LocalClientTransport.java | 13 +- .../local/LocalDuplexConnection.java | 16 +- .../transport/local/LocalServerTransport.java | 14 +- .../transport/netty/TcpDuplexConnection.java | 8 +- .../netty/WebsocketDuplexConnection.java | 8 +- .../transport/netty/SetupRejectionTest.java | 29 +- .../WebsocketPingPongIntegrationTest.java | 34 ++- 26 files changed, 510 insertions(+), 335 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 600b2ab08..c5853531b 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -43,7 +43,7 @@ import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Sinks; import reactor.util.annotation.Nullable; /** @@ -65,7 +65,7 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { @Nullable private final RequesterLeaseTracker requesterLeaseTracker; private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; - private final MonoProcessor onClose; + private final Sinks.Empty onClose; RSocketRequester( DuplexConnection connection, @@ -89,7 +89,7 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { requestInterceptorFunction); this.requesterLeaseTracker = requesterLeaseTracker; - this.onClose = MonoProcessor.create(); + this.onClose = Sinks.empty(); // DO NOT Change the order here. The Send processor must be subscribed to before receiving connection.onClose().subscribe(null, this::tryTerminateOnConnectionError, this::tryShutdown); @@ -196,7 +196,7 @@ public boolean isDisposed() { @Override public Mono onClose() { - return onClose; + return onClose.asMono(); } private void handleIncomingFrames(ByteBuf frame) { @@ -356,9 +356,9 @@ private void terminate(Throwable e) { } if (e == CLOSED_CHANNEL_EXCEPTION) { - onClose.onComplete(); + onClose.tryEmitEmpty(); } else { - onClose.onError(e); + onClose.tryEmitError(e); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java index 9fd33591a..98bed7ba7 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java @@ -1,17 +1,33 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.internal; import io.netty.buffer.ByteBuf; import io.rsocket.DuplexConnection; +import reactor.core.Scannable; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Sinks; public abstract class BaseDuplexConnection implements DuplexConnection { - protected MonoProcessor onClose = MonoProcessor.create(); + protected Sinks.Empty onClose = Sinks.empty(); protected UnboundedProcessor sender = new UnboundedProcessor(); public BaseDuplexConnection() { - onClose.doFinally(s -> doOnClose()).subscribe(); + onClose().doFinally(s -> doOnClose()).subscribe(); } @Override @@ -27,16 +43,17 @@ public void sendFrame(int streamId, ByteBuf frame) { @Override public final Mono onClose() { - return onClose; + return onClose.asMono(); } @Override public final void dispose() { - onClose.onComplete(); + onClose.tryEmitEmpty(); } @Override + @SuppressWarnings("ConstantConditions") public final boolean isDisposed() { - return onClose.isDisposed(); + return onClose.scan(Scannable.Attr.TERMINATED) || onClose.scan(Scannable.Attr.CANCELLED); } } 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 189799315..f0a370ae6 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,8 +28,8 @@ import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Operators; +import reactor.core.publisher.Sinks; /** * writes - n (where n is frequent, primary operation) reads - m (where m == KeepAliveFrequency) @@ -40,7 +40,7 @@ public class InMemoryResumableFramesStore extends Flux private static final Logger logger = LoggerFactory.getLogger(InMemoryResumableFramesStore.class); - final MonoProcessor disposed = MonoProcessor.create(); + final Sinks.Empty disposed = Sinks.empty(); final ArrayList cachedFrames; final String tag; final int cacheLimit; @@ -189,7 +189,7 @@ void resumeImplied() { @Override public Mono onClose() { - return disposed; + return disposed.asMono(); } @Override @@ -205,7 +205,7 @@ public void dispose() { } cachedFrames.clear(); } - disposed.onComplete(); + disposed.tryEmitEmpty(); } } 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 dbf77b902..6e90e6d63 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,9 +30,9 @@ import org.slf4j.LoggerFactory; import reactor.core.CoreSubscriber; import reactor.core.Disposable; +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.core.publisher.Sinks; @@ -46,7 +46,7 @@ public class ResumableDuplexConnection extends Flux final UnboundedProcessor savableFramesSender; final Disposable framesSaverDisposable; - final MonoProcessor onClose; + final Sinks.Empty onClose; final SocketAddress remoteAddress; final Sinks.Many onConnectionClosedSink; @@ -72,7 +72,7 @@ public ResumableDuplexConnection( this.resumableFramesStore = resumableFramesStore; this.savableFramesSender = new UnboundedProcessor(); this.framesSaverDisposable = resumableFramesStore.saveFrames(savableFramesSender).subscribe(); - this.onClose = MonoProcessor.create(); + this.onClose = Sinks.empty(); this.remoteAddress = initialConnection.remoteAddress(); ACTIVE_CONNECTION.lazySet(this, initialConnection); @@ -164,7 +164,7 @@ public void sendErrorAndClose(RSocketErrorException rSocketErrorException) { framesSaverDisposable.dispose(); savableFramesSender.dispose(); onConnectionClosedSink.tryEmitComplete(); - onClose.onError(t); + onClose.tryEmitError(t); }, () -> { framesSaverDisposable.dispose(); @@ -172,9 +172,9 @@ public void sendErrorAndClose(RSocketErrorException rSocketErrorException) { onConnectionClosedSink.tryEmitComplete(); final Throwable cause = rSocketErrorException.getCause(); if (cause == null) { - onClose.onComplete(); + onClose.tryEmitEmpty(); } else { - onClose.onError(cause); + onClose.tryEmitError(cause); } }); } @@ -191,7 +191,7 @@ public ByteBufAllocator alloc() { @Override public Mono onClose() { - return onClose; + return onClose.asMono(); } @Override @@ -210,12 +210,13 @@ public void dispose() { activeReceivingSubscriber.dispose(); savableFramesSender.dispose(); onConnectionClosedSink.tryEmitComplete(); - onClose.onComplete(); + onClose.tryEmitEmpty(); } @Override + @SuppressWarnings("ConstantConditions") public boolean isDisposed() { - return onClose.isDisposed(); + return onClose.scan(Scannable.Attr.TERMINATED) || onClose.scan(Scannable.Attr.CANCELLED); } @Override diff --git a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java index 1e3f86a7c..0c4682554 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -1,6 +1,6 @@ package io.rsocket.core; /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,8 +57,8 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.SignalType; +import reactor.core.publisher.Sinks; import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; import reactor.test.util.RaceTestUtils; @@ -516,16 +516,17 @@ public static class ClientSocketRule extends AbstractSocketRule producer; + protected Sinks.One producer; @Override protected void init() { super.init(); - delayer = () -> producer.onNext(socket); - producer = MonoProcessor.create(); + delayer = () -> producer.tryEmitValue(socket); + producer = Sinks.one(); client = new DefaultRSocketClient( producer + .asMono() .doOnCancel(() -> socket.dispose()) .doOnDiscard(Disposable.class, Disposable::dispose)); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java index 1a41346e5..40487bec1 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java @@ -1,6 +1,22 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.core; import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; +import static org.assertj.core.api.Assertions.assertThat; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -20,12 +36,11 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicLong; -import org.assertj.core.api.Assertions; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; import reactor.test.StepVerifier; import reactor.util.retry.Retry; @@ -89,7 +104,7 @@ public void unexpectedFramesBeforeResumeOKFrame(String frameType) { @Test public void ensuresThatSetupPayloadCanBeRetained() { - MonoProcessor retainedSetupPayload = MonoProcessor.create(); + AtomicReference retainedSetupPayload = new AtomicReference<>(); TestClientTransport transport = new TestClientTransport(); ByteBuf data = transport.alloc().buffer(); @@ -100,13 +115,13 @@ public void ensuresThatSetupPayloadCanBeRetained() { .setupPayload(ByteBufPayload.create(data)) .acceptor( (setup, sendingSocket) -> { - retainedSetupPayload.onNext(setup.retain()); + retainedSetupPayload.set(setup.retain()); return Mono.just(new RSocket() {}); }) .connect(transport) .block(); - Assertions.assertThat(transport.testConnection().getSent()) + assertThat(transport.testConnection().getSent()) .hasSize(1) .first() .matches( @@ -121,17 +136,10 @@ public void ensuresThatSetupPayloadCanBeRetained() { return buf.refCnt() == 1; }); - retainedSetupPayload - .as(StepVerifier::create) - .expectNextMatches( - setup -> { - String dataUtf8 = setup.getDataUtf8(); - return "data".equals(dataUtf8) && setup.release(); - }) - .expectComplete() - .verify(Duration.ofSeconds(5)); - - Assertions.assertThat(retainedSetupPayload.peek().refCnt()).isZero(); + ConnectionSetupPayload setup = retainedSetupPayload.get(); + String dataUtf8 = setup.getDataUtf8(); + assertThat("data".equals(dataUtf8) && setup.release()).isTrue(); + assertThat(setup.refCnt()).isZero(); transport.alloc().assertHasNoLeaks(); } @@ -139,7 +147,7 @@ public void ensuresThatSetupPayloadCanBeRetained() { @Test public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions() { Payload setupPayload = ByteBufPayload.create("TestData", "TestMetadata"); - Assertions.assertThat(setupPayload.refCnt()).isOne(); + assertThat(setupPayload.refCnt()).isOne(); // Keep the data and metadata around so we can try changing them independently ByteBuf dataBuf = setupPayload.data(); @@ -157,7 +165,7 @@ public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions .expectComplete() .verify(Duration.ofMillis(100)); - Assertions.assertThat(testClientTransport.testConnection().getSent()) + assertThat(testClientTransport.testConnection().getSent()) .hasSize(1) .allMatch( bb -> { @@ -182,7 +190,7 @@ public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions metadataBuf.writeChar('m'); metadataBuf.release(); - Assertions.assertThat(testClientTransport.testConnection().getSent()) + assertThat(testClientTransport.testConnection().getSent()) .hasSize(1) .allMatch( bb -> { @@ -195,7 +203,7 @@ public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions System.out.println("calling release " + byteBuf.refCnt()); return byteBuf.release(); }); - Assertions.assertThat(setupPayload.refCnt()).isZero(); + assertThat(setupPayload.refCnt()).isZero(); } @Test @@ -222,7 +230,7 @@ public void ensuresThatSetupPayloadProvidedAsMonoIsReleased() { .expectComplete() .verify(Duration.ofMillis(100)); - Assertions.assertThat(testClientTransport.testConnection().getSent()) + assertThat(testClientTransport.testConnection().getSent()) .hasSize(1) .allMatch( bb -> { @@ -238,7 +246,7 @@ public void ensuresThatSetupPayloadProvidedAsMonoIsReleased() { .expectComplete() .verify(Duration.ofMillis(100)); - Assertions.assertThat(testClientTransport.testConnection().getSent()) + assertThat(testClientTransport.testConnection().getSent()) .hasSize(1) .allMatch( bb -> { @@ -248,7 +256,7 @@ public void ensuresThatSetupPayloadProvidedAsMonoIsReleased() { }) .allMatch(ReferenceCounted::release); - Assertions.assertThat(saved) + assertThat(saved) .as("Metadata and data were consumed and released as slices") .allMatch( payload -> 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 a4978bd4f..a9c9ed9a5 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-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,12 @@ package io.rsocket.core; import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; -import static io.rsocket.frame.FrameType.*; +import static io.rsocket.frame.FrameType.COMPLETE; +import static io.rsocket.frame.FrameType.ERROR; +import static io.rsocket.frame.FrameType.LEASE; +import static io.rsocket.frame.FrameType.REQUEST_CHANNEL; +import static io.rsocket.frame.FrameType.REQUEST_FNF; +import static io.rsocket.frame.FrameType.SETUP; import static org.assertj.core.data.Offset.offset; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; @@ -68,10 +73,10 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.publisher.BaseSubscriber; -import reactor.core.publisher.EmitterProcessor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; +import reactor.core.publisher.Sinks; import reactor.test.StepVerifier; class RSocketLeaseTest { @@ -84,7 +89,7 @@ class RSocketLeaseTest { private RSocketResponder rSocketResponder; private RSocket mockRSocketHandler; - private EmitterProcessor leaseSender = EmitterProcessor.create(); + private Sinks.Many leaseSender = Sinks.many().multicast().onBackpressureBuffer(); private RequesterLeaseTracker requesterLeaseTracker; @BeforeEach @@ -94,7 +99,7 @@ void setUp() { connection = new TestDuplexConnection(byteBufAllocator); requesterLeaseTracker = new RequesterLeaseTracker(TAG, 0); - responderLeaseTracker = new ResponderLeaseTracker(TAG, connection, () -> leaseSender); + responderLeaseTracker = new ResponderLeaseTracker(TAG, connection, () -> leaseSender.asFlux()); ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(connection, new InitializingInterceptorRegistry(), true); @@ -425,7 +430,7 @@ void responderMissingLeaseRequestsAreRejected(FrameType frameType) { @ParameterizedTest @MethodSource("responderInteractions") void responderPresentLeaseRequestsAreAccepted(FrameType frameType) { - leaseSender.onNext(Lease.create(Duration.ofMillis(5_000), 2)); + leaseSender.tryEmitNext(Lease.create(Duration.ofMillis(5_000), 2)); ByteBuf buffer = byteBufAllocator.buffer(); buffer.writeCharSequence("test", CharsetUtil.UTF_8); @@ -492,7 +497,7 @@ void responderPresentLeaseRequestsAreAccepted(FrameType frameType) { @ParameterizedTest @MethodSource("responderInteractions") void responderDepletedAllowedLeaseRequestsAreRejected(FrameType frameType) { - leaseSender.onNext(Lease.create(Duration.ofMillis(5_000), 1)); + leaseSender.tryEmitNext(Lease.create(Duration.ofMillis(5_000), 1)); ByteBuf buffer = byteBufAllocator.buffer(); buffer.writeCharSequence("test", CharsetUtil.UTF_8); @@ -586,7 +591,7 @@ void responderDepletedAllowedLeaseRequestsAreRejected(FrameType frameType) { @ParameterizedTest @MethodSource("interactions") void expiredLeaseRequestsAreRejected(BiFunction> interaction) { - leaseSender.onNext(Lease.create(Duration.ofMillis(50), 1)); + leaseSender.tryEmitNext(Lease.create(Duration.ofMillis(50), 1)); ByteBuf buffer = byteBufAllocator.buffer(); buffer.writeCharSequence("test", CharsetUtil.UTF_8); @@ -615,7 +620,7 @@ void sendLease() { metadata.writeCharSequence(metadataContent, utf8); int ttl = 5_000; int numberOfRequests = 2; - leaseSender.onNext(Lease.create(Duration.ofMillis(5_000), 2, metadata)); + leaseSender.tryEmitNext(Lease.create(Duration.ofMillis(5_000), 2, metadata)); ByteBuf leaseFrame = connection 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 0904abe0d..cc52984d0 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,13 @@ import static io.rsocket.core.TestRequesterResponderSupport.randomPayload; import static io.rsocket.frame.FrameHeaderCodec.frameType; import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; -import static io.rsocket.frame.FrameType.*; +import static io.rsocket.frame.FrameType.CANCEL; +import static io.rsocket.frame.FrameType.COMPLETE; +import static io.rsocket.frame.FrameType.METADATA_PUSH; +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; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasSize; @@ -93,12 +99,12 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import reactor.core.Scannable; 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; +import reactor.core.publisher.Sinks; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; @@ -260,11 +266,11 @@ public void testRequestReplyErrorOnSend() { @Test @Timeout(2_000) public void testChannelRequestCancellation() { - MonoProcessor cancelled = MonoProcessor.create(); - Flux request = Flux.never().doOnCancel(cancelled::onComplete); + Sinks.Empty cancelled = Sinks.empty(); + Flux request = Flux.never().doOnCancel(cancelled::tryEmitEmpty); rule.socket.requestChannel(request).subscribe().dispose(); - Flux.first( - cancelled, + Flux.firstWithSignal( + cancelled.asMono(), Flux.error(new IllegalStateException("Channel request not cancelled")) .delaySubscription(Duration.ofSeconds(1))) .blockFirst(); @@ -274,12 +280,12 @@ public void testChannelRequestCancellation() { @Test @Timeout(2_000) public void testChannelRequestCancellation2() { - MonoProcessor cancelled = MonoProcessor.create(); + Sinks.Empty cancelled = Sinks.empty(); Flux request = - Flux.just(EmptyPayload.INSTANCE).repeat(259).doOnCancel(cancelled::onComplete); + Flux.just(EmptyPayload.INSTANCE).repeat(259).doOnCancel(cancelled::tryEmitEmpty); rule.socket.requestChannel(request).subscribe().dispose(); - Flux.first( - cancelled, + Flux.firstWithSignal( + cancelled.asMono(), Flux.error(new IllegalStateException("Channel request not cancelled")) .delaySubscription(Duration.ofSeconds(1))) .blockFirst(); @@ -289,20 +295,24 @@ public void testChannelRequestCancellation2() { @Test public void testChannelRequestServerSideCancellation() { - MonoProcessor cancelled = MonoProcessor.create(); - UnicastProcessor request = UnicastProcessor.create(); - request.onNext(EmptyPayload.INSTANCE); - rule.socket.requestChannel(request).subscribe(cancelled); + Sinks.One cancelled = Sinks.one(); + Sinks.Many request = Sinks.many().unicast().onBackpressureBuffer(); + request.tryEmitNext(EmptyPayload.INSTANCE); + rule.socket + .requestChannel(request.asFlux()) + .subscribe(cancelled::tryEmitValue, cancelled::tryEmitError, cancelled::tryEmitEmpty); int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); rule.connection.addToReceivedBuffer(CancelFrameCodec.encode(rule.alloc(), streamId)); rule.connection.addToReceivedBuffer(PayloadFrameCodec.encodeComplete(rule.alloc(), streamId)); - Flux.first( - cancelled, + Flux.firstWithSignal( + cancelled.asMono(), Flux.error(new IllegalStateException("Channel request not cancelled")) .delaySubscription(Duration.ofSeconds(1))) .blockFirst(); - Assertions.assertThat(request.isDisposed()).isTrue(); + Assertions.assertThat( + request.scan(Scannable.Attr.TERMINATED) || request.scan(Scannable.Attr.CANCELLED)) + .isTrue(); Assertions.assertThat(rule.connection.getSent()) .hasSize(1) .first() @@ -313,7 +323,7 @@ public void testChannelRequestServerSideCancellation() { @Test public void testCorrectFrameOrder() { - MonoProcessor delayer = MonoProcessor.create(); + Sinks.One delayer = Sinks.one(); BaseSubscriber subscriber = new BaseSubscriber() { @Override @@ -321,13 +331,13 @@ protected void hookOnSubscribe(Subscription subscription) {} }; rule.socket .requestChannel( - Flux.concat(Flux.just(0).delayUntil(i -> delayer), Flux.range(1, 999)) + Flux.concat(Flux.just(0).delayUntil(i -> delayer.asMono()), Flux.range(1, 999)) .map(i -> DefaultPayload.create(i + ""))) .subscribe(subscriber); subscriber.request(1); subscriber.request(Long.MAX_VALUE); - delayer.onComplete(); + delayer.tryEmitEmpty(); Iterator iterator = rule.connection.getSent().iterator(); 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 b82848b91..f2983dc8e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -96,8 +96,8 @@ import reactor.core.publisher.FluxSink; import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Operators; +import reactor.core.publisher.Sinks; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import reactor.test.publisher.TestPublisher; @@ -274,14 +274,14 @@ public void checkNoLeaksOnRacingCancelFromRequestChannelAndNextFromUpstream() { rule.setRequestInterceptor(testRequestInterceptor); for (int i = 0; i < 10000; i++) { AssertSubscriber assertSubscriber = AssertSubscriber.create(); - final MonoProcessor monoProcessor = MonoProcessor.create(); + final Sinks.One sink = Sinks.one(); rule.setAcceptingSocket( new RSocket() { @Override public Flux requestChannel(Publisher payloads) { payloads.subscribe(assertSubscriber); - return monoProcessor.flux(); + return sink.asMono().flux(); } }, Integer.MAX_VALUE); @@ -315,7 +315,7 @@ public Flux requestChannel(Publisher payloads) { }, () -> { assertSubscriber.cancel(); - monoProcessor.onComplete(); + sink.tryEmitEmpty(); }); Assertions.assertThat(assertSubscriber.values()).allMatch(ReferenceCounted::release); @@ -1097,32 +1097,32 @@ public Flux requestChannel(Publisher payloads) { void receivingRequestOnStreamIdThaIsAlreadyInUseMUSTBeIgnored_ReassemblyCase( FrameType requestType) { AtomicReference receivedPayload = new AtomicReference<>(); - final MonoProcessor delayer = MonoProcessor.create(); + final Sinks.Empty delayer = Sinks.empty(); rule.setAcceptingSocket( new RSocket() { @Override public Mono fireAndForget(Payload payload) { receivedPayload.set(payload); - return delayer; + return delayer.asMono(); } @Override public Mono requestResponse(Payload payload) { receivedPayload.set(payload); - return Mono.just(genericPayload(rule.allocator)).delaySubscription(delayer); + return Mono.just(genericPayload(rule.allocator)).delaySubscription(delayer.asMono()); } @Override public Flux requestStream(Payload payload) { receivedPayload.set(payload); - return Flux.just(genericPayload(rule.allocator)).delaySubscription(delayer); + return Flux.just(genericPayload(rule.allocator)).delaySubscription(delayer.asMono()); } @Override public Flux requestChannel(Publisher payloads) { Flux.from(payloads).subscribe(receivedPayload::set, null, null, s -> s.request(1)); - return Flux.just(genericPayload(rule.allocator)).delaySubscription(delayer); + return Flux.just(genericPayload(rule.allocator)).delaySubscription(delayer.asMono()); } }); final Payload randomPayload1 = fixedSizePayload(rule.allocator, 128); @@ -1138,9 +1138,9 @@ public Flux requestChannel(Publisher payloads) { rule.connection.addToReceivedBuffer(fragments1.toArray(new ByteBuf[0])); if (requestType != REQUEST_CHANNEL) { rule.connection.addToReceivedBuffer(fragments2.toArray(new ByteBuf[0])); - delayer.onComplete(); + delayer.tryEmitEmpty(); } else { - delayer.onComplete(); + delayer.tryEmitEmpty(); rule.connection.addToReceivedBuffer(PayloadFrameCodec.encodeComplete(rule.allocator, 1)); rule.connection.addToReceivedBuffer(fragments2.toArray(new ByteBuf[0])); } @@ -1166,25 +1166,25 @@ public Flux requestChannel(Publisher payloads) { void receivingRequestOnStreamIdThaIsAlreadyInUseMUSTBeIgnored(FrameType requestType) { Assumptions.assumeThat(requestType).isNotEqualTo(REQUEST_FNF); AtomicReference receivedPayload = new AtomicReference<>(); - final MonoProcessor delayer = MonoProcessor.create(); + final Sinks.One delayer = Sinks.one(); rule.setAcceptingSocket( new RSocket() { @Override public Mono requestResponse(Payload payload) { receivedPayload.set(payload); - return Mono.just(genericPayload(rule.allocator)).delaySubscription(delayer); + return Mono.just(genericPayload(rule.allocator)).delaySubscription(delayer.asMono()); } @Override public Flux requestStream(Payload payload) { receivedPayload.set(payload); - return Flux.just(genericPayload(rule.allocator)).delaySubscription(delayer); + return Flux.just(genericPayload(rule.allocator)).delaySubscription(delayer.asMono()); } @Override public Flux requestChannel(Publisher payloads) { Flux.from(payloads).subscribe(receivedPayload::set, null, null, s -> s.request(1)); - return Flux.just(genericPayload(rule.allocator)).delaySubscription(delayer); + return Flux.just(genericPayload(rule.allocator)).delaySubscription(delayer.asMono()); } }); final Payload randomPayload1 = fixedSizePayload(rule.allocator, 64); @@ -1192,7 +1192,7 @@ public Flux requestChannel(Publisher payloads) { rule.sendRequest(1, requestType, randomPayload1.retain()); rule.sendRequest(1, requestType, randomPayload2); - delayer.onComplete(); + delayer.tryEmitEmpty(); PayloadAssert.assertThat(receivedPayload.get()).isEqualTo(randomPayload1).hasNoLeaks(); randomPayload1.release(); diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java index fc3da93dd..08555740c 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.core; import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; @@ -15,8 +30,9 @@ import java.time.Duration; import java.util.Random; import org.junit.jupiter.api.Test; +import reactor.core.Scannable; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Sinks; import reactor.test.StepVerifier; public class RSocketServerTest { @@ -81,13 +97,13 @@ public void ensuresMaxFrameLengthCanNotBeGreaterThenMaxPossibleFrameLength() { @Test public void unexpectedFramesBeforeSetup() { - MonoProcessor connectedMono = MonoProcessor.create(); + Sinks.Empty connectedSink = Sinks.empty(); TestServerTransport transport = new TestServerTransport(); RSocketServer.create() .acceptor( (setup, sendingSocket) -> { - connectedMono.onComplete(); + connectedSink.tryEmitEmpty(); return Mono.just(new RSocket() {}); }) .bind(transport) @@ -106,6 +122,8 @@ public void unexpectedFramesBeforeSetup() { ByteBufAllocator.DEFAULT.buffer(bytes.length).writeBytes(bytes))); StepVerifier.create(connection.onClose()).expectComplete().verify(Duration.ofSeconds(30)); - assertThat(connectedMono.isTerminated()).as("Connection should not succeed").isFalse(); + assertThat(connectedSink.scan(Scannable.Attr.TERMINATED)) + .as("Connection should not succeed") + .isFalse(); } } 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 62b449be1..f502f2f88 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-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,9 +43,9 @@ 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; +import reactor.core.publisher.Sinks; import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; @@ -489,8 +489,8 @@ void errorFromRequesterPublisher( public static class SocketRule extends ExternalResource { - DirectProcessor serverProcessor; - DirectProcessor clientProcessor; + Sinks.Many serverProcessor; + Sinks.Many clientProcessor; private RSocketRequester crs; @SuppressWarnings("unused") @@ -517,8 +517,8 @@ public LeaksTrackingByteBufAllocator alloc() { protected void init() { allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); - serverProcessor = DirectProcessor.create(); - clientProcessor = DirectProcessor.create(); + serverProcessor = Sinks.many().multicast().directBestEffort(); + clientProcessor = Sinks.many().multicast().directBestEffort(); LocalDuplexConnection serverConnection = new LocalDuplexConnection("server", allocator, clientProcessor, serverProcessor); diff --git a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java index 8d96222df..25a5fb221 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import static org.junit.Assert.assertEquals; +import io.rsocket.internal.subscriber.AssertSubscriber; import java.io.IOException; import java.time.Duration; import java.util.ArrayList; @@ -39,7 +40,6 @@ 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; @@ -76,7 +76,8 @@ public void subscribe(CoreSubscriber actual) { .doOnDiscard(Object.class, System.out::println) .as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + final AssertSubscriber subscriber = + reconnectMono.subscribeWith(new AssertSubscriber<>()); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); @@ -85,17 +86,17 @@ public void subscribe(CoreSubscriber actual) { monoSubscribers[0].onComplete(); - Assertions.assertThat(processor.isTerminated()).isTrue(); + subscriber.assertTerminated(); Mockito.verify(mockSubscription).cancel(); - if (processor.isError()) { - Assertions.assertThat(processor.getError()) - .isInstanceOf(CancellationException.class) - .hasMessage("ReconnectMono has already been disposed"); + if (!subscriber.errors().isEmpty()) { + subscriber + .assertError(CancellationException.class) + .assertErrorMessage("ReconnectMono has already been disposed"); Assertions.assertThat(expired).containsOnly("value" + i); } else { - Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + subscriber.assertValues("value" + i); } expired.clear(); @@ -113,20 +114,20 @@ public void shouldNotifyAllTheSubscribersUnderRacingBetweenSubscribeAndComplete( final ReconnectMono reconnectMono = cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); - final MonoProcessor racerProcessor = MonoProcessor.create(); + final AssertSubscriber subscriber = + reconnectMono.subscribeWith(new AssertSubscriber<>()); + final AssertSubscriber raceSubscriber = new AssertSubscriber<>(); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); cold.next("value" + i); - RaceTestUtils.race(cold::complete, () -> reconnectMono.subscribe(racerProcessor)); - - Assertions.assertThat(processor.isTerminated()).isTrue(); + RaceTestUtils.race(cold::complete, () -> reconnectMono.subscribe(raceSubscriber)); - Assertions.assertThat(processor.peek()).isEqualTo("value" + i); - Assertions.assertThat(racerProcessor.peek()).isEqualTo("value" + i); + subscriber.assertTerminated(); + subscriber.assertValues("value" + i); + raceSubscriber.assertValues("value" + i); Assertions.assertThat(reconnectMono.resolvingInner.subscribers) .isEqualTo(ResolvingOperator.READY); @@ -134,7 +135,7 @@ public void shouldNotifyAllTheSubscribersUnderRacingBetweenSubscribeAndComplete( Assertions.assertThat( reconnectMono.resolvingInner.add( new ResolvingOperator.MonoDeferredResolutionOperator<>( - reconnectMono.resolvingInner, processor))) + reconnectMono.resolvingInner, subscriber))) .isEqualTo(ResolvingOperator.READY_STATE); Assertions.assertThat(expired).isEmpty(); @@ -157,8 +158,9 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() final ReconnectMono reconnectMono = cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); - final MonoProcessor racerProcessor = MonoProcessor.create(); + final AssertSubscriber subscriber = + reconnectMono.subscribeWith(new AssertSubscriber<>()); + final AssertSubscriber raceSubscriber = new AssertSubscriber<>(); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); @@ -169,28 +171,24 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() RaceTestUtils.race( reconnectMono::invalidate, () -> { - reconnectMono.subscribe(racerProcessor); - if (!racerProcessor.isTerminated()) { + reconnectMono.subscribe(raceSubscriber); + if (!raceSubscriber.isTerminated()) { reconnectMono.resolvingInner.mainSubscriber.onNext("value_to_not_expire" + index); reconnectMono.resolvingInner.mainSubscriber.onComplete(); } }, Schedulers.parallel()); - Assertions.assertThat(processor.isTerminated()).isTrue(); - - Assertions.assertThat(processor.peek()).isEqualTo("value_to_expire" + i); - StepVerifier.create(racerProcessor) - .expectNextMatches( - (v) -> { - if (reconnectMono.resolvingInner.subscribers == ResolvingOperator.READY) { - return v.equals("value_to_not_expire" + index); - } else { - return v.equals("value_to_expire" + index); - } - }) - .expectComplete() - .verify(Duration.ofMillis(100)); + subscriber.assertTerminated(); + subscriber.assertValues("value_to_expire" + i); + + raceSubscriber.assertComplete(); + String v = raceSubscriber.values().get(0); + if (reconnectMono.resolvingInner.subscribers == ResolvingOperator.READY) { + Assertions.assertThat(v).isEqualTo("value_to_not_expire" + index); + } else { + Assertions.assertThat(v).isEqualTo("value_to_expire" + index); + } Assertions.assertThat(expired).hasSize(1).containsOnly("value_to_expire" + i); if (reconnectMono.resolvingInner.subscribers == ResolvingOperator.READY) { @@ -221,8 +219,9 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates( final ReconnectMono reconnectMono = cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); - final MonoProcessor racerProcessor = MonoProcessor.create(); + final AssertSubscriber subscriber = + reconnectMono.subscribeWith(new AssertSubscriber<>()); + final AssertSubscriber raceSubscriber = new AssertSubscriber<>(); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); @@ -235,8 +234,8 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates( RaceTestUtils.race( reconnectMono::invalidate, reconnectMono::invalidate, Schedulers.parallel()), () -> { - reconnectMono.subscribe(racerProcessor); - if (!racerProcessor.isTerminated()) { + reconnectMono.subscribe(raceSubscriber); + if (!raceSubscriber.isTerminated()) { reconnectMono.resolvingInner.mainSubscriber.onNext( "value_to_possibly_expire" + index); reconnectMono.resolvingInner.mainSubscriber.onComplete(); @@ -244,16 +243,12 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates( }, Schedulers.parallel()); - Assertions.assertThat(processor.isTerminated()).isTrue(); + subscriber.assertTerminated(); + subscriber.assertValues("value_to_expire" + i); - Assertions.assertThat(processor.peek()).isEqualTo("value_to_expire" + i); - StepVerifier.create(racerProcessor) - .expectNextMatches( - (v) -> - v.equals("value_to_possibly_expire" + index) - || v.equals("value_to_expire" + index)) - .expectComplete() - .verify(Duration.ofMillis(100)); + raceSubscriber.assertComplete(); + Assertions.assertThat(raceSubscriber.values().get(0)) + .isIn("value_to_possibly_expire" + index, "value_to_expire" + index); if (expired.size() == 2) { Assertions.assertThat(expired) @@ -290,7 +285,8 @@ public void shouldNotExpireNewlyResolvedValueIfBlockIsRacingWithInvalidate() { final ReconnectMono reconnectMono = cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + final AssertSubscriber subscriber = + reconnectMono.subscribeWith(new AssertSubscriber<>()); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); @@ -321,9 +317,9 @@ public void shouldNotExpireNewlyResolvedValueIfBlockIsRacingWithInvalidate() { Schedulers.parallel()), Schedulers.parallel()); - Assertions.assertThat(processor.isTerminated()).isTrue(); + subscriber.assertTerminated(); - Assertions.assertThat(processor.peek()).isEqualTo("value_to_expire" + i); + subscriber.assertValues("value_to_expire" + i); Assertions.assertThat(expired).hasSize(1).containsOnly("value_to_expire" + i); if (reconnectMono.resolvingInner.subscribers == ResolvingOperator.READY) { @@ -352,8 +348,8 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribers() { final ReconnectMono reconnectMono = cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - final MonoProcessor processor = MonoProcessor.create(); - final MonoProcessor racerProcessor = MonoProcessor.create(); + final AssertSubscriber subscriber = new AssertSubscriber<>(); + final AssertSubscriber raceSubscriber = new AssertSubscriber<>(); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); @@ -361,13 +357,13 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribers() { Assertions.assertThat(cold.subscribeCount()).isZero(); RaceTestUtils.race( - () -> reconnectMono.subscribe(processor), () -> reconnectMono.subscribe(racerProcessor)); + () -> reconnectMono.subscribe(subscriber), () -> reconnectMono.subscribe(raceSubscriber)); - Assertions.assertThat(processor.isTerminated()).isTrue(); - Assertions.assertThat(racerProcessor.isTerminated()).isTrue(); + subscriber.assertTerminated(); + Assertions.assertThat(raceSubscriber.isTerminated()).isTrue(); - Assertions.assertThat(processor.peek()).isEqualTo("value" + i); - Assertions.assertThat(racerProcessor.peek()).isEqualTo("value" + i); + subscriber.assertValues("value" + i); + raceSubscriber.assertValues("value" + i); Assertions.assertThat(reconnectMono.resolvingInner.subscribers) .isEqualTo(ResolvingOperator.READY); @@ -377,7 +373,7 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribers() { Assertions.assertThat( reconnectMono.resolvingInner.add( new ResolvingOperator.MonoDeferredResolutionOperator<>( - reconnectMono.resolvingInner, processor))) + reconnectMono.resolvingInner, subscriber))) .isEqualTo(ResolvingOperator.READY_STATE); Assertions.assertThat(expired).isEmpty(); @@ -399,7 +395,7 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { final ReconnectMono reconnectMono = cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - final MonoProcessor processor = MonoProcessor.create(); + final AssertSubscriber subscriber = new AssertSubscriber<>(); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); @@ -409,11 +405,12 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { String[] values = new String[1]; RaceTestUtils.race( - () -> values[0] = reconnectMono.block(timeout), () -> reconnectMono.subscribe(processor)); + () -> values[0] = reconnectMono.block(timeout), + () -> reconnectMono.subscribe(subscriber)); - Assertions.assertThat(processor.isTerminated()).isTrue(); + subscriber.assertTerminated(); - Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + subscriber.assertValues("value" + i); Assertions.assertThat(values).containsExactly("value" + i); Assertions.assertThat(reconnectMono.resolvingInner.subscribers) @@ -424,7 +421,7 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { Assertions.assertThat( reconnectMono.resolvingInner.add( new ResolvingOperator.MonoDeferredResolutionOperator<>( - reconnectMono.resolvingInner, processor))) + reconnectMono.resolvingInner, subscriber))) .isEqualTo(ResolvingOperator.READY_STATE); Assertions.assertThat(expired).isEmpty(); @@ -469,7 +466,7 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenBlocks() { Assertions.assertThat( reconnectMono.resolvingInner.add( new ResolvingOperator.MonoDeferredResolutionOperator<>( - reconnectMono.resolvingInner, MonoProcessor.create()))) + reconnectMono.resolvingInner, new AssertSubscriber<>()))) .isEqualTo(ResolvingOperator.READY_STATE); Assertions.assertThat(expired).isEmpty(); @@ -491,16 +488,17 @@ public void shouldExpireValueOnRacingDisposeAndNoValueComplete() { final ReconnectMono reconnectMono = cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + final AssertSubscriber subscriber = + reconnectMono.subscribeWith(new AssertSubscriber<>()); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); RaceTestUtils.race(cold::complete, reconnectMono::dispose); - Assertions.assertThat(processor.isTerminated()).isTrue(); + subscriber.assertTerminated(); - Throwable error = processor.getError(); + Throwable error = subscriber.errors().get(0); if (error instanceof CancellationException) { Assertions.assertThat(error) @@ -529,7 +527,8 @@ public void shouldExpireValueOnRacingDisposeAndComplete() { final ReconnectMono reconnectMono = cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + final AssertSubscriber subscriber = + reconnectMono.subscribeWith(new AssertSubscriber<>()); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); @@ -538,17 +537,17 @@ public void shouldExpireValueOnRacingDisposeAndComplete() { RaceTestUtils.race(cold::complete, reconnectMono::dispose); - Assertions.assertThat(processor.isTerminated()).isTrue(); + subscriber.assertTerminated(); - if (processor.isError()) { - Assertions.assertThat(processor.getError()) + if (!subscriber.errors().isEmpty()) { + Assertions.assertThat(subscriber.errors().get(0)) .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); + subscriber.assertValues("value" + i); } Assertions.assertThat(expired).hasSize(1).containsOnly("value" + i); @@ -569,7 +568,8 @@ public void shouldExpireValueOnRacingDisposeAndError() { final ReconnectMono reconnectMono = cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + final AssertSubscriber subscriber = + reconnectMono.subscribeWith(new AssertSubscriber<>()); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); @@ -578,23 +578,22 @@ public void shouldExpireValueOnRacingDisposeAndError() { RaceTestUtils.race(() -> cold.error(runtimeException), reconnectMono::dispose); - Assertions.assertThat(processor.isTerminated()).isTrue(); + subscriber.assertTerminated(); - if (processor.isError()) { - if (processor.getError() instanceof CancellationException) { - Assertions.assertThat(processor.getError()) + if (!subscriber.errors().isEmpty()) { + Throwable error = subscriber.errors().get(0); + if (error instanceof CancellationException) { + Assertions.assertThat(error) .isInstanceOf(CancellationException.class) .hasMessage("ReconnectMono has already been disposed"); } else { - Assertions.assertThat(processor.getError()) - .isInstanceOf(RuntimeException.class) - .hasMessage("test"); + Assertions.assertThat(error).isInstanceOf(RuntimeException.class).hasMessage("test"); } } else { Assertions.assertThat(received) .hasSize(1) .containsOnly(Tuples.of("value" + i, reconnectMono)); - Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + subscriber.assertValues("value" + i); } Assertions.assertThat(expired).hasSize(1).containsOnly("value" + i); @@ -617,7 +616,8 @@ public void shouldExpireValueOnRacingDisposeAndErrorWithNoBackoff() { .retryWhen(Retry.max(1).filter(t -> t instanceof Exception)) .as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + final AssertSubscriber subscriber = + reconnectMono.subscribeWith(new AssertSubscriber<>()); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); @@ -626,17 +626,17 @@ public void shouldExpireValueOnRacingDisposeAndErrorWithNoBackoff() { RaceTestUtils.race(() -> cold.error(runtimeException), reconnectMono::dispose); - Assertions.assertThat(processor.isTerminated()).isTrue(); - - if (processor.isError()) { + subscriber.assertTerminated(); - if (processor.getError() instanceof CancellationException) { - Assertions.assertThat(processor.getError()) + if (!subscriber.errors().isEmpty()) { + Throwable error = subscriber.errors().get(0); + if (error instanceof CancellationException) { + Assertions.assertThat(error) .isInstanceOf(CancellationException.class) .hasMessage("ReconnectMono has already been disposed"); } else { - Assertions.assertThat(processor.getError()) - .matches(t -> Exceptions.isRetryExhausted(t)) + Assertions.assertThat(error) + .matches(Exceptions::isRetryExhausted) .hasCause(runtimeException); } @@ -645,7 +645,7 @@ public void shouldExpireValueOnRacingDisposeAndErrorWithNoBackoff() { Assertions.assertThat(received) .hasSize(1) .containsOnly(Tuples.of("value" + i, reconnectMono)); - Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + subscriber.assertValues("value" + i); } expired.clear(); @@ -702,9 +702,10 @@ public void shouldBeScannable() { .isEqualTo(false); Assertions.assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.ERROR)).isNull(); - final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + final AssertSubscriber subscriber = + reconnectMono.subscribeWith(new AssertSubscriber<>()); - final Scannable scannableOfMonoProcessor = Scannable.from(processor); + final Scannable scannableOfMonoProcessor = Scannable.from(subscriber); Assertions.assertThat( (List) @@ -735,25 +736,25 @@ public void shouldNotExpiredIfNotCompleted() { final ReconnectMono reconnectMono = publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - MonoProcessor processor = MonoProcessor.create(); + AssertSubscriber subscriber = new AssertSubscriber<>(); - reconnectMono.subscribe(processor); + reconnectMono.subscribe(subscriber); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(processor.isTerminated()).isFalse(); + Assertions.assertThat(subscriber.isTerminated()).isFalse(); publisher.next("test"); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(processor.isTerminated()).isFalse(); + Assertions.assertThat(subscriber.isTerminated()).isFalse(); reconnectMono.invalidate(); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(processor.isTerminated()).isFalse(); + Assertions.assertThat(subscriber.isTerminated()).isFalse(); publisher.assertSubscribers(1); Assertions.assertThat(publisher.subscribeCount()).isEqualTo(1); @@ -761,7 +762,7 @@ public void shouldNotExpiredIfNotCompleted() { Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).hasSize(1); - Assertions.assertThat(processor.isTerminated()).isTrue(); + subscriber.assertTerminated(); publisher.assertSubscribers(0); Assertions.assertThat(publisher.subscribeCount()).isEqualTo(1); @@ -775,26 +776,26 @@ public void shouldNotEmitUntilCompletion() { final ReconnectMono reconnectMono = publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - MonoProcessor processor = MonoProcessor.create(); + AssertSubscriber subscriber = new AssertSubscriber<>(); - reconnectMono.subscribe(processor); + reconnectMono.subscribe(subscriber); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(processor.isTerminated()).isFalse(); + Assertions.assertThat(subscriber.isTerminated()).isFalse(); publisher.next("test"); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(processor.isTerminated()).isFalse(); + Assertions.assertThat(subscriber.isTerminated()).isFalse(); publisher.complete(); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).hasSize(1); - Assertions.assertThat(processor.isTerminated()).isTrue(); - Assertions.assertThat(processor.peek()).isEqualTo("test"); + subscriber.assertTerminated(); + subscriber.assertValues("test"); } @Test @@ -805,21 +806,21 @@ public void shouldBePossibleToRemoveThemSelvesFromTheList_CancellationTest() { final ReconnectMono reconnectMono = publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - MonoProcessor processor = MonoProcessor.create(); + AssertSubscriber subscriber = new AssertSubscriber<>(); - reconnectMono.subscribe(processor); + reconnectMono.subscribe(subscriber); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(processor.isTerminated()).isFalse(); + Assertions.assertThat(subscriber.isTerminated()).isFalse(); publisher.next("test"); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(processor.isTerminated()).isFalse(); + Assertions.assertThat(subscriber.isTerminated()).isFalse(); - processor.cancel(); + subscriber.cancel(); Assertions.assertThat(reconnectMono.resolvingInner.subscribers) .isEqualTo(ResolvingOperator.EMPTY_SUBSCRIBED); @@ -828,8 +829,7 @@ public void shouldBePossibleToRemoveThemSelvesFromTheList_CancellationTest() { Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).hasSize(1); - Assertions.assertThat(processor.isTerminated()).isFalse(); - Assertions.assertThat(processor.peek()).isNull(); + Assertions.assertThat(subscriber.values()).isEmpty(); } @Test @@ -870,10 +870,10 @@ public void shouldNotifyAllTheSubscribers() { 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(); + final AssertSubscriber sub1 = new AssertSubscriber<>(); + final AssertSubscriber sub2 = new AssertSubscriber<>(); + final AssertSubscriber sub3 = new AssertSubscriber<>(); + final AssertSubscriber sub4 = new AssertSubscriber<>(); reconnectMono.subscribe(sub1); reconnectMono.subscribe(sub2); @@ -882,31 +882,31 @@ public void shouldNotifyAllTheSubscribers() { Assertions.assertThat(reconnectMono.resolvingInner.subscribers).hasSize(4); - final ArrayList> processors = new ArrayList<>(200); + final ArrayList> subscribers = 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); + final AssertSubscriber subA = new AssertSubscriber<>(); + final AssertSubscriber subB = new AssertSubscriber<>(); + subscribers.add(subA); + subscribers.add(subB); RaceTestUtils.race(() -> reconnectMono.subscribe(subA), () -> reconnectMono.subscribe(subB)); } Assertions.assertThat(reconnectMono.resolvingInner.subscribers).hasSize(204); - sub1.dispose(); + sub1.cancel(); Assertions.assertThat(reconnectMono.resolvingInner.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"); + Assertions.assertThat(sub1.scan(Scannable.Attr.CANCELLED)).isTrue(); + Assertions.assertThat(sub2.values().get(0)).isEqualTo("value"); + Assertions.assertThat(sub3.values().get(0)).isEqualTo("value"); + Assertions.assertThat(sub4.values().get(0)).isEqualTo("value"); - for (MonoProcessor sub : processors) { - Assertions.assertThat(sub.peek()).isEqualTo("value"); + for (AssertSubscriber sub : subscribers) { + Assertions.assertThat(sub.values().get(0)).isEqualTo("value"); Assertions.assertThat(sub.isTerminated()).isTrue(); } 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 173385b55..44ff78a64 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.core; import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; @@ -6,7 +21,12 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.rsocket.*; +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.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.Exceptions; import io.rsocket.exceptions.RejectedSetupException; @@ -20,7 +40,7 @@ import java.time.Duration; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; -import reactor.core.publisher.UnicastProcessor; +import reactor.core.publisher.Sinks; import reactor.test.StepVerifier; public class SetupRejectionTest { @@ -115,7 +135,8 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { private static class RejectingAcceptor implements SocketAcceptor { private final String errorMessage; - private final UnicastProcessor senderRSockets = UnicastProcessor.create(); + private final Sinks.Many senderRSockets = + Sinks.many().unicast().onBackpressureBuffer(); public RejectingAcceptor(String errorMessage) { this.errorMessage = errorMessage; @@ -123,12 +144,12 @@ public RejectingAcceptor(String errorMessage) { @Override public Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket) { - senderRSockets.onNext(sendingSocket); + senderRSockets.tryEmitNext(sendingSocket); return Mono.error(new RuntimeException(errorMessage)); } public Mono senderRSocket() { - return senderRSockets.next(); + return senderRSockets.asFlux().next(); } } 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 index 28206b4ff..ceb69531f 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/subscriber/AssertSubscriber.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/subscriber/AssertSubscriber.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2017 Pivotal Software Inc, All Rights Reserved. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; +import reactor.core.Scannable; import reactor.core.publisher.Operators; import reactor.util.annotation.NonNull; import reactor.util.context.Context; @@ -78,7 +79,7 @@ * @author Stephane Maldini * @author Brian Clozel */ -public class AssertSubscriber implements CoreSubscriber, Subscription { +public class AssertSubscriber implements CoreSubscriber, Subscription, Scannable { /** Default timeout for waiting next values to be received */ public static final Duration DEFAULT_VALUES_TIMEOUT = Duration.ofSeconds(3); @@ -1197,6 +1198,10 @@ public List values() { return values; } + public List errors() { + return errors; + } + public final AssertSubscriber assertNoEvents() { return assertNoValues().assertNoError().assertNotComplete(); } @@ -1205,4 +1210,20 @@ public final AssertSubscriber assertNoEvents() { public final AssertSubscriber assertIncomplete(T... values) { return assertValues(values).assertNotComplete().assertNoError(); } + + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) { + return upstream(); + } + + boolean t = isTerminated(); + if (key == Attr.TERMINATED) return t; + if (key == Attr.ERROR) return (!errors.isEmpty() ? errors.get(0) : null); + if (key == Attr.PREFETCH) return Integer.MAX_VALUE; + if (key == Attr.CANCELLED) return isCancelled(); + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + + return null; + } } diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java index 52b4e0e13..8d55b1422 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.loadbalance; import io.rsocket.Payload; @@ -20,7 +35,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Sinks; import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; import reactor.test.util.RaceTestUtils; @@ -285,7 +300,7 @@ public Flux requestChannel(Publisher source) { static class TestRSocket extends RSocketProxy { - final MonoProcessor processor = MonoProcessor.create(); + final Sinks.Empty sink = Sinks.empty(); public TestRSocket(RSocket rSocket) { super(rSocket); @@ -293,12 +308,12 @@ public TestRSocket(RSocket rSocket) { @Override public Mono onClose() { - return processor; + return sink.asMono(); } @Override public void dispose() { - processor.onComplete(); + sink.tryEmitEmpty(); } } 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 9f5c021af..cdfcefdc8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,48 +24,49 @@ import java.net.SocketAddress; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; -import reactor.core.publisher.DirectProcessor; +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.core.publisher.Sinks; public class LocalDuplexConnection implements DuplexConnection { private final ByteBufAllocator allocator; - private final DirectProcessor send; - private final DirectProcessor receive; - private final MonoProcessor onClose; + private final Sinks.Many send; + private final Sinks.Many receive; + private final Sinks.Empty onClose; private final String name; public LocalDuplexConnection( String name, ByteBufAllocator allocator, - DirectProcessor send, - DirectProcessor receive) { + Sinks.Many send, + Sinks.Many receive) { this.name = name; this.allocator = allocator; this.send = send; this.receive = receive; - this.onClose = MonoProcessor.create(); + this.onClose = Sinks.empty(); } @Override public void sendFrame(int streamId, ByteBuf frame) { System.out.println(name + " - " + frame.toString()); - send.onNext(frame); + send.tryEmitNext(frame); } @Override public void sendErrorAndClose(RSocketErrorException e) { final ByteBuf errorFrame = ErrorFrameCodec.encode(allocator, 0, e); System.out.println(name + " - " + errorFrame.toString()); - send.onNext(errorFrame); - onClose.onComplete(); + send.tryEmitNext(errorFrame); + onClose.tryEmitEmpty(); } @Override public Flux receive() { return receive + .asFlux() .doOnNext(f -> System.out.println(name + " - " + f.toString())) .transform( Operators.lift( @@ -107,16 +108,17 @@ public SocketAddress remoteAddress() { @Override public void dispose() { - onClose.onComplete(); + onClose.tryEmitEmpty(); } @Override + @SuppressWarnings("ConstantConditions") public boolean isDisposed() { - return onClose.isDisposed(); + return onClose.scan(Scannable.Attr.TERMINATED) || onClose.scan(Scannable.Attr.CANCELLED); } @Override public Mono onClose() { - return onClose; + return onClose.asMono(); } } 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 0f9ea8e48..fa9331d3b 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,3 +1,18 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.test.util; import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; @@ -6,11 +21,13 @@ import io.rsocket.Closeable; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.transport.ServerTransport; +import reactor.core.Scannable; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Sinks; public class TestServerTransport implements ServerTransport { - private final MonoProcessor conn = MonoProcessor.create(); + private final Sinks.One connSink = Sinks.one(); + private TestDuplexConnection connection; private final LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); @@ -18,29 +35,33 @@ public class TestServerTransport implements ServerTransport { @Override public Mono start(ConnectionAcceptor acceptor) { - conn.flatMap(acceptor::apply) + connSink + .asMono() + .flatMap(duplexConnection -> acceptor.apply(duplexConnection)) .subscribe(ignored -> {}, err -> disposeConnection(), this::disposeConnection); return Mono.just( new Closeable() { @Override public Mono onClose() { - return conn.then(); + return connSink.asMono().then(); } @Override public void dispose() { - conn.onComplete(); + connSink.tryEmitEmpty(); } @Override + @SuppressWarnings("ConstantConditions") public boolean isDisposed() { - return conn.isTerminated(); + return connSink.scan(Scannable.Attr.TERMINATED) + || connSink.scan(Scannable.Attr.CANCELLED); } }); } private void disposeConnection() { - TestDuplexConnection c = conn.peek(); + TestDuplexConnection c = connection; if (c != null) { c.dispose(); } @@ -48,7 +69,8 @@ private void disposeConnection() { public TestDuplexConnection connect() { TestDuplexConnection c = new TestDuplexConnection(allocator); - conn.onNext(c); + connection = c; + connSink.tryEmitValue(c); return c; } 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 de27bcb9b..bad28f4dc 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.UnicastProcessor; +import reactor.core.publisher.Sinks; import reactor.core.scheduler.Schedulers; public class TcpIntegrationTest { @@ -147,17 +147,17 @@ public Mono requestResponse(Payload payload) { @Test(timeout = 15_000L) public void testTwoConcurrentStreams() throws InterruptedException { - ConcurrentHashMap> map = new ConcurrentHashMap<>(); - UnicastProcessor processor1 = UnicastProcessor.create(); + ConcurrentHashMap> map = new ConcurrentHashMap<>(); + Sinks.Many processor1 = Sinks.many().unicast().onBackpressureBuffer(); map.put("REQUEST1", processor1); - UnicastProcessor processor2 = UnicastProcessor.create(); + Sinks.Many processor2 = Sinks.many().unicast().onBackpressureBuffer(); map.put("REQUEST2", processor2); handler = new RSocket() { @Override public Flux requestStream(Payload payload) { - return map.get(payload.getDataUtf8()); + return map.get(payload.getDataUtf8()).asFlux(); } }; @@ -177,13 +177,13 @@ public Flux requestStream(Payload payload) { .subscribeOn(Schedulers.newSingle("2")) .subscribe(c -> nextCountdown.countDown(), t -> {}, completeCountdown::countDown); - processor1.onNext(DefaultPayload.create("RESPONSE1A")); - processor2.onNext(DefaultPayload.create("RESPONSE2A")); + processor1.tryEmitNext(DefaultPayload.create("RESPONSE1A")); + processor2.tryEmitNext(DefaultPayload.create("RESPONSE2A")); nextCountdown.await(); - processor1.onComplete(); - processor2.onComplete(); + processor1.tryEmitComplete(); + processor2.tryEmitComplete(); completeCountdown.await(); } diff --git a/rsocket-load-balancer/src/test/java/io/rsocket/client/TestingRSocket.java b/rsocket-load-balancer/src/test/java/io/rsocket/client/TestingRSocket.java index 96982121b..2827c8ed4 100644 --- a/rsocket-load-balancer/src/test/java/io/rsocket/client/TestingRSocket.java +++ b/rsocket-load-balancer/src/test/java/io/rsocket/client/TestingRSocket.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,12 +24,13 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import reactor.core.Scannable; import reactor.core.publisher.*; public class TestingRSocket implements RSocket { private final AtomicInteger count; - private final MonoProcessor onClose = MonoProcessor.create(); + private final Sinks.Empty onClose = Sinks.empty(); private final BiFunction, Payload, Boolean> eachPayloadHandler; public TestingRSocket(Function responder) { @@ -128,16 +129,17 @@ public double availability() { @Override public void dispose() { - onClose.onComplete(); + onClose.tryEmitEmpty(); } @Override + @SuppressWarnings("ConstantConditions") public boolean isDisposed() { - return onClose.isDisposed(); + return onClose.scan(Scannable.Attr.TERMINATED) || onClose.scan(Scannable.Attr.CANCELLED); } @Override public Mono onClose() { - return onClose; + return onClose.asMono(); } } 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 ef15c9a09..588f772d3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import io.rsocket.transport.ServerTransport; import java.util.Objects; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Sinks; /** * An implementation of {@link ClientTransport} that connects to a {@link ServerTransport} in the @@ -79,15 +79,12 @@ public Mono connect() { UnboundedProcessor in = new UnboundedProcessor(); UnboundedProcessor out = new UnboundedProcessor(); - MonoProcessor closeNotifier = MonoProcessor.create(); + Sinks.Empty closeSink = Sinks.empty(); - server - .apply(new LocalDuplexConnection(name, allocator, out, in, closeNotifier)) - .subscribe(); + server.apply(new LocalDuplexConnection(name, allocator, out, in, closeSink)).subscribe(); return Mono.just( - (DuplexConnection) - new LocalDuplexConnection(name, allocator, in, out, closeNotifier)); + (DuplexConnection) new LocalDuplexConnection(name, allocator, in, out, closeSink)); }); } } 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 6c1782073..5e18aa4cc 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,10 +27,11 @@ 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.Mono; -import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Operators; +import reactor.core.publisher.Sinks; /** An implementation of {@link DuplexConnection} that connects inside the same JVM. */ final class LocalDuplexConnection implements DuplexConnection { @@ -39,7 +40,7 @@ final class LocalDuplexConnection implements DuplexConnection { private final ByteBufAllocator allocator; private final Flux in; - private final MonoProcessor onClose; + private final Sinks.Empty onClose; private final UnboundedProcessor out; @@ -57,7 +58,7 @@ final class LocalDuplexConnection implements DuplexConnection { ByteBufAllocator allocator, Flux in, UnboundedProcessor out, - MonoProcessor onClose) { + Sinks.Empty onClose) { this.address = new LocalSocketAddress(name); this.allocator = Objects.requireNonNull(allocator, "allocator must not be null"); this.in = Objects.requireNonNull(in, "in must not be null"); @@ -68,17 +69,18 @@ final class LocalDuplexConnection implements DuplexConnection { @Override public void dispose() { out.onComplete(); - onClose.onComplete(); + onClose.tryEmitEmpty(); } @Override + @SuppressWarnings("ConstantConditions") public boolean isDisposed() { - return onClose.isDisposed(); + return onClose.scan(Scannable.Attr.TERMINATED) || onClose.scan(Scannable.Attr.CANCELLED); } @Override public Mono onClose() { - return onClose; + return onClose.asMono(); } @Override 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 c07713cb3..7ea1f8cda 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,9 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import reactor.core.Scannable; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Sinks; import reactor.util.annotation.Nullable; /** @@ -120,7 +121,7 @@ static class ServerCloseable implements Closeable { private final ConnectionAcceptor acceptor; - private final MonoProcessor onClose = MonoProcessor.create(); + private final Sinks.Empty onClose = Sinks.empty(); ServerCloseable(String name, ConnectionAcceptor acceptor) { Objects.requireNonNull(name, "name must not be null"); @@ -133,17 +134,18 @@ public void dispose() { if (!registry.remove(address.getName(), acceptor)) { throw new AssertionError(); } - onClose.onComplete(); + onClose.tryEmitEmpty(); } @Override + @SuppressWarnings("ConstantConditions") public boolean isDisposed() { - return onClose.isDisposed(); + return onClose.scan(Scannable.Attr.TERMINATED) || onClose.scan(Scannable.Attr.CANCELLED); } @Override public Mono onClose() { - return onClose; + return onClose.asMono(); } } } 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 c57ebe59c..f9ac705b1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,13 +77,13 @@ public void sendErrorAndClose(RSocketErrorException e) { .then() .subscribe( null, - t -> onClose.onError(t), + t -> onClose.tryEmitError(t), () -> { final Throwable cause = e.getCause(); if (cause == null) { - onClose.onComplete(); + onClose.tryEmitEmpty(); } else { - onClose.onError(cause); + onClose.tryEmitError(cause); } }); } 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 b6d542dcb..c81f040da 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,13 +87,13 @@ public void sendErrorAndClose(RSocketErrorException e) { .then() .subscribe( null, - t -> onClose.onError(t), + t -> onClose.tryEmitError(t), () -> { final Throwable cause = e.getCause(); if (cause == null) { - onClose.onComplete(); + onClose.tryEmitEmpty(); } else { - onClose.onError(cause); + onClose.tryEmitError(cause); } }); } 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 6fd3de791..76c352768 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 @@ -1,3 +1,18 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.transport.netty; import io.rsocket.ConnectionSetupPayload; @@ -20,9 +35,9 @@ import java.util.function.Function; import java.util.stream.Stream; import org.junit.jupiter.params.provider.Arguments; -import reactor.core.publisher.EmitterProcessor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; import reactor.test.StepVerifier; public class SetupRejectionTest { @@ -85,21 +100,21 @@ static Stream transports() { } static class ErrorConsumer implements Consumer { - private final EmitterProcessor errors = EmitterProcessor.create(); + private final Sinks.Many errors = Sinks.many().multicast().onBackpressureBuffer(); @Override public void accept(Throwable t) { - errors.onNext(t); + errors.tryEmitNext(t); } Flux errors() { - return errors; + return errors.asFlux(); } } private static class RejectingAcceptor implements SocketAcceptor { private final String msg; - private final EmitterProcessor requesters = EmitterProcessor.create(); + private final Sinks.Many requesters = Sinks.many().multicast().onBackpressureBuffer(); public RejectingAcceptor(String msg) { this.msg = msg; @@ -107,12 +122,12 @@ public RejectingAcceptor(String msg) { @Override public Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket) { - requesters.onNext(sendingSocket); + requesters.tryEmitNext(sendingSocket); return Mono.error(new RuntimeException(msg)); } public Mono requesterRSocket() { - return requesters.next(); + return requesters.asFlux().next(); } } } 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 e2ee9e521..ff0fa75b4 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 @@ -1,3 +1,18 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.transport.netty; import io.netty.buffer.Unpooled; @@ -25,8 +40,9 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.Scannable; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Sinks; import reactor.netty.http.client.HttpClient; import reactor.netty.http.server.HttpServer; import reactor.test.StepVerifier; @@ -100,13 +116,13 @@ private static Stream provideServerTransport() { } private static class PingSender extends ChannelInboundHandlerAdapter { - private final MonoProcessor channel = MonoProcessor.create(); - private final MonoProcessor pong = MonoProcessor.create(); + private final Sinks.One channel = Sinks.one(); + private final Sinks.One pong = Sinks.one(); @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof PongWebSocketFrame) { - pong.onNext(((PongWebSocketFrame) msg).content().toString(StandardCharsets.UTF_8)); + pong.tryEmitValue(((PongWebSocketFrame) msg).content().toString(StandardCharsets.UTF_8)); ReferenceCountUtil.safeRelease(msg); ctx.read(); } else { @@ -117,8 +133,8 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception @Override public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { Channel ch = ctx.channel(); - if (!channel.isTerminated() && ch.isWritable()) { - channel.onNext(ctx.channel()); + if (!(channel.scan(Scannable.Attr.TERMINATED)) && ch.isWritable()) { + channel.tryEmitValue(ctx.channel()); } super.channelWritabilityChanged(ctx); } @@ -127,7 +143,7 @@ public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exceptio public void handlerAdded(ChannelHandlerContext ctx) throws Exception { Channel ch = ctx.channel(); if (ch.isWritable()) { - channel.onNext(ch); + channel.tryEmitValue(ch); } super.handlerAdded(ctx); } @@ -142,11 +158,11 @@ public Mono sendPong() { } public Mono receivePong() { - return pong; + return pong.asMono(); } private Mono send(WebSocketFrame webSocketFrame) { - return channel.doOnNext(ch -> ch.writeAndFlush(webSocketFrame)).then(); + return channel.asMono().doOnNext(ch -> ch.writeAndFlush(webSocketFrame)).then(); } } } From 04a5e35c200119c6011348977ac1482719a2ab34 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 5 May 2021 15:05:51 +0100 Subject: [PATCH 025/116] Switch to Reactor 2020.0.7 snapshots See gh-1005 Signed-off-by: Rossen Stoyanchev --- build.gradle | 2 +- .../java/io/rsocket/core/ReconnectMonoTests.java | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index f665350c3..a105fae5e 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = '2020.0.4' + ext['reactor-bom.version'] = '2020.0.7-SNAPSHOT' ext['logback.version'] = '1.2.3' ext['netty-bom.version'] = '4.1.59.Final' ext['netty-boringssl.version'] = '2.0.36.Final' diff --git a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java index 25a5fb221..427320216 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java @@ -857,7 +857,7 @@ public void shouldExpireValueOnDispose() { Assertions.assertThat(received).hasSize(1); Assertions.assertThat(reconnectMono.isDisposed()).isTrue(); - StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.boundedElastic())) .expectSubscription() .expectError(CancellationException.class) .verify(Duration.ofSeconds(timeout)); @@ -923,7 +923,7 @@ public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidates() { final ReconnectMono reconnectMono = cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.boundedElastic())) .expectSubscription() .expectNext("value") .expectComplete() @@ -937,7 +937,7 @@ public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidates() { Assertions.assertThat(expired).hasSize(1).containsOnly("value"); Assertions.assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); - StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.boundedElastic())) .expectSubscription() .expectNext("value") .expectComplete() @@ -965,7 +965,7 @@ public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidateAndDispose() { final ReconnectMono reconnectMono = cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.boundedElastic())) .expectSubscription() .expectNext("value") .expectComplete() @@ -979,7 +979,7 @@ public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidateAndDispose() { Assertions.assertThat(expired).hasSize(1).containsOnly("value"); Assertions.assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); - StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.boundedElastic())) .expectSubscription() .expectError(CancellationException.class) .verify(Duration.ofSeconds(timeout)); @@ -1011,7 +1011,7 @@ public void shouldTimeoutRetryWithVirtualTime() { .maxBackoff(Duration.ofSeconds(maxBackoff))) .timeout(Duration.ofSeconds(timeout)) .as(m -> new ReconnectMono<>(m, onExpire(), onValue())) - .subscribeOn(Schedulers.elastic())) + .subscribeOn(Schedulers.boundedElastic())) .expectSubscription() .thenAwait(Duration.ofSeconds(timeout)) .expectError(TimeoutException.class) @@ -1027,7 +1027,7 @@ public void ensuresThatMainSubscriberAllowsOnlyTerminationWithValue() { final ReconnectMono reconnectMono = new ReconnectMono<>(Mono.empty(), onExpire(), onValue()); - StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.boundedElastic())) .expectSubscription() .expectErrorSatisfies( t -> From 026ec4b7831161c1b74e34d97de731866f956915 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 5 May 2021 15:22:59 +0100 Subject: [PATCH 026/116] Switch to Reactor Dysprosium snapshots See gh-1006 Signed-off-by: Rossen Stoyanchev --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bd1c0b388..5230ce018 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = 'Dysprosium-SR17' + ext['reactor-bom.version'] = 'Dysprosium-BUILD-SNAPSHOT' ext['logback.version'] = '1.2.3' ext['netty-bom.version'] = '4.1.59.Final' ext['netty-boringssl.version'] = '2.0.36.Final' From dc50e7ad0135117f14c10f140d20c9aabebf33d5 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 11 May 2021 15:04:19 +0100 Subject: [PATCH 027/116] Upgrade to Reactor 2020.0.7 Closes gh-1005 Signed-off-by: Rossen Stoyanchev --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a105fae5e..2101a3255 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = '2020.0.7-SNAPSHOT' + ext['reactor-bom.version'] = '2020.0.7' ext['logback.version'] = '1.2.3' ext['netty-bom.version'] = '4.1.59.Final' ext['netty-boringssl.version'] = '2.0.36.Final' From 42e98f202539a4b849db8a864b620fc93d93aee7 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 13 May 2021 17:47:14 +0300 Subject: [PATCH 028/116] fixes netty-tcnative-boringssl-static dependency resolution (#1001) --- rsocket-transport-netty/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 70db87b3a..baabe1f4e 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -23,7 +23,7 @@ plugins { } def os_suffix = "" -if (osdetector.classifier in ["linux-x86_64"] || ["osx-x86_64"] || ["windows-x86_64"]) { +if (osdetector.classifier in ["linux-x86_64", "osx-x86_64", "windows-x86_64"]) { os_suffix = "::" + osdetector.classifier } From c337cba6bd947485ef6871595dcba7e43f49e11f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 13 May 2021 15:13:49 +0100 Subject: [PATCH 029/116] Set an "Automatic-Module-Name" for each module Closes gh-910 Signed-off-by: Rossen Stoyanchev --- rsocket-core/build.gradle | 6 ++++++ rsocket-micrometer/build.gradle | 6 ++++++ rsocket-test/build.gradle | 6 ++++++ rsocket-transport-local/build.gradle | 6 ++++++ rsocket-transport-netty/build.gradle | 6 ++++++ 5 files changed, 30 insertions(+) diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index 53a896aea..5d33c2b5f 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -45,4 +45,10 @@ dependencies { testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' } +jar { + manifest { + attributes("Automatic-Module-Name": "rsocket.core") + } +} + description = "Core functionality for the RSocket library" \ No newline at end of file diff --git a/rsocket-micrometer/build.gradle b/rsocket-micrometer/build.gradle index 4be616623..1827111b6 100644 --- a/rsocket-micrometer/build.gradle +++ b/rsocket-micrometer/build.gradle @@ -37,4 +37,10 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' } +jar { + manifest { + attributes("Automatic-Module-Name": "rsocket.micrometer") + } +} + description = 'Transparent Metrics exposure to Micrometer' diff --git a/rsocket-test/build.gradle b/rsocket-test/build.gradle index 5ec1a8061..bdbecda41 100644 --- a/rsocket-test/build.gradle +++ b/rsocket-test/build.gradle @@ -34,4 +34,10 @@ dependencies { implementation 'junit:junit' } +jar { + manifest { + attributes("Automatic-Module-Name": "rsocket.test") + } +} + description = 'Test utilities for RSocket projects' diff --git a/rsocket-transport-local/build.gradle b/rsocket-transport-local/build.gradle index a5ba84d5c..816d16db6 100644 --- a/rsocket-transport-local/build.gradle +++ b/rsocket-transport-local/build.gradle @@ -33,4 +33,10 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' } +jar { + manifest { + attributes("Automatic-Module-Name": "rsocket.transport.local") + } +} + description = 'Local RSocket transport implementation' diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index baabe1f4e..919a82abb 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -46,4 +46,10 @@ dependencies { testRuntimeOnly 'io.netty:netty-tcnative-boringssl-static' + os_suffix } +jar { + manifest { + attributes("Automatic-Module-Name": "rsocket.transport.netty") + } +} + description = 'Reactor Netty RSocket transport implementations (TCP, Websocket)' From b828b849a77d831e3787dafef829cfb875d7a515 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 18 May 2021 09:26:36 +0300 Subject: [PATCH 030/116] migrates from deprecated RaceTestUtils.race; fixes observed issues Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- build.gradle | 1 + .../io/rsocket/core/RSocketConnector.java | 2 +- .../io/rsocket/core/ResolvingOperator.java | 15 +- .../rsocket/internal/UnboundedProcessor.java | 64 ++-- .../io/rsocket/core/RSocketRequesterTest.java | 13 +- .../io/rsocket/core/RSocketResponderTest.java | 32 +- .../io/rsocket/core/ReconnectMonoTests.java | 83 ++--- .../rsocket/core/ResolvingOperatorTests.java | 34 +-- .../io/rsocket/exceptions/ExceptionsTest.java | 2 + .../internal/UnboundedProcessorTest.java | 289 ++++++++++++++---- .../internal/subscriber/AssertSubscriber.java | 112 +++++-- 11 files changed, 426 insertions(+), 221 deletions(-) diff --git a/build.gradle b/build.gradle index 5230ce018..64e7401df 100644 --- a/build.gradle +++ b/build.gradle @@ -128,6 +128,7 @@ subprojects { links 'https://projectreactor.io/docs/core/release/api/' links 'https://netty.io/4.1/api/' } + failOnError = false } tasks.named("javadoc").configure { 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 b6eaec7c9..eab70cc30 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -213,7 +213,7 @@ public RSocketConnector metadataMimeType(String metadataMimeType) { *
  • 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. + * frames is often {@code >} 30,000ms. * * *

    By default these are set to 20 seconds and 90 seconds respectively. diff --git a/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java b/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java index c431b3f3f..979743fb1 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java @@ -153,19 +153,19 @@ public T block(@Nullable Duration timeout) { delay = System.nanoTime() + timeout.toNanos(); } for (; ; ) { - BiConsumer[] inners = this.subscribers; + subscribers = this.subscribers; - if (inners == READY) { + if (subscribers == READY) { final T value = this.value; if (value != null) { return value; } else { // value == null means racing between invalidate and this block // thus, we have to update the state again and see what happened - inners = this.subscribers; + subscribers = this.subscribers; } } - if (inners == TERMINATED) { + if (subscribers == TERMINATED) { RuntimeException re = Exceptions.propagate(this.t); re = Exceptions.addSuppressed(re, new Exception("Terminated with an error")); throw re; @@ -174,6 +174,12 @@ public T block(@Nullable Duration timeout) { throw new IllegalStateException("Timeout on Mono blocking read"); } + // connect again since invalidate() has happened in between + if (subscribers == EMPTY_UNSUBSCRIBED + && SUBSCRIBERS.compareAndSet(this, EMPTY_UNSUBSCRIBED, EMPTY_SUBSCRIBED)) { + this.doSubscribe(); + } + Thread.sleep(1); } } catch (InterruptedException ie) { @@ -186,6 +192,7 @@ public T block(@Nullable Duration timeout) { @SuppressWarnings("unchecked") final void terminate(Throwable t) { if (isDisposed()) { + Operators.onErrorDropped(t, Context.empty()); return; } 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 d2a438dfd..94d5e9a7a 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -115,13 +115,9 @@ void drainRegular(Subscriber a) { while (r != e) { boolean d = done; - T t; - boolean empty; - - if (!pq.isEmpty()) { - t = pq.poll(); - empty = false; - } else { + T t = pq.poll(); + boolean empty = t == null; + if (empty) { t = q.poll(); empty = t == null; } @@ -196,8 +192,9 @@ void drainFused(Subscriber a) { } public void drain() { - if (WIP.getAndIncrement(this) != 0) { - if ((!outputFused && cancelled) || terminated) { + final int previousWip = WIP.getAndIncrement(this); + if (previousWip != 0) { + if (previousWip < 0 || terminated) { this.clear(); } return; @@ -231,6 +228,7 @@ boolean checkTerminated(boolean d, boolean empty, Subscriber a) { return true; } if (d && empty) { + this.clear(); Throwable e = error; hasDownstream = false; if (e != null) { @@ -330,11 +328,7 @@ public void subscribe(CoreSubscriber actual) { actual.onSubscribe(this); this.actual = actual; - if (cancelled) { - this.hasDownstream = false; - } else { - drain(); - } + drain(); } else { Operators.error( actual, @@ -388,6 +382,18 @@ public boolean isEmpty() { @Override public void clear() { terminated = true; + for (; ; ) { + int wip = this.wip; + + clearSafely(); + + if (WIP.compareAndSet(this, wip, Integer.MIN_VALUE)) { + return; + } + } + } + + void clearSafely() { if (DISCARD_GUARD.getAndIncrement(this) != 0) { return; } @@ -428,34 +434,20 @@ public void dispose() { error = new CancellationException("Disposed"); done = true; - boolean once = true; if (WIP.getAndIncrement(this) == 0) { cancelled = true; - int m = 1; - for (; ; ) { - final CoreSubscriber a = this.actual; - - if (!outputFused || terminated) { - clear(); - } - - if (a != null && once) { - try { - a.onError(error); - } catch (Throwable ignored) { - } - } + final CoreSubscriber a = this.actual; - cancelled = true; - once = false; + if (!outputFused || terminated) { + clear(); + } - int wip = this.wip; - if (wip == m) { - break; + if (a != null) { + try { + a.onError(error); + } catch (Throwable ignored) { } - m = wip; } - hasDownstream = false; } } 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 1ce68cfeb..b5a3dcb83 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -91,7 +91,6 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.UnicastProcessor; -import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; import reactor.test.util.RaceTestUtils; @@ -1082,15 +1081,11 @@ public void shouldTerminateAllStreamsIfThereRacingBetweenDisposeAndRequests( Publisher publisher2 = interaction2.apply(rule, payload2); RaceTestUtils.race( () -> rule.socket.dispose(), - () -> - RaceTestUtils.race( - () -> publisher1.subscribe(assertSubscriber1), - () -> publisher2.subscribe(assertSubscriber2), - Schedulers.parallel()), - Schedulers.parallel()); + () -> publisher1.subscribe(assertSubscriber1), + () -> publisher2.subscribe(assertSubscriber2)); assertSubscriber1.await().assertTerminated(); - if (interactionType1 != REQUEST_FNF) { + if (interactionType1 != REQUEST_FNF && interactionType1 != METADATA_PUSH) { assertSubscriber1.assertError(ClosedChannelException.class); } else { try { @@ -1101,7 +1096,7 @@ public void shouldTerminateAllStreamsIfThereRacingBetweenDisposeAndRequests( } } assertSubscriber2.await().assertTerminated(); - if (interactionType2 != REQUEST_FNF) { + if (interactionType2 != REQUEST_FNF && interactionType2 != METADATA_PUSH) { assertSubscriber2.assertError(ClosedChannelException.class); } else { try { 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 0d0fbd8c0..76691adce 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -84,8 +84,6 @@ 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.publisher.TestPublisher; import reactor.test.util.RaceTestUtils; @@ -340,7 +338,6 @@ public Flux requestChannel(Publisher payloads) { @Test public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChannelTest1() { - Scheduler parallel = Schedulers.parallel(); Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); for (int i = 0; i < 10000; i++) { @@ -366,17 +363,13 @@ public Flux requestChannel(Publisher payloads) { ByteBuf requestNFrame = RequestNFrameCodec.encode(allocator, 1, Integer.MAX_VALUE); FluxSink sink = sinks[0]; RaceTestUtils.race( - () -> - RaceTestUtils.race( - () -> rule.connection.addToReceivedBuffer(requestNFrame), - () -> rule.connection.addToReceivedBuffer(cancelFrame), - parallel), + () -> rule.connection.addToReceivedBuffer(requestNFrame), + () -> 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); @@ -387,7 +380,6 @@ public Flux requestChannel(Publisher payloads) { @Test public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromUpstreamOnErrorFromRequestChannelTest1() { - Scheduler parallel = Schedulers.parallel(); Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); for (int i = 0; i < 10000; i++) { @@ -453,18 +445,14 @@ public Flux requestChannel(Publisher payloads) { FluxSink sink = sinks[0]; RaceTestUtils.race( - () -> - RaceTestUtils.race( - () -> rule.connection.addToReceivedBuffer(requestNFrame), - () -> rule.connection.addToReceivedBuffer(nextFrame1, nextFrame2, nextFrame3), - parallel), + () -> rule.connection.addToReceivedBuffer(requestNFrame), + () -> rule.connection.addToReceivedBuffer(nextFrame1, nextFrame2, nextFrame3), () -> { sink.next(np1); sink.next(np2); sink.next(np3); sink.error(new RuntimeException()); - }, - parallel); + }); Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); @@ -484,7 +472,6 @@ public Flux requestChannel(Publisher payloads) { @Test public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestStreamTest1() { - Scheduler parallel = Schedulers.parallel(); Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); for (int i = 0; i < 10000; i++) { @@ -510,8 +497,7 @@ public Flux requestStream(Payload payload) { 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); @@ -521,7 +507,6 @@ public Flux requestStream(Payload payload) { @Test public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestResponseTest1() { - Scheduler parallel = Schedulers.parallel(); Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); for (int i = 0; i < 10000; i++) { @@ -550,8 +535,7 @@ public void subscribe(CoreSubscriber actual) { () -> rule.connection.addToReceivedBuffer(cancelFrame), () -> { sources[0].complete(ByteBufPayload.create("d1", "m1")); - }, - parallel); + }); Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); diff --git a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java index 8d96222df..ad3013f8e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java @@ -18,6 +18,7 @@ import static org.junit.Assert.assertEquals; +import io.rsocket.internal.subscriber.AssertSubscriber; import java.io.IOException; import java.time.Duration; import java.util.ArrayList; @@ -29,6 +30,7 @@ import java.util.concurrent.TimeoutException; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.assertj.core.api.Assertions; import org.junit.Test; @@ -174,8 +176,7 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() reconnectMono.resolvingInner.mainSubscriber.onNext("value_to_not_expire" + index); reconnectMono.resolvingInner.mainSubscriber.onComplete(); } - }, - Schedulers.parallel()); + }); Assertions.assertThat(processor.isTerminated()).isTrue(); @@ -231,9 +232,8 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates( reconnectMono.resolvingInner.mainSubscriber.onComplete(); RaceTestUtils.race( - () -> - RaceTestUtils.race( - reconnectMono::invalidate, reconnectMono::invalidate, Schedulers.parallel()), + reconnectMono::invalidate, + reconnectMono::invalidate, () -> { reconnectMono.subscribe(racerProcessor); if (!racerProcessor.isTerminated()) { @@ -241,8 +241,7 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates( "value_to_possibly_expire" + index); reconnectMono.resolvingInner.mainSubscriber.onComplete(); } - }, - Schedulers.parallel()); + }); Assertions.assertThat(processor.isTerminated()).isTrue(); @@ -284,46 +283,54 @@ public void shouldNotExpireNewlyResolvedValueIfBlockIsRacingWithInvalidate() { Hooks.onErrorDropped(t -> {}); for (int i = 0; i < 10000; i++) { final int index = i; - final TestPublisher cold = - TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + final Mono source = + Mono.fromSupplier( + new Supplier() { + boolean once = false; - final ReconnectMono reconnectMono = - cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + @Override + public String get() { - final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + if (!once) { + once = true; + return "value_to_expire" + index; + } + + return "value_to_not_expire" + index; + } + }); + + final ReconnectMono reconnectMono = + new ReconnectMono<>( + source.subscribeOn(Schedulers.boundedElastic()), onExpire(), onValue()); Assertions.assertThat(expired).isEmpty(); Assertions.assertThat(received).isEmpty(); - reconnectMono.resolvingInner.mainSubscriber.onNext("value_to_expire" + i); - reconnectMono.resolvingInner.mainSubscriber.onComplete(); + final AssertSubscriber subscriber = + reconnectMono.subscribeWith(new AssertSubscriber<>()); - RaceTestUtils.race( - () -> - Assertions.assertThat(reconnectMono.block()) - .matches( - (v) -> - v.equals("value_to_not_expire" + index) - || v.equals("value_to_expire" + index)), - () -> - RaceTestUtils.race( - reconnectMono::invalidate, - () -> { - for (; ; ) { - if (reconnectMono.resolvingInner.subscribers != ResolvingOperator.READY) { - reconnectMono.resolvingInner.mainSubscriber.onNext( - "value_to_not_expire" + index); - reconnectMono.resolvingInner.mainSubscriber.onComplete(); - break; - } - } - }, - Schedulers.parallel()), - Schedulers.parallel()); + subscriber.await().assertComplete(); - Assertions.assertThat(processor.isTerminated()).isTrue(); + Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(processor.peek()).isEqualTo("value_to_expire" + i); + try { + + RaceTestUtils.race( + () -> + Assertions.assertThat(reconnectMono.block()) + .matches( + (v) -> + v.equals("value_to_not_expire" + index) + || v.equals("value_to_expire" + index)), + reconnectMono::invalidate); + } catch (Throwable t) { + t.printStackTrace(); + } + + subscriber.assertTerminated(); + + subscriber.assertValues("value_to_expire" + i); Assertions.assertThat(expired).hasSize(1).containsOnly("value_to_expire" + i); if (reconnectMono.resolvingInner.subscribers == ResolvingOperator.READY) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java b/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java index 29748abbe..608e1a336 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java @@ -38,7 +38,6 @@ import org.reactivestreams.Subscription; import reactor.core.publisher.Hooks; import reactor.core.publisher.MonoProcessor; -import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; import reactor.test.util.RaceTestUtils; import reactor.util.retry.Retry; @@ -194,8 +193,7 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() if (!processor2.isTerminated()) { self.complete(valueToSend2); } - }, - Schedulers.parallel())) + })) .then( self -> { if (self.isPending()) { @@ -270,16 +268,14 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates( .then( self -> RaceTestUtils.race( - () -> - RaceTestUtils.race( - self::invalidate, self::invalidate, Schedulers.parallel()), + self::invalidate, + self::invalidate, () -> { self.observe(consumer2); if (!processor2.isTerminated()) { self.complete(valueToSend2); } - }, - Schedulers.parallel())) + })) .then( self -> { if (!self.isPending()) { @@ -371,19 +367,15 @@ public void shouldNotExpireNewlyResolvedValueIfBlockIsRacingWithInvalidate() { () -> Assertions.assertThat(self.block(null)) .matches((v) -> v.equals(valueToSend) || v.equals(valueToSend2)), - () -> - RaceTestUtils.race( - self::invalidate, - () -> { - for (; ; ) { - if (self.subscribers != ResolvingOperator.READY) { - self.complete(valueToSend2); - break; - } - } - }, - Schedulers.parallel()), - Schedulers.parallel())) + self::invalidate, + () -> { + for (; ; ) { + if (self.subscribers != ResolvingOperator.READY) { + self.complete(valueToSend2); + break; + } + } + })) .then( self -> { if (self.isPending()) { 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 b3f596a37..b09548245 100644 --- a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java @@ -217,6 +217,8 @@ void fromCustomRSocketException() { assertThat(Exceptions.from(0, byteBuf)) .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", randomCode, "test-message") .isInstanceOf(IllegalArgumentException.class); + + byteBuf.release(); } } 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 271c08664..5177a65be 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java @@ -28,13 +28,11 @@ import java.time.Duration; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.RepeatedTest; -import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import reactor.core.Fuseable; import reactor.core.publisher.Hooks; -import reactor.core.publisher.Operators; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; import reactor.test.util.RaceTestUtils; @@ -110,7 +108,7 @@ public void testPrioritizedSending(boolean fusedCase) { public void ensureUnboundedProcessorDisposesQueueProperly(boolean withFusionEnabled) { final LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); - for (int i = 0; i < 100000; i++) { + for (int i = 0; i < 10000; i++) { final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); final ByteBuf buffer1 = allocator.buffer(1); @@ -123,68 +121,247 @@ public void ensureUnboundedProcessorDisposesQueueProperly(boolean withFusionEnab unboundedProcessor.subscribe(assertSubscriber); RaceTestUtils.race( - () -> - RaceTestUtils.race( - () -> - RaceTestUtils.race( - () -> { - unboundedProcessor.onNext(buffer1); - unboundedProcessor.onNext(buffer2); - }, - unboundedProcessor::dispose, - Schedulers.elastic()), - assertSubscriber::cancel, - Schedulers.elastic()), + () -> { + unboundedProcessor.onNext(buffer1); + unboundedProcessor.onNext(buffer2); + }, + unboundedProcessor::dispose, + assertSubscriber::cancel, () -> { assertSubscriber.request(1); assertSubscriber.request(1); + }); + + assertSubscriber.values().forEach(ReferenceCountUtil::release); + + allocator.assertHasNoLeaks(); + } + } + + @ParameterizedTest( + name = + "Ensures that racing between onNext | dispose | cancel | request(n) | terminal will not cause any issues and leaks; mode[fusionEnabled={0}]") + @ValueSource(booleans = {true, false}) + public void smokeTest1(boolean withFusionEnabled) { + final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + final RuntimeException runtimeException = new RuntimeException("test"); + for (int i = 0; i < 10000; i++) { + final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); + + final ByteBuf buffer1 = allocator.buffer(1); + final ByteBuf buffer2 = allocator.buffer(2); + final ByteBuf buffer3 = allocator.buffer(3); + final ByteBuf buffer4 = allocator.buffer(4); + + final AssertSubscriber assertSubscriber = + new AssertSubscriber(0) + .requestedFusionMode(withFusionEnabled ? Fuseable.ANY : Fuseable.NONE); + + unboundedProcessor.subscribe(assertSubscriber); + + RaceTestUtils.race( + () -> { + unboundedProcessor.onNext(buffer1); + unboundedProcessor.onNextPrioritized(buffer2); + }, + () -> { + unboundedProcessor.onNextPrioritized(buffer3); + unboundedProcessor.onNext(buffer4); + }, + unboundedProcessor::dispose, + unboundedProcessor::onComplete, + () -> unboundedProcessor.onError(runtimeException), + assertSubscriber::cancel, + () -> { + assertSubscriber.request(1); + assertSubscriber.request(1); + assertSubscriber.request(1); + assertSubscriber.request(1); + }); + + assertSubscriber.values().forEach(ReferenceCountUtil::release); + + allocator.assertHasNoLeaks(); + } + } + + @ParameterizedTest( + name = + "Ensures that racing between onNext | dispose | subscribe | request(n) | terminal will not cause any issues and leaks; mode[fusionEnabled={0}]") + @ValueSource(booleans = {true, false}) + @Disabled("hard to support in 1.0.x") + public void smokeTest2(boolean withFusionEnabled) { + final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + final RuntimeException runtimeException = new RuntimeException("test"); + for (int i = 0; i < 10000; i++) { + final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); + + final ByteBuf buffer1 = allocator.buffer(1); + final ByteBuf buffer2 = allocator.buffer(2); + final ByteBuf buffer3 = allocator.buffer(3); + final ByteBuf buffer4 = allocator.buffer(4); + + final AssertSubscriber assertSubscriber = + new AssertSubscriber(0) + .requestedFusionMode(withFusionEnabled ? Fuseable.ANY : Fuseable.NONE); + + RaceTestUtils.race( + Schedulers.boundedElastic(), + () -> { + unboundedProcessor.onNext(buffer1); + unboundedProcessor.onNextPrioritized(buffer2); + }, + () -> { + unboundedProcessor.onNextPrioritized(buffer3); + unboundedProcessor.onNext(buffer4); }, - Schedulers.elastic()); + unboundedProcessor::dispose, + unboundedProcessor::onComplete, + () -> unboundedProcessor.onError(runtimeException), + () -> { + unboundedProcessor.subscribe(assertSubscriber); + assertSubscriber.request(1); + assertSubscriber.request(1); + assertSubscriber.request(1); + assertSubscriber.request(1); + }); - assertSubscriber.values().forEach(ReferenceCountUtil::safeRelease); + assertSubscriber.values().forEach(ReferenceCountUtil::release); allocator.assertHasNoLeaks(); } } - @RepeatedTest( + @ParameterizedTest( name = - "Ensures that racing between onNext + dispose | downstream async drain should not cause any issues and leaks", - value = 100000) - @Timeout(60) - public void ensuresAsyncFusionAndDisposureHasNoDeadlock() { - // TODO: enable leaks tracking - // final LeaksTrackingByteBufAllocator allocator = - // LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); - final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); - - // final ByteBuf buffer1 = allocator.buffer(1); - // final ByteBuf buffer2 = allocator.buffer(2); - - final AssertSubscriber assertSubscriber = - new AssertSubscriber<>(Operators.enableOnDiscard(null, ReferenceCountUtil::safeRelease)); - - unboundedProcessor.publishOn(Schedulers.parallel()).subscribe(assertSubscriber); - - RaceTestUtils.race( - () -> { - // unboundedProcessor.onNext(buffer1); - // unboundedProcessor.onNext(buffer2); - unboundedProcessor.onNext(Unpooled.EMPTY_BUFFER); - unboundedProcessor.onNext(Unpooled.EMPTY_BUFFER); - unboundedProcessor.onNext(Unpooled.EMPTY_BUFFER); - unboundedProcessor.onNext(Unpooled.EMPTY_BUFFER); - unboundedProcessor.onNext(Unpooled.EMPTY_BUFFER); - unboundedProcessor.onNext(Unpooled.EMPTY_BUFFER); - unboundedProcessor.dispose(); - }, - unboundedProcessor::dispose); - - assertSubscriber - .await(Duration.ofSeconds(50)) - .values() - .forEach(ReferenceCountUtil::safeRelease); - - // allocator.assertHasNoLeaks(); + "Ensures that racing between onNext | dispose | subscribe(cancelled) | terminal will not cause any issues and leaks; mode[fusionEnabled={0}]") + @ValueSource(booleans = {true, false}) + public void smokeTest3(boolean withFusionEnabled) { + final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + final RuntimeException runtimeException = new RuntimeException("test"); + for (int i = 0; i < 10000; i++) { + final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); + + final ByteBuf buffer1 = allocator.buffer(1); + final ByteBuf buffer2 = allocator.buffer(2); + final ByteBuf buffer3 = allocator.buffer(3); + final ByteBuf buffer4 = allocator.buffer(4); + + final AssertSubscriber assertSubscriber = + new AssertSubscriber(0) + .requestedFusionMode(withFusionEnabled ? Fuseable.ANY : Fuseable.NONE); + + assertSubscriber.cancel(); + + RaceTestUtils.race( + Schedulers.boundedElastic(), + () -> { + unboundedProcessor.onNext(buffer1); + unboundedProcessor.onNextPrioritized(buffer2); + }, + () -> { + unboundedProcessor.onNextPrioritized(buffer3); + unboundedProcessor.onNext(buffer4); + }, + unboundedProcessor::dispose, + unboundedProcessor::onComplete, + () -> unboundedProcessor.onError(runtimeException), + () -> unboundedProcessor.subscribe(assertSubscriber)); + + assertSubscriber.values().forEach(ReferenceCountUtil::release); + + allocator.assertHasNoLeaks(); + } + } + + @ParameterizedTest( + name = + "Ensures that racing between onNext | dispose | subscribe(cancelled) | terminal will not cause any issues and leaks; mode[fusionEnabled={0}]") + @ValueSource(booleans = {true, false}) + public void smokeTest31(boolean withFusionEnabled) { + final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + final RuntimeException runtimeException = new RuntimeException("test"); + for (int i = 0; i < 10000; i++) { + final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); + + final ByteBuf buffer1 = allocator.buffer(1); + final ByteBuf buffer2 = allocator.buffer(2); + final ByteBuf buffer3 = allocator.buffer(3); + final ByteBuf buffer4 = allocator.buffer(4); + + final AssertSubscriber assertSubscriber = + new AssertSubscriber(0) + .requestedFusionMode(withFusionEnabled ? Fuseable.ANY : Fuseable.NONE); + + RaceTestUtils.race( + Schedulers.boundedElastic(), + () -> { + unboundedProcessor.onNext(buffer1); + unboundedProcessor.onNextPrioritized(buffer2); + }, + () -> { + unboundedProcessor.onNextPrioritized(buffer3); + unboundedProcessor.onNext(buffer4); + }, + unboundedProcessor::dispose, + unboundedProcessor::onComplete, + () -> unboundedProcessor.onError(runtimeException), + () -> unboundedProcessor.subscribe(assertSubscriber), + () -> { + assertSubscriber.request(1); + assertSubscriber.request(1); + assertSubscriber.request(1); + assertSubscriber.request(1); + }, + assertSubscriber::cancel); + + assertSubscriber.values().forEach(ReferenceCountUtil::release); + allocator.assertHasNoLeaks(); + } + } + + @ParameterizedTest( + name = + "Ensures that racing between onNext + dispose | downstream async drain should not cause any issues and leaks; mode[fusionEnabled={0}]") + @ValueSource(booleans = {true, false}) + public void ensuresAsyncFusionAndDisposureHasNoDeadlock(boolean withFusionEnabled) { + final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + + for (int i = 0; i < 10000; i++) { + final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); + final ByteBuf buffer1 = allocator.buffer(1); + final ByteBuf buffer2 = allocator.buffer(2); + final ByteBuf buffer3 = allocator.buffer(3); + final ByteBuf buffer4 = allocator.buffer(4); + final ByteBuf buffer5 = allocator.buffer(5); + final ByteBuf buffer6 = allocator.buffer(6); + + final AssertSubscriber assertSubscriber = + new AssertSubscriber() + .requestedFusionMode(withFusionEnabled ? Fuseable.ANY : Fuseable.NONE); + + unboundedProcessor.subscribe(assertSubscriber); + + RaceTestUtils.race( + () -> { + unboundedProcessor.onNext(buffer1); + unboundedProcessor.onNext(buffer2); + unboundedProcessor.onNext(buffer3); + unboundedProcessor.onNext(buffer4); + unboundedProcessor.onNext(buffer5); + unboundedProcessor.onNext(buffer6); + unboundedProcessor.dispose(); + }, + unboundedProcessor::dispose); + + assertSubscriber.await(Duration.ofSeconds(50)).values().forEach(ReferenceCountUtil::release); + } + + allocator.assertHasNoLeaks(); } } 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 index 28206b4ff..54b99c797 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/subscriber/AssertSubscriber.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/subscriber/AssertSubscriber.java @@ -864,8 +864,11 @@ public void cancel() { if (a != null && a != Operators.cancelledSubscription()) { a.cancel(); - if (establishedFusionMode == Fuseable.ASYNC && WIP.getAndIncrement(this) == 0) { - qs.clear(); + if (establishedFusionMode == Fuseable.ASYNC) { + final int previousState = markWorkAdded(); + if (!isWorkInProgress(previousState)) { + clearAndFinalize(); + } } } } @@ -924,11 +927,54 @@ public void onNext(T t) { } } - void drain() { - if (this.wip != 0 || WIP.getAndIncrement(this) != 0) { - if (isCancelled()) { - qs.clear(); + static boolean isFinalized(int state) { + return state == Integer.MIN_VALUE; + } + + static boolean isWorkInProgress(int state) { + return state > 0; + } + + int markWorkAdded() { + for (; ; ) { + int state = this.wip; + + if (isFinalized(state)) { + return state; + } + + if ((state & Integer.MAX_VALUE) == Integer.MAX_VALUE) { + return state; + } + int nextState = state + 1; + + if (WIP.compareAndSet(this, state, nextState)) { + return state; + } + } + } + + void clearAndFinalize() { + final Fuseable.QueueSubscription qs = this.qs; + for (; ; ) { + int state = this.wip; + + qs.clear(); + + if (WIP.compareAndSet(this, state, Integer.MIN_VALUE)) { + return; } + } + } + + void drain() { + final int previousState = markWorkAdded(); + if (isWorkInProgress(previousState)) { + return; + } + + if (isFinalized(previousState)) { + qs.clear(); return; } @@ -936,14 +982,14 @@ void drain() { int m = 1; for (; ; ) { if (isCancelled()) { - qs.clear(); + clearAndFinalize(); break; } boolean done = this.done; t = qs.poll(); if (t == null) { if (done) { - qs.clear(); // clear upstream to terminated it due to the contract + clearAndFinalize(); cdl.countDown(); return; } @@ -973,39 +1019,41 @@ 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; + if (s instanceof Fuseable.QueueSubscription) { + this.qs = (Fuseable.QueueSubscription) s; - int m = qs.requestFusion(requestMode); - establishedFusionMode = m; + int m = qs.requestFusion(requestMode); + establishedFusionMode = m; - if (m == Fuseable.SYNC) { - for (; ; ) { - T v = qs.poll(); - if (v == null) { - onComplete(); - break; - } + if (!setWithoutRequesting(s)) { + qs.clear(); + if (!isCancelled()) { + errors.add(new IllegalStateException("Subscription already set: " + subscriptionCount)); + } + return; + } - onNext(v); + if (m == Fuseable.SYNC) { + for (; ; ) { + T v = qs.poll(); + if (v == null) { + onComplete(); + break; } - } else { - requestDeferred(); + + onNext(v); } } else { requestDeferred(); } + + return; } - } else { - if (!set(s)) { - if (!isCancelled()) { - errors.add(new IllegalStateException("Subscription already set: " + subscriptionCount)); - } + } + + if (!set(s)) { + if (!isCancelled()) { + errors.add(new IllegalStateException("Subscription already set: " + subscriptionCount)); } } } From 52a60e94df8d08ac3e7849329d35958cedcc7e78 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 19 May 2021 00:01:29 +0300 Subject: [PATCH 031/116] migrates from travis Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .github/workflows/gradle-all.yml | 45 +++++++++++++++++++++++ .github/workflows/gradle-main.yml | 53 ++++++++++++++++++++++++++++ .github/workflows/gradle-pr.yml | 31 ++++++++++++++++ .github/workflows/gradle-release.yml | 44 +++++++++++++++++++++++ .travis.yml | 45 ----------------------- ci/travis.sh | 44 ----------------------- 6 files changed, 173 insertions(+), 89 deletions(-) create mode 100644 .github/workflows/gradle-all.yml create mode 100644 .github/workflows/gradle-main.yml create mode 100644 .github/workflows/gradle-pr.yml create mode 100644 .github/workflows/gradle-release.yml delete mode 100644 .travis.yml delete mode 100755 ci/travis.sh diff --git a/.github/workflows/gradle-all.yml b/.github/workflows/gradle-all.yml new file mode 100644 index 000000000..8540539bb --- /dev/null +++ b/.github/workflows/gradle-all.yml @@ -0,0 +1,45 @@ +name: Branches Java CI + +on: + # Trigger the workflow on push + # but only for the non master/1.0.x branches + push: + branches-ignore: + - 1.0.x + - master + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 14 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build + - name: Publish Packages to Artifactory + if: ${{ matrix.jdk == '1.8' }} + run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PversionSuffix="-${githubRef#refs/heads/}-SNAPSHOT" -PbuildNumber="${buildNumber}" artifactoryPublish --stacktrace + env: + bintrayUser: ${{ secrets.bintrayUser }} + bintrayKey: ${{ secrets.bintrayKey }} + githubRef: ${{ github.ref }} + buildNumber: ${{ github.run_number }} \ No newline at end of file diff --git a/.github/workflows/gradle-main.yml b/.github/workflows/gradle-main.yml new file mode 100644 index 000000000..d8ba3c3d5 --- /dev/null +++ b/.github/workflows/gradle-main.yml @@ -0,0 +1,53 @@ +name: Main Branches Java CI + +on: + # Trigger the workflow on push + # but only for the master/1.0.x branch + push: + branches: + - master + - 1.0.x + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 14 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build + - name: Publish Packages to Artifactory + if: ${{ matrix.jdk == '1.8' }} + run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PversionSuffix="-SNAPSHOT" -PbuildNumber="${buildNumber}" artifactoryPublish --stacktrace + env: + bintrayUser: ${{ secrets.bintrayUser }} + bintrayKey: ${{ secrets.bintrayKey }} + buildNumber: ${{ github.run_number }} + - name: Aggregate test reports with ciMate + if: always() + continue-on-error: true + env: + CIMATE_PROJECT_ID: m84qx17y + run: | + wget -q https://get.cimate.io/release/linux/cimate + chmod +x cimate + ./cimate "**/TEST-*.xml" \ No newline at end of file diff --git a/.github/workflows/gradle-pr.yml b/.github/workflows/gradle-pr.yml new file mode 100644 index 000000000..994450faf --- /dev/null +++ b/.github/workflows/gradle-pr.yml @@ -0,0 +1,31 @@ +name: Pull Request Java CI + +on: [pull_request] + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 14 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build \ No newline at end of file diff --git a/.github/workflows/gradle-release.yml b/.github/workflows/gradle-release.yml new file mode 100644 index 000000000..08f2698dc --- /dev/null +++ b/.github/workflows/gradle-release.yml @@ -0,0 +1,44 @@ +name: Release Java CI + +on: + # Trigger the workflow on push + push: + # Sequence of patterns matched against refs/tags + tags: + - '*' # Push events to matching *, i.e. 1.0, 20.15.10 + +jobs: + publish: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build + - name: Publish Packages to Bintray + run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" -Pversion="${githubRef#refs/tags/}" -PbuildNumber="${buildNumber}" bintrayUpload + env: + bintrayUser: ${{ secrets.bintrayUser }} + bintrayKey: ${{ secrets.bintrayKey }} + sonatypeUsername: ${{ secrets.sonatypeUsername }} + sonatypePassword: ${{ secrets.sonatypePassword }} + githubRef: ${{ github.ref }} + buildNumber: ${{ github.run_number }} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4722957c8..000000000 --- a/.travis.yml +++ /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. -# ---- -language: java - -dist: trusty - -matrix: - include: - - jdk: openjdk8 - - jdk: openjdk11 - env: SKIP_RELEASE=true - - jdk: openjdk14 - env: SKIP_RELEASE=true - -env: - global: - - secure: "WBCy0hsF96Xybj4n0AUrGY2m5FWCUa30XR+aVElSOO8d7v7BMypAT8mAd+yC2Y+j8WUGpIv59CqgeK1JrYdR9b3qRKhJmoE1Q92TotrxXMTIC9OKuU51LaaOqGYqx4SqiA2AyaikTFPd8um7KZfUpW/dG4IXySsiJ2OKT1jMUq6TmbWHnAYtjbl3u3WdjBQTIZNMtqG1+H1vIpsWyZrvbB4TWlNzhKBAu/YnlzMtvStrDaF7XrCJ2BQdMomQO18NH2gWxUEvLbQb6ip3wFl9CRe6vID7K1dmFwm08RPt9hRPC9yDahlIy8VvuNcWrP42TV+BVYy8V/hfaIo1pPsDBrtmVyc7YZjXSUM68orDFOkRB35qGkNIaAhy5Yt6G9QfwLXJkDFofW5KMKtDFUzf+j4DwS0CiDMF4k6Qq7YN1tYFXE9R8xa6Gv+wTNHqs4RURbYMS9IlbkhKxNbtyuema2sIUbsIfDezIzLI5BnfH2uli7O6/z0/G0Vfmf6A4q5Olm+7uhzMTI0GKheUIKr16SOxABlrwJtLJftzoKz9hYd3b7C9t61vYzccC3rWYobplwIcK2w50gFHQS8HLeiCjo8yjCx+IRSvAGaZIBPQdHCktrEYCVDUTXOxdaD6k6Ef+ppm8Nn+M+iC8x/G1wYE4x1lDqHw3GfhKsEQmiHL/98=" - - secure: "mbB+rv9eWUFQ9/yr2REH2ztH6r/Uq7cq/OJ5WK6yFp0TmPzlJ8jbEVwe/sdAMW2E4qrfMu1c2h3qsVm41pNx0MwEsIW/lTIZRiRmNYon32n+SHlRWyTn8dJeY/p1HoHs450OjLgB4X4jmRmfSt8IQ/w9ZCjF6HVcgR4ctt+myECTNcRidEIOahljnSJmnFFDsKbt2UJN96AfvvhbxcarEKgKLXLd9tQT2GlvEOM+hVOY9hKD5FvIoRp9heyCEAsSBXe+MIWQlh4jx+B4zCajZJ+8KN6M8KIt40lV8z4Zbc11jgq/xULJwkQIuVZvkJ3huIfUrxwLPgYWeai/TR/m3+2jy1hFajt96pnhJzFEz0IBL0wFALwAY1n2R/6uugEUYnDsFcGQGTsO5OeeOixiRPH5HNgfOhInqJoFh/887f+gq7OLXjlRCTsw+S9KknZ3iBpHX/+khurfAUC9khiMvufEq6Wyu0TvxhmGERFrs7uugeJ1VA85SDVQ6Au9MV831PeBGqzHpYG7w2kJj1EiFjBRUhCthxyDfX2b04egozlKF8JEifZ9EVj7pNMQUvVG2c9Wj6M0fG84NusnlZlA16XxAmfLevc9b/BOSSrqc2r9Z1ZvxFnBPP9H94Uqt9ZninhW/T49jRF+lQzD45MTVogzVk77XtdpzUemf4t5mHc=" - - secure: "GcPu3U4o2Dp7QLCqaAo3mGMJTl9yd+w+elXqqt7WDjrjm5p8mrzvQfyiJA7mRJVDTGpgib8fLctL1X1+QOX4fNKElrDUFhE3bWAqwVwHGPK4D3HCb6THD5XVqE4qcPmdLWPkvJ9ZY5nSIfuRVASjZTcc4XSXISK2jUSGar0PNYlo62/OFGvNvMz/qINU9RU7iYdDlL19yd72TKDfuK0UOKhQEGypamEHam3SMNCw/p8Q5K1vQe+Oba3ILCvYHJvqWc2NLjRXJjXfIaOq/NpCK6Lx2U9etdpkb5lyW5Cx1lkzIcRUq8ZUCwbkHog9LJoZGrZFh5AzlZ6kRuejBqu7AISmZy4s9HVAb7AQmNxvXkK9EIt8lavcaHnLYUIfuxvBqK/ptcUN5P/KXCs1DsbpADjB7YbUu/EQ2OAWncV31Z+O4uMHV29eGTtaz9LoK28+mHRfFHqoazWyuUejor6iSSkrCeqsLEvU8o6rH4oenKz7hLlZsJqHGACYtYNYi2CXYlTu0bMX+Hb1EtTu6Awm9Gn04TqVdmNexgF5CdqW4A696i6jlkPpVCt4B4nq4VPs2RMTkjVl3B7uOkDm18u35dncuhgsnMfVmo9cWX5COeyefdh6kdnKsUf0+IPbV/hix/OCP72dpuhxgcyzN+DvaVLzX7YOx7TpJTzPSKNEQZc=" - - secure: "UFJEzDEv6H2Qscg9UgZFVJq5oFvq7nQkVoSuGfh5Y4ZhL9PCK5f3Ft9oYEZOQwXaxWD1qivtJjQV3DdBiqsHkrnPrJ0hi3iYVDJo26xLNtu3welFw5Veqmgu2NuwjaDn6cjRFCJRLzpszMUWO1DvfLJTs3LuJDuXEyAKDw9eQgfOakqO4xeloyXgM7xnoXz11rgqtJNU6snjVPHftXNPTHGsNDlTR7SAIbjYwLMbdIKM2qjzrXkg+a94QOz2stnTDz9V5iYNH+3XXCcYxD9nb1Ol1XGWvtDnNGEhtGmylLdjHXwGLHiW2HOXskLzSkm7ASie1WdyHVHZb4X8LjxCy62S0FPevBgat1a443Khx5HCMYR/8dQrlOI82GYTr8n9U6QQE4Li8XLw64DVP9HGs9jdbsfEdlIsiPWqB6ujlwiO6pyfmQGQCgjALA+oD87uDQLcgh+SDYgE0ZwmwGzbjeynZpoCrEE8A1GHhSwkM9khx6EJFacm9XzqoUGK0wB1f8su+51fqPglF1zye80IFA4wOMMAY+KUc9du/vQ98f0lfjsNSOC02CKYxbA5RaakQMAYjirsZraA57xLmCSIGMhhW4wClQdJBww6LLz463yZU4WPwyqU+ZW12aV5dVLb5RWXIbZKmdT74DfZajHvqgTYpb05L5cJl7ApMspUkKk=" - -script: ci/travis.sh - -before_cache: -- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock -- rm -fr $HOME/.gradle/caches/*/plugin-resolution/ - -cache: - directories: - - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ diff --git a/ci/travis.sh b/ci/travis.sh deleted file mode 100755 index df3fc1245..000000000 --- a/ci/travis.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash - -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" != "" ] && [ "$TRAVIS_BRANCH" == "1.0.x" ] ; then - - 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 - - echo -e "Building Tag $TRAVIS_REPO_SLUG/$TRAVIS_TAG" - ./gradlew \ - -Pversion="$TRAVIS_TAG" \ - -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" \ - -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" \ - -PbuildNumber="$TRAVIS_BUILD_NUMBER" \ - build bintrayUpload --stacktrace - -else - - echo -e "Building $TRAVIS_REPO_SLUG/$TRAVIS_BRANCH" - ./gradlew build - -fi - From a2a35407b76a1c1d0ce35a1e618c8a677182a20d Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 19 May 2021 09:36:44 +0300 Subject: [PATCH 032/116] polishes tests Signed-off-by: Oleh Dokuka --- .../io/rsocket/core/ReconnectMonoTests.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java index ad3013f8e..88ac062d1 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java @@ -314,19 +314,14 @@ public String get() { Assertions.assertThat(expired).isEmpty(); - try { - - RaceTestUtils.race( - () -> - Assertions.assertThat(reconnectMono.block()) - .matches( - (v) -> - v.equals("value_to_not_expire" + index) - || v.equals("value_to_expire" + index)), - reconnectMono::invalidate); - } catch (Throwable t) { - t.printStackTrace(); - } + RaceTestUtils.race( + () -> + Assertions.assertThat(reconnectMono.block()) + .matches( + (v) -> + v.equals("value_to_not_expire" + index) + || v.equals("value_to_expire" + index)), + reconnectMono::invalidate); subscriber.assertTerminated(); From 72b4dbfe4b0969da6b5b388edb5ea87d412cbb22 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 24 May 2021 11:54:43 +0100 Subject: [PATCH 033/116] Upgrade to Dysprosium-SR20 Closes gh-1006 Signed-off-by: Rossen Stoyanchev --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 64e7401df..f8083a4ea 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = 'Dysprosium-BUILD-SNAPSHOT' + ext['reactor-bom.version'] = 'Dysprosium-SR20' ext['logback.version'] = '1.2.3' ext['netty-bom.version'] = '4.1.59.Final' ext['netty-boringssl.version'] = '2.0.36.Final' From a1a2579f51e7a4651dd4a38a83eacc919199e7fe Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 27 Mar 2021 23:57:25 +0200 Subject: [PATCH 034/116] adds jcstress support Signed-off-by: Oleh Dokuka --- .github/workflows/gradle-all.yml | 111 ++++++++++- .github/workflows/gradle-main.yml | 111 ++++++++++- .github/workflows/gradle-pr.yml | 84 +++++++- build.gradle | 7 +- rsocket-core/build.gradle | 8 + .../io/rsocket/core/StressSubscriber.java | 188 ++++++++++++++++++ .../io/rsocket/core/StressSubscription.java | 64 ++++++ .../src/jcstress/resources/logback.xml | 39 ++++ 8 files changed, 601 insertions(+), 11 deletions(-) create mode 100644 rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscriber.java create mode 100644 rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscription.java create mode 100644 rsocket-core/src/jcstress/resources/logback.xml diff --git a/.github/workflows/gradle-all.yml b/.github/workflows/gradle-all.yml index 8540539bb..ca9bd35e8 100644 --- a/.github/workflows/gradle-all.yml +++ b/.github/workflows/gradle-all.yml @@ -10,13 +10,66 @@ on: jobs: build: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 16 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build -x test --no-daemon + + coretest: + needs: [build] + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 16 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew rsocket-core:test --no-daemon + othertest: + needs: [build] runs-on: ${{ matrix.os }} strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 14 ] + jdk: [ 1.8, 11, 16 ] fail-fast: false steps: @@ -34,10 +87,62 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew clean build + run: ./gradlew test -x :rsocket-core:test --no-daemon + + jcstress: + needs: [build] + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 16 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew jcstress --no-daemon + + publish: + needs: [ build, coretest, othertest, jcstress ] + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 16 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew - name: Publish Packages to Artifactory if: ${{ matrix.jdk == '1.8' }} - run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PversionSuffix="-${githubRef#refs/heads/}-SNAPSHOT" -PbuildNumber="${buildNumber}" artifactoryPublish --stacktrace + run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PversionSuffix="-${githubRef#refs/heads/}-SNAPSHOT" -PbuildNumber="${buildNumber}" artifactoryPublish --no-daemon --stacktrace env: bintrayUser: ${{ secrets.bintrayUser }} bintrayKey: ${{ secrets.bintrayKey }} diff --git a/.github/workflows/gradle-main.yml b/.github/workflows/gradle-main.yml index d8ba3c3d5..c8c1f1f95 100644 --- a/.github/workflows/gradle-main.yml +++ b/.github/workflows/gradle-main.yml @@ -10,13 +10,66 @@ on: jobs: build: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 16 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build -x test --no-daemon + + coretest: + needs: [build] + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 16 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew rsocket-core:test --no-daemon + othertest: + needs: [build] runs-on: ${{ matrix.os }} strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 14 ] + jdk: [ 1.8, 11, 16 ] fail-fast: false steps: @@ -34,10 +87,62 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew clean build + run: ./gradlew test -x :rsocket-core:test --no-daemon + + jcstress: + needs: [build] + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 16 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew jcstress --no-daemon + + publish: + needs: [ build, coretest, othertest, jcstress ] + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 16 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew - name: Publish Packages to Artifactory if: ${{ matrix.jdk == '1.8' }} - run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PversionSuffix="-SNAPSHOT" -PbuildNumber="${buildNumber}" artifactoryPublish --stacktrace + run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PversionSuffix="-SNAPSHOT" -PbuildNumber="${buildNumber}" artifactoryPublish --no-daemon --stacktrace env: bintrayUser: ${{ secrets.bintrayUser }} bintrayKey: ${{ secrets.bintrayKey }} diff --git a/.github/workflows/gradle-pr.yml b/.github/workflows/gradle-pr.yml index 994450faf..fd88ad76f 100644 --- a/.github/workflows/gradle-pr.yml +++ b/.github/workflows/gradle-pr.yml @@ -4,13 +4,93 @@ on: [pull_request] jobs: build: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 16 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build -x test --no-daemon + + coretest: + needs: [build] + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 16 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew rsocket-core:test --no-daemon + + othertest: + needs: [build] + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 16 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew test -x :rsocket-core:test --no-daemon + jcstress: + needs: [build] runs-on: ${{ matrix.os }} strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 14 ] + jdk: [ 1.8, 11, 16 ] fail-fast: false steps: @@ -28,4 +108,4 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew clean build \ No newline at end of file + run: ./gradlew jcstress --no-daemon \ No newline at end of file diff --git a/build.gradle b/build.gradle index 2101a3255..170da1f0c 100644 --- a/build.gradle +++ b/build.gradle @@ -15,12 +15,13 @@ */ plugins { - id 'com.github.sherter.google-java-format' version '0.8' apply false + id 'com.github.sherter.google-java-format' version '0.9' apply false id 'com.jfrog.artifactory' version '4.15.2' apply false id 'com.jfrog.bintray' version '1.8.5' apply false - id 'me.champeau.gradle.jmh' version '0.5.0' apply false - id 'io.spring.dependency-management' version '1.0.9.RELEASE' apply false + id 'me.champeau.gradle.jmh' version '0.5.3' apply false + id 'io.spring.dependency-management' version '1.0.11.RELEASE' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false + id "io.github.reyerizo.gradle.jcstress" version "0.8.11" apply false } boolean isCiServer = ["CI", "CONTINUOUS_INTEGRATION", "TRAVIS", "CIRCLECI", "bamboo_planKey", "GITHUB_ACTION"].with { diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index 5d33c2b5f..2b5fcd095 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -21,6 +21,7 @@ plugins { id 'com.jfrog.bintray' id 'io.morethan.jmhreport' id 'me.champeau.gradle.jmh' + id 'io.github.reyerizo.gradle.jcstress' } dependencies { @@ -43,6 +44,13 @@ dependencies { testCompileOnly 'junit:junit' testImplementation 'org.hamcrest:hamcrest-library' testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' + + jcstressImplementation "ch.qos.logback:logback-classic" +} + +jcstress { + mode = 'default' //quick, default, tough + jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.7" } jar { diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscriber.java b/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscriber.java new file mode 100644 index 000000000..1b7c050bc --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscriber.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2020-Present 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.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.Consumer; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Operators; +import reactor.util.context.Context; + +public class StressSubscriber implements CoreSubscriber { + + enum Operation { + ON_NEXT, + ON_ERROR, + ON_COMPLETE, + ON_SUBSCRIBE + } + + final long initRequest; + + final Context context; + + volatile Subscription subscription; + + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater S = + AtomicReferenceFieldUpdater.newUpdater( + StressSubscriber.class, Subscription.class, "subscription"); + + public Throwable error; + + public List droppedErrors = new CopyOnWriteArrayList<>(); + + public List values = new ArrayList<>(); + + public volatile Operation guard; + + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater GUARD = + AtomicReferenceFieldUpdater.newUpdater(StressSubscriber.class, Operation.class, "guard"); + + public volatile boolean concurrentOnNext; + + public volatile boolean concurrentOnError; + + public volatile boolean concurrentOnComplete; + + public volatile boolean concurrentOnSubscribe; + + public volatile int onNextCalls; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater ON_NEXT_CALLS = + AtomicIntegerFieldUpdater.newUpdater(StressSubscriber.class, "onNextCalls"); + + public volatile int onNextDiscarded; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater ON_NEXT_DISCARDED = + AtomicIntegerFieldUpdater.newUpdater(StressSubscriber.class, "onNextDiscarded"); + + public volatile int onErrorCalls; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater ON_ERROR_CALLS = + AtomicIntegerFieldUpdater.newUpdater(StressSubscriber.class, "onErrorCalls"); + + public volatile int onCompleteCalls; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater ON_COMPLETE_CALLS = + AtomicIntegerFieldUpdater.newUpdater(StressSubscriber.class, "onCompleteCalls"); + + public volatile int onSubscribeCalls; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater ON_SUBSCRIBE_CALLS = + AtomicIntegerFieldUpdater.newUpdater(StressSubscriber.class, "onSubscribeCalls"); + + /** Build a {@link StressSubscriber} that makes an unbounded request upon subscription. */ + public StressSubscriber() { + this(Long.MAX_VALUE); + } + + /** + * Build a {@link StressSubscriber} that requests the provided amount in {@link + * #onSubscribe(Subscription)}. Use {@code 0} to avoid any initial request upon subscription. + * + * @param initRequest the requested amount upon subscription, or zero to disable initial request + */ + public StressSubscriber(long initRequest) { + this.initRequest = initRequest; + this.context = + Operators.enableOnDiscard( + Context.of( + "reactor.onErrorDropped.local", + (Consumer) + throwable -> { + droppedErrors.add(throwable); + }), + (__) -> ON_NEXT_DISCARDED.incrementAndGet(this)); + } + + @Override + public Context currentContext() { + return this.context; + } + + @Override + public void onSubscribe(Subscription subscription) { + if (!GUARD.compareAndSet(this, null, Operation.ON_SUBSCRIBE)) { + concurrentOnSubscribe = true; + } else { + boolean wasSet = Operators.setOnce(S, this, subscription); + GUARD.compareAndSet(this, Operation.ON_SUBSCRIBE, null); + if (wasSet) { + if (initRequest > 0) { + subscription.request(initRequest); + } + } + } + ON_SUBSCRIBE_CALLS.incrementAndGet(this); + } + + @Override + public void onNext(T value) { + if (!GUARD.compareAndSet(this, null, Operation.ON_NEXT)) { + concurrentOnNext = true; + } else { + values.add(value); + GUARD.compareAndSet(this, Operation.ON_NEXT, null); + } + ON_NEXT_CALLS.incrementAndGet(this); + } + + @Override + public void onError(Throwable throwable) { + if (!GUARD.compareAndSet(this, null, Operation.ON_ERROR)) { + concurrentOnError = true; + } else { + GUARD.compareAndSet(this, Operation.ON_ERROR, null); + } + error = throwable; + ON_ERROR_CALLS.incrementAndGet(this); + } + + @Override + public void onComplete() { + if (!GUARD.compareAndSet(this, null, Operation.ON_COMPLETE)) { + concurrentOnComplete = true; + } else { + GUARD.compareAndSet(this, Operation.ON_COMPLETE, null); + } + ON_COMPLETE_CALLS.incrementAndGet(this); + } + + public void request(long n) { + if (Operators.validate(n)) { + Subscription s = this.subscription; + if (s != null) { + s.request(n); + } + } + } + + public void cancel() { + Operators.terminate(S, this); + } +} diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscription.java b/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscription.java new file mode 100644 index 000000000..583ba7ad2 --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscription.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020-Present 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.core; + +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Operators; + +public class StressSubscription implements Subscription { + + CoreSubscriber actual; + + public volatile int subscribes; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater SUBSCRIBES = + AtomicIntegerFieldUpdater.newUpdater(StressSubscription.class, "subscribes"); + + public volatile long requested; + + @SuppressWarnings("rawtypes") + static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(StressSubscription.class, "requested"); + + public volatile int requestsCount; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater REQUESTS_COUNT = + AtomicIntegerFieldUpdater.newUpdater(StressSubscription.class, "requestsCount"); + + public volatile boolean cancelled; + + void subscribe(CoreSubscriber actual) { + this.actual = actual; + actual.onSubscribe(this); + SUBSCRIBES.getAndIncrement(this); + } + + @Override + public void request(long n) { + REQUESTS_COUNT.incrementAndGet(this); + Operators.addCap(REQUESTED, this, n); + } + + @Override + public void cancel() { + cancelled = true; + } +} diff --git a/rsocket-core/src/jcstress/resources/logback.xml b/rsocket-core/src/jcstress/resources/logback.xml new file mode 100644 index 000000000..e5877552c --- /dev/null +++ b/rsocket-core/src/jcstress/resources/logback.xml @@ -0,0 +1,39 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + \ No newline at end of file From 88d2ac674a9869a43df772e3448b22c3cbe2fb5b Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 17 May 2021 01:36:52 +0300 Subject: [PATCH 035/116] updates gradle and libs versions Signed-off-by: Oleh Dokuka --- build.gradle | 24 ++++++++++-------- gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 5 ++-- gradlew | 31 ++++++++++------------- gradlew.bat | 25 +++++------------- rsocket-core/build.gradle | 2 +- rsocket-transport-netty/build.gradle | 1 + 7 files changed, 38 insertions(+), 50 deletions(-) diff --git a/build.gradle b/build.gradle index 170da1f0c..83b20bb50 100644 --- a/build.gradle +++ b/build.gradle @@ -16,9 +16,9 @@ plugins { id 'com.github.sherter.google-java-format' version '0.9' apply false - id 'com.jfrog.artifactory' version '4.15.2' apply false + id 'com.jfrog.artifactory' version '4.21.0' apply false id 'com.jfrog.bintray' version '1.8.5' apply false - id 'me.champeau.gradle.jmh' version '0.5.3' apply false + id 'me.champeau.jmh' version '0.6.4' apply false id 'io.spring.dependency-management' version '1.0.11.RELEASE' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false id "io.github.reyerizo.gradle.jcstress" version "0.8.11" apply false @@ -35,17 +35,18 @@ subprojects { ext['reactor-bom.version'] = '2020.0.7' ext['logback.version'] = '1.2.3' - ext['netty-bom.version'] = '4.1.59.Final' - ext['netty-boringssl.version'] = '2.0.36.Final' - ext['hdrhistogram.version'] = '2.1.10' - ext['mockito.version'] = '3.2.0' - ext['slf4j.version'] = '1.7.25' - ext['jmh.version'] = '1.21' - ext['junit.version'] = '5.5.2' + ext['netty-bom.version'] = '4.1.64.Final' + ext['netty-boringssl.version'] = '2.0.39.Final' + ext['hdrhistogram.version'] = '2.1.12' + ext['mockito.version'] = '3.10.0' + ext['slf4j.version'] = '1.7.30' + ext['jmh.version'] = '1.31' + ext['junit.version'] = '5.7.2' ext['hamcrest.version'] = '1.3' - ext['micrometer.version'] = '1.0.6' - ext['assertj.version'] = '3.11.1' + ext['micrometer.version'] = '1.6.7' + ext['assertj.version'] = '3.19.0' ext['netflix.limits.version'] = '0.3.6' + ext['bouncycastle-bcpkix.version'] = '1.68' group = "io.rsocket" @@ -74,6 +75,7 @@ subprojects { dependency "com.netflix.concurrency-limits:concurrency-limits-core:${ext['netflix.limits.version']}" dependency "ch.qos.logback:logback-classic:${ext['logback.version']}" dependency "io.netty:netty-tcnative-boringssl-static:${ext['netty-boringssl.version']}" + dependency "org.bouncycastle:bcpkix-jdk15on:${ext['bouncycastle-bcpkix.version']}" dependency "io.micrometer:micrometer-core:${ext['micrometer.version']}" dependency "org.assertj:assertj-core:${ext['assertj.version']}" dependency "org.hdrhistogram:HdrHistogram:${ext['hdrhistogram.version']}" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch delta 23334 zcmZ6yQ*_^7)b$%Swr#tyZQHhuU-WHk+qUgAc4J!&nxrusy#I5a=UlvJjD59l*Pe6C zy*_IVG(!&0LN+phBc)L-m3M)If#E@dfw80{QedYjfnx%cY|Q2krta=>YST_jBA9|p zot|vvp%0RvR1srYTl+z-NNCL@5oSg;&!BaMOR}sfJn192cT55<(x!dL7ut~~3^-Ur z4>ora_t}-M=h->qJpjxnx)1EWvn8?z{O>`3f+7iuKL<2+zHP~ldyrmD0P{Z4X%%`W zo_)z~Yy==^IcLFQUXFGeH8WebVkw~L>r{vkbd$z5MQq(ni#a^*>hw=_Z;C^Gfrdev z!mgg_pG zeMQUU+?X~Em$z2qQyLw%`*oeVS_0m|fcm)7q6xUbNU;Eku2#8)2t3}hj!-y+-89iQ z3fZ2srkJN7rV0vd0?Or&O+;oeJrGw6+{`LpB@d3*VpO>Un|q3BNDJspjozc(4hJDz zwgOl$df!`k*;k(~&;GPfVBAD3Hi3C}ZFV~#*$f>4hj%YsCq6tRQfp_Dt-)S_Uj!o= ze~fwe`&6h3{1?2yCfi zXybknxod^Z|~hQkrhOl74q z$G@Js5lv&IFx8Sm%&;&R^ZS012w;u(#-d_d7z}E<_L7JxsnmzL7!JXpt9>W$Br_-E zrt)8pGV-SsMKD!epNc6VMP@dY9SZ~}4KEJ0{AM}D(Ur&6>Xwy(7hK_??ybcBfV^H zx_aQ9cAG-(o3ZK6^5ob$c;XQ+WUNPojJo*4bQPb@#nF;E%h&FNJuVpSRK{}ljl}!b z#w$tS(t%=z)Q_2_4&C(JNz3Z&rgJG<@$5eR{6=#eNx!WXg2rrliM1=mC{vw4N32Vt z(hz+({@Wh2Y$x_R-d{$2XdqlCZW<@Yvix3|nho{g3fcY`x3r&v zC3T%<=pJrdP1&am@lIKma2=I=^4+>BZP8iAC+!5rKrxkP-K0t^lPkRKzej86htd0P z#d#*bI0LJ?=)BWl*(f{h=~UK26R;3?r6Z!LAuS$vtfd9{cVHb61Hh{>!#phiJ%Th9 zF?=-pJ;B(60kgq8M!6s_=E5q^V1BZqUk45QP(0*!5vKTDdWw8Z2W(yF7Cd4q6#8Au zDKAwS7y&OlW39}KP7u;mRY_qmKm6ZlbFdopRZRb2WvuPtfGOrS@2QJ&4I=v~NILZ5 zeRhAPI(ofewJkMGXux=19@_Z8{!gjzB73;zNpU}X|DXwxK^;Cvj0Ph3u|D+PK~V7Z z?T_+HtO$qw$Y7Eiis5+%de#S_2Eg{NT?gs+rEQ*+9;JM`;i65mGIf65%GmAWA1&vF zlc?PlDec;zALdLmib;DC&8{{TV>uUmnkgCuNg83d=~K)66oA^Xl2_g3joQ7h45dDe zhrM9pl;y7z>d~B9=jQH;Q=2Fr{5!6n4(@U2+i4B!LnEVpkskhl8Y&h?h2<}2MvUa(Z=c-L0$s#VLm_n6MN={uuQNF?aO%NJt-w^*Q^v38n zSik;)49a!p_y;?PBm+2+r&6d%&w5wFcSS3i(Q0})76N`VU$9#xpY*=PpEvRJL*_v? zq`fJn6uibh+U?Oh=7TngAZ+QgfVq{*FP4XT@%T4DJXQ3^Q%|A#S*bgV=uQOkLs3B> zPb@_|qGW^GJGUz;Rdk=&!X5<@+IA_92osMhzl2w&pZpOkH2wg6{QNKJ_SprLV)J7~ zswn~v{%5cFd4Dchvot~B4Q=>*(PzriPyl!KvQ;DQT4Jwc7b z@=RK6_wy*9Ls}eOd#i_ifu-1gyG1I4B$wrf0s~uz`Oi=PUk3$X;9w*ytxP=~JW?)j ziGecB9d!at%>E`;fCYBIE`?LXQ%q2#KyT1)F3gKTVQ(^OFF_%e>U9C|Jftsp-L z-uBgv--?x$jQ!7JVOO%A6s_NIULK3t`AUvLNRGy1+2c=*hNLTgEU{(f`aS3R&0c#8 zJ)H~+lk7p>Antxg8%KDw8HA(zRyL7IsRXPZq(&|IG=anACS|u!&ze?(596{Wa^56I z(Hh0)W(B=vPMB&$-+voJG+fh`2n6^ zE<#-hLF2)fS!S>(AgaU7)DA<}B0gb;cUhr}#B$zitS3?I zQ2dfsjc&|!;>ZmeP`tUDacf0iky2%{sdnvR10i;nHt{`{s%AE_Ck=O!`CgKV{TxZt zvGG&6h(`32V2E)jIe5jAb7h61MnLCplX!amDU*7b478F^m0qqf96LN3N^S2xtX@WV zqjdFPUpJ(hHl4?SW`Rxi^WJaHe&^dS6OY9@unu!n*p3<-W-CQ>pb^E?XzN3;LFQ%}E-2`SgWHo)7f-p+JMy`RG3E&3PwN54o9wVP*Nq{9PKSNP@R_eO zKB~SbZXrKS%qqUV1h!p7JvFb&fbotnqw2Q5-wA7wlEq4H?+^~Js$F8pms&<$wDQtJ zl0cD0WH*i-3Lza6dDXZ-#eh8JlXkv(BGQT%ufa%jHyi2P_PS;2Q-5b!JPW(HoNzYg z2(g^gwcm)p-Q2=kK{=bNP4d6yB|A(BM{w}7e~-*Rt}#Z0uO{Xa=nY%!B|uW5EG{vg zbLt&cVKr)8e;2Fjx3r;i#5>@hs!6e6@JKF5xyGp+&#)QM4t?M}2m%79NOpKi>$f_G zEbVBL#9J#iY7hDnU;}~%>)&#&&6NL$+Y}5cc(#RW7pC-r5LDH|vnfahGt*C$(Ng4D z@UDxQAtvS2YmtXYUy%%-_Rv?oQ+J+2A0XduD3tbTMwumZ;T%JDNb|+ing}FNbj9t~ zYGxl7j3TfT+7h#O8vy*@Fq~5xnOT1>jYI=xJWjqnga#r=N9ytv{fvN2b{8`alWjGR zxGp9OJ=YMcpx>2RD*S{iX1{ua$G_fF-G`KzuP(cV`XlqHAo&r7f6owqz}@^MOA{#l z4KRTMsx;y;x}?Yp$|XFTGd=EXS28c9e09?>)%mkh%af}^xQtw8f2@dr7LZh@?Sq?> zcW-rMFZvfi!!af2oBTEFEzu_^TzVv`3!l41E93Syt^yVFVj~8=LJ2f0!YqbD6YAk7 zKmYI0w$QC~$@pI|ANU3a#__+FLk|4sGU%$9UxpGmYm!ka>h~0!kQyrg7CF?}ro^aJ zmM$&Bh_;6e_0pGtO6v>oyxjAmau&Zc6ua{CZ7e(q>9`2LS;159*^j)IQzPWhz;`GU zSQbg2d79#U7UBnOiXWtF-y{&tWCj$`AfDkme-Ah^Uq^Pvn8HXAc8;&8f&=E{f6Wa- z5m0=p;lR})#1J*jtIM;G5V4H*&_e`EX|Te(Bdh7$yW%)UbrRPWEnKA^LUWChkgd#q}YO& z-pbQge_K3HLX{vY(v8Ndy#VD-l=A-7^=uxXfF$iZecnnss~ZngOBXAjT?%fNp=jA@ zJ$hVjBu#m=2~kpYLW_odtK3bm|tv16fZEfF7}7vKNtrxO>y&HXNY zk@aEbvcNc!%FRn9e-n0v=&ZM~tIvl%zUWONu6EzU5^P=>J9d(xjqA&t-4RL^kT$9l zs!&!tAx2x}F{d&--V5*q=Tp4jlGPnDEu6(X`YCrSOJRNsR_>@G$&QqRv*Wj?Cm3z1 z+B)G{0Tpehdc0unLyH^!<{~%!Q{=gk$$^+9v)6?MC%xlIu!lE;cR}zfui*qpu zU^U+QL4`B4A|#i(N|ymR?a!s_^Ah%HmhZ7vH#H{U^TAxnUVzYX*gi{ZONznMsp>8G zlXqmIR+hA;1|j(3Gmj_!Y9i{2*2{s$HMiU;=fA^~lna|G zxh0n{QMbc&j`l3G^&pebs;Ioym)!V;h)pUY*1FX27P^te?Y!%E9}ie*`yK((+Qt;c zOz*W3T1(fUGu(h0!oCiP`+vo+kYS(m;!bZAY%lHmZ{}&ABjSMEp6dA==9@c;=AyCB z8OwPO@f*ZPn$4$P<42s$=c;(mxgY#To)~al#PN04wIJIxvGI~PN*cW*v1o!=EzemPx0zMa zZ;bBC-;*cnZ5Fu(CV*q;^X=o^R6(neD;u2-MbsJ?Kjh~J;wxUx7rv7sMa6 zyXZ?tB}`;n(PPqEne_ZKK8veIPl?3xc=X=iHCs{s?(J;=^q2zSXfX0of1;|Y8-6~E z0M@h~)kmZj8PSo0-SNBm`LprhHawiDmwzvb2zgeBF8{!X^8suvETN+W_L=@4d4A7W zmL_iFGYhIs30Q{ZoSWb6&XY11zMGy$g_^c`Ov>t1n{1aP5GW8ogd;NGaULmfMu9$U zn5j>t{)SjQJ1+Pv?+z~;{rmxa-^X3hY#TYbVk%`~;i=8x^iVpcOtAVRkk1PCE5}rj zt5jc=%`1}Gj}eF_ZP1&r$h2X$*+^*FdG3x&Gi4V-CsNcM+rCV8VyVMXNF&onDL7xn zm~~o?EWwUaEl48ZzDytdEG(h2YrjkwL#z^Apg=RlSF1_HqQhlN_Tu<^R!wgZ19c{V z!-Z~!9%J9k7vj3rc<76Wpe8%K$#2J_8wXpU6c-!0ObhVtB9GoK`}`z}t!-4)Pw>RM zRrO<3PDYzdenBPA`qhZcPNhL=bAxoLm+tI^15f7^8m8KqSoBc7ah`}LWWEl$;5w|Z z!Fx2Q9nGe0=oHdN$Dh=U_D!5*+(Q=AF8$albswx3DM9U%mt9ui3x8Vjn427Oh z<0Ww@!X21VEnjhmXtAxo*TzB>OL5f~);4jMi>wlV*nG6$5a4F#!a{oYr-{P633WH8 zOo-HD6*7Z>P`;2g|F=5pqqDjg{zlHLhxp4*3W>jE;t$s)8wQzC{a5al8z=UxphGwIEah$cFjbEH#H{9_a9S-93G65cv3RM3dFTa!q6L_9(KzDb zR4D*OJ-W&f98>?9*_xEntwV~W_#QtXHeUp4%z+|N4rz{$f!Ho3>#x|1Fw8Q z%=fgQR!p;CNSfpCY2p~9K;&t9EhPUP851Bk zAxxcpgugdR!_lo^8@F4?eV}dX(t=nzMgzQJD$PJUti3p`atbkJvzpu7M2?jRl)Gpg z`Mt!Bv6()f;+<$nKsW1Fg*r-L#@jo%1>343`}n$_$F&I53rk7WCmIj+TT{{hk- zJnV~qI@rH+1`7AlIdqexY%9jF z)q(f5rmv4Yxp^EzJjov|oph-da{!Yt_AAPS$BncKzSe_>+zr%w02^c^eL7W%OPO$* zIxc*nR2bh<^zNxhC%<{96w8ukobU|E!i#DkA~ALjvWNxaJTti7(fDhL%#7~3WY{lJ zo;a49@!Zfk;~wUYVtU9PNGs~?_p6uq)d%SD1B2auw;*cYGSQmKfW@YZNZmR;4Jx`{h%yy)dYQr zt@w6Sex+QF4u@e!9ym`89{(vWzH`&Vt=BnGZA8?Vl!`Iho3K=WF)bNpvza!9Zl5FAhzk;2?O~IOhJz<5C8nJx!boh5 zeRIU;CDx{3AT@eh@*O#VXla?V2=LBc8ls1(3V;3iTf-7)j^(bo?j#`WGJQJ1*h%Zx zR1(z_#qZ}b` z_j*zU3xpSIr`jU`rv4;!#F#3Ic28Ex?YG?cdl~o~OsS0ed2`_93i95wyaqr-xTQ1F zi-iZmY3XQQn#J~Uf8ur_&~4m9I=g$(Z?Ju{9V(Y}|C=9y47Xv4p|vcfMt38s;=AcR zOdh;-S~GdvzW^pn#99R8FWMGoD6qQ*@I_ zHlQZ@RhZSv-X{dsxwIrHRCz`ui+7lbs@cD{C_VlgiT^e~*;|O}1<wPnjA&`|P)rr>99aZ=5x4*D#;(U-K6`Ir zSOW`9F0mTS&-_LSviyZE1#Z>CDqwmO<|7sYp-M#Q0ScV_-$-%W%L0=Ave6)o@9Bk( zWNA)C<>JD8UmEQTIK~eNt)lkg=D6hJ_$}O{^@(;WwLXKRS zqNbV>!OFaoo@j?WLF|YU}0P}K=ani9qJHOnzwAt=SpT=*PFXmu! z@>E_*KCrDO2tO=SZ>=3aRZ3}CS(!g`S6py=36!ikbO&j_rE=8Wb=h$b&2!E!UAvc^ zm#;Q&`ua*bYL41mc`3ifN8b^p^?xtOF3*YR$jA^-9>dbhD1R&{r(#+7c0I{S5g z=KQz3NcG#+4rF>_tB~gFEW2c7yy2-9U}?L#=%44Cv*dAs;L)gw247*jb%W{n{8wg4 zscFt|SL*$ z2!y5c!8O>CSr?+T66REewdMc8fhWNc!Rm*(%x{a!32+ltu{XP_DXFe%&Yu`?t-NCNZ+qV9}-dF%ibhW-Soz?`vjqUhmlsD=_h5QZ*5NSf23 z65X)`bqx_5`3}McHHQVJ3&nB5x9%y=Em$X-!kxXqnMmRyS%uPx^e1Fv$;y=HCaMyq*Sl87b+d6}O1Nl@% z=bYi3;Uwi1%k;})v8!lR&D#NCUJMV=Vf~f!G4KJhMJx;+YC1E_BD07qEEA*27bo3# zxDA-UAzyx(BtWMeD>RAeQ@|VMg10YYn!9}dfc}NZ1)?AVtyD(ONh1$zqX;A5+U1w; z3?tcY4%;}5Un9Ri9j?V2k7Hi-taB>QMXbc zn*=$+py&qwtsNaePb6_b7%vDY4^0tSDGkb~C$*jdex$S>WlelM8T4xcn1E{ogkS@eKF9RDdr z!(#S($E?h#bMf@hY`cybuYL(a5Ul|nsxKj)^yPymlw^SYsN@^q6Rx5}KV^#dL?F`Y zRg@ZEsPd+YYfc*nqk@f6%o_UhZ!k=Hka@OIP$(GuwdR9CA!Etf89q7BHxg?bl*7wc z{10^B53n3#Ddppdu-pa~nV*NqP?4`#Z<_100^2fF>?+3eOSsSvo~n=)R*8c3gm6%@ z{}uM3J7sdtlrk9T+8`K1+qjA=yt3_9vj36Gkn2DA+TQX_$DYIb?l*a}{jnLd`JZD@ z02+8N)RwW>uK;Kl5HE{5*Jx5h<%^)f>xch;04K(x@3T}75BytBOP18+~=(K$L_!W=YNW`AE!kT z;I%`-C#H~$PRZN7i3B-0nB4KP0Cp)AVG`O>dG{_jMuR0imc8f=X35&qK1hGz4%!snx>1ehns-T$;(Ra~dbQoHeA_HbaKh9FN9am&FQFo%Xe&CVI;tzU^C{ft;na zLBGpdTXX27IT6dZN^`nfB=_sHH((L+RP56EFQ`cD%2(R_px^7XVte}=#kt$+JE zo-0ELBc_m%r;S!tLHULc_jJ&yUQ3j>;n{Mw9DR1_DYZ7`;{RmP0m-W3@^+ri=)XyA z$hHfna0MQg$_)mTHoP0JrIZR@=#zAWuV#oiq9vp1a$DX`!uTu68@SVOE5xe~3I6?6 zwoMv2oM!mx_!MK{Lwa(8rEOT|imtU55ndAPun8V7@XCBw1WCxnRD+sf_5A5GT@Brl zUg|~s?Wou9#L{udfOoZQhU8EMWp45fm@dDiuiTJr(6sxk2SvC0O(VAD&b{wLXBD4q z&az{kY@#)or8I}*R`$7s-egp5eW;*YLRx!C_GzhsLw07YNXt$vzE*VMauu(*mcmd4 zmOvyM^pRo0qA?t$Xr7E<5?u9q7XkQ?( zYG2z&Vese$XbawJ{M;i~%CucV{AKDjL;~7wPDm=Gx#5TVseJ?Ut~!|Vk`gR@#3Eq; zkr`U4#o#zntvFq!l+$rBX(v}`H(sp70TWjY(v{4H1G2GcMBDREz4N!Kw3+%)c%{i!h*p(&{7sNpJvXEtDDke+v+ zY_FQ1k#1x_SHxv!Uww2^KME;}pMlhxMrpVd}5U^`LCYO%}FbsToEL*RYo;N8`n(dSDq1I3tUMO@~a z(@B@qY*%b}eL^?ID4oo|a&RVDKiaMKf@ZT3$eJock;T-Kt-l?BT=3xT|q@lFWbbHS_56z5n)Bch5eqJpxnbtzY zVs9D;HPw@Qb666^N#V;H8D6P&IeQ*Gx!~N5;BoG3CWRia%$h`fzR6$2Q+|uTLf3qO zcFSj~_2h&Xc{&g;G=a|G*w;V2tLS1#&tyhUB{(f1!_t#KlKm9D3>ESO2UHqM8A=Ef zLQo9!FLY2UKdH8sLME=x6_1}D7~TAQxfi&L69V~f{12Tf7Qm)RRRKf84_pbuVce-d z_~ZLE2>-_S8xUZ|P%9B&#!+htA|Aj1)${`^yO0r-+7YH@tp$8p5twc;?~&{?(LrU1 zO$xz&eKZq6%RAlBw+mtk-Ea4^Vt+}bySUZAXBv0?$VSADU+T%w3cxeqihg{=(}*w5 z!iHk;C5WMR0a*`2VJDDF7_L+;>4<$`;e|#8+7{5X-U-QkV%+@WTG|#4vNW6qq}c>& z;HE1SY;GeybXCnDw5?|O~ws%h9 zTcL)6*gKU>Fmpg2eTAo%l~g*VrQxZeAsz~I*|o(kE)Z=2G@txgX@nDn%ptz3(!!e# z6HcihI|AkX_H>b?GuWsHMvDU=jiIlKh2N1`C3Czznu$EDrUG^-D3?g+PFfH;6y-GB zqRO5ru7^^{!hWLhGL=_60Go+Vaol48mz3Q z^qA}=JXt?(gbyvd82FIn2rlJ`{g3m|^`N%+BEDwEx+jrOlK-1ptRp5<`a}FTr}rNU1pl7_E`S*pkacqRFm-Scx3M(0{~v^r zmTIVsA&MEkXWL=ey(7jHNLuVKuTQTJpN%?-D;rBK$-=65cH?xuV%zM3&wId7w?+_|O6p*gRmO4r*v=cWXsJ0ccK=*WD>+833#iZTs#T!E zs7%whGkVZp^I3n}vjaISpmwqQrrqH0zai`O86%C;DWnEFXzE%NVrQ-}>#)=?Bm9+x zcKm-D7PXhlqZeL|%0AAo`85Wd4u7>ePbUO=fy%X6g^R$gb~@AbiTrDq%s;m@N;|fK zmYLTfh&I(?R{9ahnuO)S2QOF$yfE?W){$23*SKo@Oim=u_g3qvgPJr5HKXL>WPX;N z7Lr2PJwKA691y|Jgz>ElIpH=5@jX7FsOC1+0zAK4F0R|Q3hGZZ??ASblTkYzrbnq7 z0PLpZmO~wXeE%*k;ou`ypa!WmR_;nfZyjj~##gusHhez1DR zqjpA3d=npHwp7I*uY8vYe8tr3cZojB0FbH0sRqi6n(!#s8KpLI#b%+tD;y#hTA|M_ zD{v7MkqEvv&bZ_M?$h{WXx*D{Q=TuT@gUng@@yKnr-#}r0T7dp+0%&!IW&=cv?gMb zuGVFZ=Z*w(ajmE#M%*)hl2WsOpg1)8fX6_NEYw6@dwcaVe8x{$9;TwRcyjetFG!SMDs#8nqkHnj& zm<~xPxe>|!{c)G*Q8;PcaU6aDNvWm|a$ek`Lvp$7i$i*qKE%7y`9`&C%h(n~uiyZG zskwEc-K*hZE7Un?x9rv_ZjY$}2kP8EP&tw7E)3rov-H?-(!5$}-WM5XFUjV#j}yr=5q6egj--@?H(CQu=6@ z)H6!6r_))WZ`Q92)G&69pcb1`3i^o}C~`E-(JvsAK5sNck_tzHZYfMy$~}T)xY#?W zZS#&6*I=fm&6 z>UNR;)sCb99fw1Zfv>4bv8%h{pr7P(YF7^D33q_g;f=eHinkx2@M%-rvecSs#X(&= zTdg#0laQ?`n7**%sHYichsq9l6_xM9VcN?6%ZtK6CxbXcvm2?W<{SB#Uda#$sNV`@ z>f*@c*tv9!DNjz4|Mi$usk^jlMV*op+gW5$<94J148fV48e>FBU$!Y+(}58BcJ)$H zVhp=OCiOFHxU;A^r4Fss=~wOawh$4cVbC3=JR(dbkNJ1b+j_`vwiVXWh>XSGOmZyo z+q;;PTeGyf>>8IqLq$YMv#FNAdXj{{XVuYzOtG8;dA-dvku|-brPh2U(X@WjYO23; zN3jA1(Ua>^{bqj~IAvHDTKojm6iR>)+$Fe^E*7t(4OiRi5#z-9|jZ9c!Aa|&I{qM>0Rr(JA>&WkKCN-QZ z3uKKmTZYre=imJnNP?XCmxDoUP?L-iqKgjlx@bKOb{O+;HuW(c*|G$^0z?oYLzmS^ zw|`UP(iAAD7gjf6t_j))Igl@j;4;hOlB%_2$>W{c-RdLP*%4nty-CmBXeiJk>K_eqEFle zEl#OaykO)Dq$pfOZcmGW2T$u@Y5}{$>?E@W!@Aq?h!us126P6xSwo}mT1_eR@e`|N z@k{$qCBKyLRH4&cCncur*fm9Bx&3;6acwzhQv_9p$X4QejjPuKe}qI4WN5C4Wvdq` zbV_*_@whKj!$xuPLf3HZ!DwZd>aU@n9N6};m!c(;Wuw4G_HCS0IFuWCn6|EeOgZe? z;a@3zSKPdcO3fRs(en)$ipFcNgY8wN6uvokk|dvFJHcikv+d%-isH*{j9SDqhqD+V zL_^MLQSITo060qkvUsXG4er={`R{|^YKG+4?1z!UL=tceM4tG@2q{v@{1mPZ=JPA+ zYTXESRLP3rV9o|Tc$`!_ddyGYMd=DvSI}yQ4D+kdo{Sg+LgpR%`8QyH@jvjHl}4YX z3U9OOUDGeX3-CJX`fD*#gV@^Ob!&~JDC-6xHweiFlTDie-U{RIC5_Rr&Cza|E92^H z>^Yl)a*WPBbpK-7xl`z4#_IoyBnuba(txkDOL!YAm7D459A*!0Te=s1YXMkG^d`xqC?6-o0^YiK5~QMaLQczA9`L$jQgZosC@1X9JVtyT<9 zUVC>Yk%JcAZd8;4bic}khi@$L+PU|GUmkHGjHhpw(ZadkL!*-RytKy~YJg5fApZP0 zem^oofz}FrO8we7eYai(gKfbW_t`t$Zo_@Wt5h5yOhE$U(I4f!`r6{pZa2{(^3Tll zi8s&rK)*<=K0NaI1c@_^*59K)PB@`(j_4PhnahuQe||vpl;tkNYKgGt`!g)UDy)YL%}G%NjT6nDJ@O8hz6dV7o?bAc$IY2}I1GXrt@ z?=@4Ypkm82@CV8A>lQ1W_f=vu&0@KmAI}1Cz{R<3I?#3H9(^==i~VCOjoRuVtS46f zmrIT9*l;`AMLId@HbzqqHum_+`9O5o74xu^c{onz>L)6WNO&0pymYe47W&2D@2l@r4mzkzc`!lDZ3e!+ox^e?CL~*ORHGP5Z0#zT2&dRU zr|Giw%E6(9t3Zm%u$tji;!@tDrGB?kt(FmZj!PW<(-`8}J5fK{<1g0!_VPn7N-L`i zRJiU46)Z&SJ^bnKZ2;CaivXqE+0^c?5<7_4h5w{4rxEnXPbBf6%LJdZGza zyCMe_@(BJCGkXjZ!PW3FzMkUX3s>CVAL2448Q@BfR@@@+{hVO2eQ%y^xTyj7zLJ5k z1L6vy<=3@$f;?dQr?~7NJ+$)&>(9Pf09E=k=_|GACbL=bbdB=yLw8%iy%mEiq4Ko+ zclp6KS<{#C2obPyPV%6f_cdk=0k53%-vRn+GCL7#Ik(zN2QwWJS0dujhbgW>L}MjnFelrnhW`3*o|5~4t-eY@qd z>0JN)R`@`<#&1+uYk1Sv)2`tZtG06$&eVp(M>z4iSsX>_`+jvEd6S+x<*D{L!B|x< zJiZl$G~6K)Muk+5dv_$TV(U%kFr972&kH|CTSXvW(8p8F)8yrJ49=gFBpyR~VZOtq zRQHM8Mp2ovglp9^t_Q4ZzB~Nt*RgwYHyGu6ywBst+d#PR-JfK`o_^b4y0piDBOo*J za26w5bs$J*BF?1zZB&vJT|(Q)g@2ZH70AF&NTnN)UOJarGNEjU^AiO32W`@oin%>C z2J!TBXi|x@Zc>87G6(&-r2Kd+X5+%*-PO&uZMQ3W3I=Mt5)F{8pI&ZntXM#n$n(7O z6K7<@8(PM@l^|@hT~4yHi<%CLiViQ;(Hr^YxqNe#xN0upuuQa$sNry8aaWuR#d(MA znf>o~Xs!3yjmlfPye}krTihRd`(L(Xpqa4D(h0?^t>N5kq@HX!M2y8K+IvAaeHUNt z={(JH6}5_Wb$DQTMpOSRbPdz(G5L&8SN^FeJDxYoS-$&+bv7U;Uq9>O=4G>?bIk1G z=l&#JnH#i1pTkM*o4ATJ31o4)*&3|PqXt=BpTuLBbc^nYQ4=9{8BK@Dx%F}0i8-ic zByFcQ&b(FPh3KOq935FTcx?9ef_$_+v=^^MVkzImGi8R;t`-8(4 zBYRTO@_AmO_gLFcd^eE3@@euY)=v11CiFdoqpXba80D3IiUFpwv7lT?M$$VzxdoFi zJ;)u}qOKIL6*ZYf&CSV0YkI0H-KkJnl$@ll_yc&bb%9&_-i`M3XySwy5bhLi#a?)7 zeePbEEzf?A-TQj3HS=V4;+Pq7)LDYE7uOFa^@O9qFIS`(!qHde|HFy{q~&u@v(y2x z(l6$`TgTDz{rI9Hi=j7cS3mqy5A6;FUvyj>BL1`bvSI^9w&7`7e&S0+QaDfdim23O z8VvYV^#sy-LHHoMZrZX{6+#N@4f`x3;gNH%X-iyHwgx$u+>-4bOMY-TTTjp!j`BC$ z+z%GfSaiL5i%rOSaOEL@&z0dnKG3#Y6^gYIsnlR#qKTZEb^4&>$*Ss!u;G4>2VvJ0 zQCjJ0B%FSeQ^k0kSNc{p*8?ax#`nh%8XHHM3OCfl$7hT2fHf-8uEy@Tjy5Q^HZbzVa` zvso)Xn7Xp1y3U1Sz+CKiF0_6rpaTS=mKeQZk9k_^;`NZ2oAt;Z^D3Ff#VZOc-JA5G zS%JX#c&uK@(lMo1G=&s6EwLb5OE>lD$hse>^$=T`w{#l~)Zx>)JA4+Jin~U&H?|>` zqlZ@dMfEn&?~vvn zt?eVYUdVVhwM}2ES}w>T3?nwIf6F!=>JXgwM$1%81aS%)XRweETO z{}w3VGg7Q!Wfi8O#@ONle+Y+1Ss}~|Zh-$bldVWN{4#&&Y;hd;5lHnWzRoo(D6%^o zqOq)IbQ2F=y)mK~qOo=Ov*3@O0QANFW3cZFVZHI5fXFE?$RF~K#|=;!2GvubB`BhbwiL_3(~Jt!=5NJG-b8}gp`#*Pp)v`M72u;IEg4pBH)7;IyWO^@&H56Z&< z7aT=NKayHO*nc|-dG`P=Ein|-PsNoVx=bc*7_8l}IvbGA22#QU?=*wws!(UEpLDgWk}V>hc&i3-`scPPeoect z59)7t{_aRN1w{oV&cXu!5Cv-nK2@+GQK}lHL=g}_#De-zD}4cGgePBksPIN7(j)Wt z6(9W5W zh4o(*#dXZ_J@Fmk)RIVQ<8KXJ7s1AsRJ>zr)O}EcOG`KjO|k2u`Vsm+!+N?do{3a1d&Q?oh&GX2#w=Sc@qzxkjYZo%Q}zH zBzP$gte#v;LuhjDZ>?vNMt(8AWumrP;;hh&I>(RxF&6H0p9=p zrVoMSx@hSbW8c-5-8smUlIfd?Rj#=}gsLGgZ$-68x;j{HZZkC)Kfk5oj}ZE$Q$2qH zlcSSafoIFz&AftXSDMBl44>j0w)MPcxL8q;2Rpt~YyHOqul$oIU-$1_8x_ar4RFn44%w%P;yIVb9ef-7}0iV__Wz7o;!E>}S zoaxaqaj|bsGnk?tcIg^)29X}^i-en1Xw%D%Chn#sDLmn(yMHKt*nH#;(v1O}gRE-l zNj!FY8likgX^GzhdF$_Pav7>zSEK4^Oq6IB=)>RiH zy!TV-XP=UVNTNWx2$mjn>zDzw@5aP%Z1iHpDd3blqoAL%<0{< zefvLMTy<1bU)P2Kq`QYf>23s(mhKK|X^`#^7)qq;BGO1pcSuNgGo*A#gP9Si-|y|DEN(ofamDx=H@h3gP&^`Dxi~>F zz;(*HaHsO^{ymGm>C`-PbmCl*U<$2KD(>SCDs?;V-Y?)(&IB9;1crx=Y0*(a=trGB zD8&r1h`A!zN7y)b9-ZG)EkoQwz99`kIXxw5o+qNC#>iwx=e&{CsizuKDMZ+b6G`+rLLIRzc1f_leG8 zvqD@L%3a!qfE>%I+V(3_)000>pqyFwrV8;@V?rc~o@6-VbM)a&or~$h_7Rs&p&{Nn zU5qF4=-FoP)rCp>is*&o#^naqYuT2GPG4q;ahjrWo}A={bB14z2)Qeqy)Zk9>PJ9po=#Q`NPHZ1QGo9&CYrSnF>Pou5!pH3>U zyb5J_Zd5ytZW9+%frh3;j-mlQNS$=|m}TD4a+4qYsMRpOrAwr_S>H}xHOFTr!egG& zn`F)6(XGYLuf@w(Ie)M-SjuCYX0a=7UuoMgtEqL=cKSN1zRPzheQ=Rgf0CPcRz&E! zLMN`Bb`4T{<4AP87Z?@@tq4Pe6zB5qL2{q~@V4b*Qq{)`>A z;ffhp7`u;5N%!hAMwso&U({Dk{c_gTt7j|tQdpn+b^#P7La#U~RA}W?P}6eHaQnt_ zczfTzMVMKf>e*kf92KYS8Ei38>S4ZDBqR>>Q1(*$%lA{}C6=4bf^D{?%|F6KKDSH~ zFbPV8neFNZlXl~;5*pP*HHR@%{UtiqjrbMMb5|xAPOw>!@WqIz@Q>-}N0kQ#?hxM^ zh9m5x;BbIrQ+0iSNT{k_%x`pZLT|Y~@(kirT5{W)*L{GuLLbYvrEnzM^3n1DPe8D) z#g_VKgOw4psYwNtnWR(A*(>q@l~?kEmnfACCyM0lW_#MLG;7n)zns2(m-XSR1DEUp zj2jm`+gz%oqUix@JLjJK(#EiK5Bu6$k?7JM@0082dXI3lc-^%m)_P1D9^-nC`H}*qm!av+;V-%t z5|+zZiR$P^*t6j}r8liJ)}O0u>m0!^noOGU5At6iCcu>e+;qumP`rM%ce}a@DPO3u z!M<}qX>QEaq1i4;i8G-)+7}CxitjM}hHGYONPB!>pQ9HH{^IH7yclB=Sqb#SS_=`t zMtqj5O|emTcT(Yz7%9~xUBBg3TIf7~=6%e<%FWf%HWI0o3I zYkbGNPMh@0+#>TzM4TFJ^7nn-YpTDQM7h#zlMCi_oaVjfR;^D{kEu!g}&Js96;>vsD4% z!cTn2>BKDIi%+0YZ8 z7o^FZhM3qgy%geo7jSp?i@1YIhweG;l$@lN z1SSoE8QGZ`+J!*a%VW&ZFUYanv8a$ug4UEIs&(pq+F0f%aaRiL$hlb1W%=a+Y1gof zQPu<{;~2WLa(2C825n`%l9qe2+FHmgL&HgmfuR>8 z;EJWyl_SuWYCepitN9d)E(uhWr`4DiHYjV)2@qhF|M~7ItpHRRpE11HnscS&wEH?x zV*5p(!62QB zo9M_Uv*ah(3|I6^0-p+pxA12r^)tcJV!x(HyWn{m`kK6u_bexrGeoz13@Mr7TKWYB zuk7Tpn8VhgCDr<7H6kiULt(Bwg>NG}Ye}(xd~+koOhazK|B;$8$n;*~&2t4kK`lws zvjxj$^O7qx?T=ropoAcnoeVRcvn0=GEnmsOln>U5(vaclMwQS%4H}g%Ke)0v2-cJQ zlu-7s)Tw(mcJYn|s*1$H-*oT6yF*su`OT8*{gbhg}e!%ab?AoKYMVjYC77z{yS}>qXrz!7P z*Eu^B@Qn*J<5i-sxJ+P;6$M$(ve@);>QK8f9yhLbk#$(66%9J@iqs0qyM}D1JED7` zgtiB%^l*VrzeQ5xoX$t$dz|t_nSMX&0*%Tyo}oU}DKAZeYp4A;LFmy@%7i!Yo6Q60 z2$X@kE^6W3#g=b1)l3N%%2QCSJt>m+i*U0`pSM*^G>)JkU3!w?3J}kHsV<0RgM9X(rx5W>+=Z-DdJ~cTk#jVgQ`zFmTp#~>xKR7|s7R#r_II{P020@S4?HU7r^wif zJYiJ>2>`XJo(##S?xx^U$g{{%jQ$d}76wUZpGPbO_0m=o{U*O?B6pxiY-=E#ha(95UCF@a&(zwOsyIlw3*|vCXbr?pV@5{YN>6ZjA@4d>@zHpxtyH z>QOY$^umFMsZm+8ajxWTTLthvmvg{dSCYu~wUFA8go-sA7E-dFyVfGJuqW2=)@7*a zgu%OSyA#v~2EdiHTx{!IHwgb6-D~u%~l=xIcY{e$O~ZzYU8F zV#0C&mAoZhHWgUKfDI?|OA(*ZDo$5Bi2Em_*7^T69%tD`|6F zRf_dABa#a^1fD@grvvt$?z`$<{_W1L`_mo>{d(X2MUk?f#cWy#E~C*)gRkCdODrWm z?aI}v++t9NJ5@%PC`KJGSLlg<6Z8kMRdQ3_rEhz(p9If}^n_zDY%ltZTLIdzUhyS4 zF?t;-!%6=Z6XO58^j*BdAkm`qs?3Hga#o($Ij=VYC;pHE?bOed^B%@;vhKL9%<_xQ z!Dk<>-;ps%t17f_Xfda7h{{@!hH(DDV=s`+*VT6taYG_dTc!Q_13iCWo2i02#`diOuVZ{rd%|YCfJ6~3 z705b0heS>{H??J{8tM4@y(#~Wpo%xk-`JP+9oB~Zkl!5d%<2O%kLSMbes2oBur-zr z|Mn)i3zJIacN5+97F*&p&N!N80-jWM>yt?oYZuhq?6D1V=0HxHJB`G9M3h?O_w68T zzeA0&33$CA13m(R2r%hS2b_I?Ku2Hic@e@@irV-`^I?dJ2`thsQoD)nLBT>gcG6{a z(&Z$q99V<#IQhIDR#U+g$1UNJa_Y{KE~LU5Woy1mxc6Z@moK~p_S<-Ydb9(5_@AF0k{nPi+zDx9Zh+c|KvNFv4NrY0Hmb9EM#ssaq(arJ_P@Z5!^ss2@ zdA2-|!DUk9n<@|kn+!NnJ?h;REO~9{OP@0`Esxnei#f&dX8K>trD#;L(@wOfW&?jP zmV!U{_(*l-`Q4J4h#3blRvC2xO4muD@K<5l&#xsbOjFw`98%=b$MG$WkkR}-(+VBE z@}KulQU)b+468KIIj|>8K@B#T^9s7bkm(VrPp11XY#Z_xqZp@5nDPG5qp=BM7pqFn z6Q4q=5F!|9xP#*5h9J6b9_ZtQ^_3EwNXThX2ZD&%+LW^zwhc8kcD4Lv_4!7$GgFoV z9Lpas!19`IFn(@h;UB&Q_nA{87K(4YC~6ICQ^FP*oIeMI8M7W2LpNemQ%|w|K{+_A zuVyoQnMC$FW19U-8@Q$8OE_373a+0ouKh$Hb4A5+)jkKqz})`j3_kb2HZX`7=*I_> z7aSR3Aa&FEp0vgNER{;t|D{Lx#hY6G!#0ikT#h1$eW4_5ji&DptByD$@_4 zq$mM@?{^Gc4lRw1lkJU$hIx$jee}kLF)F%kovA)t=-Ucam^eAVDgEu7_L7pwFydqD zAyG9ObHY=cY0?-@l5j$TWQTpOK<-~x=~9PLh5!`wBQGJI%wrhcXpLD_fkT*wy= z+=_G!_sVM{jdFvH>0)$6FD;m>w(eqXXblCWp_Q<5F3_eC?-GjM7HM&eD1I zs+wi3^G<3ngJdPjNr=ZlLs(2`mf8!w2C&%sT`TlT=J^nH6r)|ODpEV5)>uA*6}+bW zFO4nO{W*ree!qt*;plg^20PFCJaaj!9+Of>`FmOz+DOzI<3-dOwTywYCW7+QjqZCh zjCt-ec(}%M8h?4VX!M3kRPBV?;2vKzYs;hEkjSqK=bk8A{?bsKT}K!LXT7SUzc-Zdr}IX~(^WGTuqsS(XMhkBlB zMb2@nwg!Q#aY@5(U(>Ag%!Jlv^{9!{Q=NUJ4f}eW()U|^>dTfrV zH(u}SsY|W|dXpv!h^Mv3>AT=LY)HCC#tCDV`0wdq`c`4g0gk165Q#w)%soFOK_rJ4 z-rtcF<+7fK)yi^b)5igBT#^|)xtZ|IyI0Df$c~qJi=8?Eog_xhHP|rc9r5y zwE8J#TVg=B%c)QR0d!5*rR%qDl3z{KuZHvu!^q98uTO`x#>NSQa2KnP>|8YCQ84jh zGq)J$Mj6#P)|1=S-3TJR1lkF-Y#N`e8-15jVqTzR;{RPYcBD2EyDQUE7Iq998)xXA_> z4zqx?_#Z%-!_Od(h>(xQ6n*gkf^y&jH^X?4|0OEGYrg+;22p7mt_rZ-(zhOU`)e*z#^b9^9M6qhZ3k9WdSAIJh&&LQlJF8e@s+BV@v>a=nkA%(*tPZ5MXo+ z2c+ZysM)Z>T^7(s58(N@5U9rka2YoOsd~dtf$qy0^gPXK~)g&q8zq=_22ttppo$aO6XXeu@V2pBF<+1O(wndEa6lK)Zny4|&y7U=UH_L+E6R5Ata3_$aS833vsw z1)ZcnV8>z7pr2X5t2AanY+4+2mIDM$n}d)G9wN9iLLkH0$G1_KWJsQ>j};n6?p>kbBp_A`>G WDWbsF$p{Gi@ZUasP|4|kdH)CXgbPdn delta 19998 zcmZ6SV|Snp6Qnb-ZQHhO+qSKV?ul)4V%wTbY}*stcJ?{%*)O~2^l#{{zN%_q8mzYw zte)-%Lgkv}Di{O^$QcX>2t#s#8D_HL4|IUh%-+P!Eml)c3r!3CD=yRA7$3q+I5;Yp z3zadlWm&VnS@sX{4~8H1;v0x#Br%GX^J9Z@*I2%vP(4p2N(NQ_FwM2=ODkW|U(td# z&zWPws6kcq%b9HN7aPx){!a(jR)2*coMDBiBld!Ve#nn|%MD9F{An-VVXdXk=+^)m zAr;&NAw8QxNkY&lSaEfKRgy(BxOm5d~Z8G`p-x_6-tcR!1 zj|#7__x>=ZY-$wsCrqv?vKY8O1dRa;&jf$;j}+g69J(;l4K3XV#ydOrU9ECR^ilM} z%pyxB2|n}kI6bN|raR+IFh=|%P0E;XD2bl$=5k3TRyQOwMQ+6m8{|?Zt}M;M6u%!T zuauvDZn(aJdCf1tX)RTXd2l=`v$e7`CRKaTah2TRD>zRM18BkP z-i7_W1UOzA8PsF->Z{aMFTw!5)Xr#mxwDFf3(_-<#aU*GQDKVCNK)s;pJ;t`{$8iuC5<%0GZFD2O9AeVZzYhjVrcW%dxWrx~c6pNn(26n!?4dCC~&c!-KvZWBl zJQ-RzWmj9Uj!Gle#T##Zh{G_1M{x`X-@C9n1gh+STV z^_AnH+red%76@YkUFAHkja7Pw2ALk~S#kLDJpc60H~S){Z$tLi%IG9L3H8P9b{2Rk zJxEzRaY9>LeHX@3bJC8IOmk80s_4_r$;V;vYsb_?1sSi?s03gn&y#<5E2vqr?)f zXKd*H?uq04)i@AZxV47+6eF>RA{k`O$S!~F>oi#M7ulD7GC&L|SX%Kei7!x5_nrFX zN52d5z{8wSY=C~h3BB-uL%(i5TH*(WP@m78DOU^%67mSODmc05U%dHdxWpldoIyGC zL-v}o8`eNfL8X0+d0w@$ej(q~X+ts@p;b3n$_ea*IR>C;O%S;cjZ2}QPC-M4u8 zS#hHf>pi3!DV*z+AOv=aXA`TVZMSIwFUO;m>uaGOnn1H^Y*Aw^~{qBecUcYD-L=jfNYP4rJ}f_L+iV!PnszDE12D1e2Q z7A^A(KB&7{iaMU-l8ZW5_!~s%&Lu=78vgYj71u33sOS+v_E(n4@&$Wn<>eLj)&_Qr&Rq zD{B2Du?W*I#UC~7U@GI3a5!)A&p|{kFqVP>ApH6z9Fg>{{&#dyS^8H{sMp;G zB*Wbf7;OV2}L?_A@AKi+yK zuXsy+oACrb;AL=cc1g5-P@ zDj-(}#!r7l=Np*6>M2`V*nRBiX;i$>Ubf+jBbbOplj|{`NUBaf828-cmrsoXwAOtVY6|x(sgXW6 zVs|>qb~@_%W@~!gY%_d=|CM{UOuW3m0tB7(Syioe6=bcb-=9~$B5=I(p#8-eblPo0 z@Dq$64xozoH*^hg3m;&_0pxpsDRThmgNPpuflSyh$;4^(GeO>jM(PVjs#CwS zU!sY(t5PyKlr}LBCKwIQ+~;*eCb_2a7esn1=i8|e@StCS7m*xO>wE;huQX2WI55~ zI%bJBy-CPdFqh0D8zH~n>ZpBu$o`@?EzgtTlF>jmKxHrCjj%J#R5g>XAzjK;bsA>{ zQ^H1t9e33+8JBH2rxnx0YaC7i>S^o{bgahTh{Mc-Y48*}Brfp^C>zI8^b|U#Ql?7n zSq?qbTC?W!Iae*Ei%1ketLPG)H>cZkWqD{s%4ZY|^LP@TD04%w@LK*9)0N|0@N6&m zRvvH87JON2IU%ie&TL>^wzlVHSV#Lf(z7%uDKBKo7xVM&BCOpuo5?l-`K@(-pQXPG ztRM7`RUAnZYGn`YL_9`zb_c@WW+b{4i7LTyrC|q?(a;bNYt9ur(Hzif1u(tV89SaH zn)h2h&Sj!lxUU+@@ZZw^kc=n{CBcY%HfQHJ=c-rorQPL(te2H+3PL5Pquv$^EVup2 z<%7D4qcGhL5Rn={#ii#2{8=nE5_(rM@r#l?wi-eflJjs~Hh=h%Ur`@ZNL{`pTn;aC zOFjHdW_be!RB6?Q4wAC`xsG~t*p}ld(e@i6o6qUx5iXy`A&1n_9xvwLs4h-(IF7Ux zt9R1EE_z@_?C>tG$7LcZHV{Yl;?j&)&CFyuO66$in#?CI6GhX_ zSqFP>-IKK;$L%nDiih)#etorD`kL8_JXe7*ROuD)AJRU4`WEs-nTTh}(n^nfvd_5d zicUYb6ixfH&FSxXmNVt)NG6ZX4oHFRDMYQ;_Net*8kC83Y3?Ff4O-<)dEX!n2sfXF zZTIz}1p?ow1q>E|(MTubQg%`acivRGio_wzp36L(gs;MBoX`t$E5mpn)W}KiM2VN& za+DxN;kVan#p+4Fw<8^1?T}=7FN74FS(rXg3mr=yd1=fljn#9lSfq-3iI@0zFtj=?~d)hqQ#j+|`8#(wZZG zX}cz-3kE99OnX@bOFr4e^jRSWE^F5#cu}KVeT;-aR@_D&oA%9M%^{eoZR?Z1C|MTI zlmZilfi4>Dnxa*ev4q$fK~NOu0r@bxu9g)PkG4LikVZa4QU(1lO$xQ4L9i?8WPWUg z(k&IKRBShZ@AqnrEfHM$ZMiLB(+;Uc-@s2enkMmDUV5(a7i~9;-2?qf`&RTFT32Mkhv&s&SPg8N z`U>;|rjyips_#U~3gHyFuCx8&HzsgQCUK0)QEk@1Z#`FOL_JsWxI2B_eh|6NgA9t1 zl8pqkvZ8zRlH4+y4n&q#WoJ;9@HD2d@vhFb zM~yXs9j!Sz9acuPAi6TdhiCUk{7CrH4C}-qFff0VSlmR_)d+GXUdKU2<&6}!@gh>z zcz6^hoG~)DkZ4k=W-u}{{)o+0Y2Djq$+ta37BL37A#IgJcM;>}RGsocimlZFo&?=L z^^m;t4ehnF!kPkyxiWA<@$uTIYMOcJaA|`;=&N$wa;vI+cZ=9S3I&Ww1>|vGxbWZn zX@<?f!J5&Te={7}6-8 zj>kLoZV&P_Y&!vK-&QWROXQSOe}7zt>?24+%@#z$>??Q__kgAVLfr>~mnkGJ6d5jBxskF};FNu^~7tUP5k zeLw)CeIjkLoOV%o*@p$nPSY_ZxT^EQ**4FVT&+e29idT6w3Va2W+TaVBPojAUgmP) z+kx&(_pY8_l%7Uy*8mF6D-%JEWEBz6JbLomI=l&sFt~~-dp(R_GL@G`Z@|KG^O6aI zm+u^tTa#Pq+>45zCg*>5RVmj>6X=w^cM9_oldZC(L5{b{f2QgR&D$Tbt+cA zX%Yavsbx8pDPb4orSs6NeV==DGNQd_dIu`@w=ITfCdI{}Vph>__y>YA5Uzvd zgV!DS!ULEGzTnq&9rF`YE}3>(pE~dE!?KW8{(KZFcFyd3bY6J)X#h9aI^NNR7)t44{$n#`(eRD>Ci}E)@7%oWr9#=DA)= z%+7E?X-@OEY>c05L%JNzQzMNA$&xqfwOC1c^K|V^bYz)zvJusDRe9%FtQ~wcSN%XQ z8vvQdaT5SGgX6s|{5KE{ndorSJeF~YBI_LQq+Lb+rq?x_#S$`aSYjSk2n`{xPDmTLT#?_2s!UgvwF?Vy=sz^7K!fk=UKRHMhI$k5xUx(kRO49rECHB{`x)uJa;EAIRo4^QbzLq_+9$ zKZ6s=^i=_vi{x^rDwqpq^yG(iO~6AhuImTrL|f8k8;dPb3EorEo7{_qq;rzs^gN;2 zV%?s^(;Eybk(rXo(>{ceQ0?b99rPi9|2sc!d_bYRUFJ5GmrDnBMO{|P=}!L^Lz>*0 zHr<>#o3A+UNE*UT$~q%_F>=P<~BiHXwZ3!qBAr*2BM04?IZ;leGl*PJ!Ld|DER*^~lvH zAW>A^bepL2H?C(m;p}>z+IkqF`NkF8+Sxu*Y`GFKyROq22-~;+oC%T8*9r3iIWInR zlT`@VoJkW6uRf8rrCGChoq?Hs4{Vdh4gcc@$YNb8Nt$~`rq35+&BNHa!X|0w6qoI%8l85Ex_-5YqpF6XA8J*uG#{mDL}!97qmq!IS+!TI z{8d;U0XtszMGznedUij3;mDcoVE<|I@7|aH`rW_hpVw0h@b`xFmx8w)4xSjNltps# zRI$DM8h*41z*dT`%~GDBX*_~Fkdnjgnxb`!vexBVLX4-xDY1qhPZEsAk~2ty@jRXy z|KC)+w5z|0!$0pPyB?}dy|4?CL0qLT%y8~A3$Dbt_!)85PKX@Dm&2GCLV;I~Z;&X}KQs{uK_O^H&>7_K|_sjCk199Gbh^ZBAZu zF^KI%J+OSX=dtFdSzhIp2a;I?HagCty^BYlfJn-f|IqIl7mf2))I|ja^$-yvohe$S!>oC14N2_?n!G`$e z(mVP8TyKu;+j|JvC7h=+$6udkr7!BV8~^!}gMEcNgjcLuw~++c1D6+8}c;PFX| z+Ao$85wd+)S`fR>@muG1)GkK8ZG~L!a4MNkNrg5TxdmUxB79TtalMJ-P0fWvYRsn8 z4HFPx70CDGs~d^TqYt z$3)Pp*BIbj>n7UZcrXqR%UvxoLF!S`YpG@b0Qm&fT1h@%F0`>g&>BFxB|}i!WgpnM zl(+HLoqpaK!3_xdZR;(`DU@s{G|~jXPFs5;&cKOx-glncyo7EFM(g<0fM*T!6%Qo^ zx#1o;8xFv==kKKB283d9bcdvKeBl0_yMYa;+Vz_6uWHZUJYl0BNIpBjsateWnw!18 zg@OPUZ*aegcRfCI28?dBV7Z8iGZ)U$YwW`>y$K}V4cY#Q9JzZV^35^iBjNx)eGR_W zj|e{txo)`-fb=h?WUpqQ3i^V}w*F!oN`?YL<<5~qZ+qge|{Y~8_~{BpvIq4y&G>*Y$ZuY0r(8}hfc z;=#17))kWiw3T^i^f3CrtU$vSX%$!CS=sG8o`pHXN4L2eu)c{8>4X29R=ZW2-b)`eO&3*Pc3uz-@GwkA2x7piV_5H0L~H9f6sGatn$7#nN8g_2fSHly z>sQ=+CXtB00;_VDdOWyNXy{K|lq)l$TFkPi(G$G8l}M1mkMWT%mJ8GaS*QbGz&WTc-FZH$1hKn{O&DQcR5@Wl-e zI}}?@NLnl1YD)bFzEEX5F0IKB{Bku@fdk~FKC&yzYP&0*6}V+ zHNL(;a0SI@v)1QB$o?*BEn)KV@l9T%wO$UW0foL;0jefMc2&u%_Y41W2r?4XaxFns zZ`Oc^z!&51>pVc3-<9whBcqRz$LDwNgtBj;hhlA6vUiFV%xnt5P?4K9pXZwpQ!0a$ zYAGr!$vcAvs%Wbb_9TM@Can zT2WA3Gmk>ekV0#lSn5k;%4?Qt+4#41_$O)PhB%WWmKeA6gbhpBk6RGPp(bwPypaTN zh=Dy1d{igXMXOyD`l2np8xc#9jI`x_&$zc+LwE6S`st> zJNzBGZ3fHxkFvgt8aHiP_nDRA3Q-l5Mo6OfgVtm}Gc2yZy4%d1(8QnnO)MxRlsWvbQH714?d)X5 zI5bn#Hj-9A(O9Boj9;9G8p$y&|Fq=CnVF-jTV70T`tbe{48Ka2jAP!U+NL|0QtEKk zjf^Ai#De+P7_5?)OHVf84i4;$`vN$l^8z7bN*<|A6b7Tqg8HWM7IFdEII-;%h z+^><`#c*%^5D=4)a>sX0(M)zvRxJ^!UEXyXfJLPD5zyNFK=xF(yJ%FnwnQ%)% zA?F;}!~EGQ%QiCQfbV?!lX08Y9;%6F&;*5XZ_o2*9uvO=MqEdQ2KxH=F!Ni+{=B_f z`+$N-ZEC3+r6*0d!ERmGsbA*CG}dU4Q$#mb=P6o`v>;PbTl5e+7R`qOWeX?%a*>7z z!+!!;KJP3GBlY}j*|E0PLBFfi^R=_3r3x3|tgF@UN}?&d;&;f_BwXyTIgFKLM|L!r zWbdX$jlxN8c@Fgw9 zjXn1vug0oSU85K?!FZW9rwM~8HYHNP&#(}*bm~@b9khK4H*6N@@D?SkT=($$pj{0Z z!r4(e9cEH5;(PoU(Ul*vD*;-+0jgj5J_eO3r zPME@8|I%STiH0iJW)CaFfG<|f81uDv@S#G3y3vA@Yt1-l5_OIoTYkv6ik1SvB(;7D z)I$?%Lg_wckkIK3o^(_Q*bZE}fVq1xgs6n!=1kqDVFvmv48^^*_WX_g&rM1H7xjcLbZS4kj<9xM{v8hm5^(`4|B)A2?Q0%si~btW#wHh8w4_bjb%`M~@f+?{_Zj zTO?LY>$UT%{3jZEWmIGrK!-aF50E<+6I(m}Aw@;72{TcwheG)yT=oYikz2u{st6^r zYGOYyUm|iNa~M9CnCuNCq)xVDYcC~r3Zuou9w)Xl{o zSblIgF6uU?mlSJ(3;* zxs4}J)Uf$PJq}S9PVzUzZOC%wFD?UZnKGZaTA|RR-bfB)aykL7D8pfm3U0hGdQeHW zv23no;UwiPAaH`!EuZL5MBF&h^jq_-=V~(7a|P{|=}S9fI_NS_6uBSFJ*JZ^TiM;- z+Oin*EEJQ+YFH_I)IE~P*`=Tvcw9tJmz0v0H_aA!C5cbVIFzhY^Pp?o-mqrUhpY%j z_RtUtb#mR_y>tNLE_y)|x3VsUq{V);G)+vdtcH!Co~#Tl$^~_wtUQ%d0w1jsLm%yu ze+xwFJ~?^Hr>JjfvRDgT8a@exs;90!uz0_fD`=v7%I4cnSyMfc8?T-P1|tze@JNkQU29w>bj(IyzCd5{E?hQ#Y3nbL>(O z5ToO5H#M~XhTE$ApuWN9DBRZaZ*pn>4S7{{M_;SF8h%xyAG)g{I{66f%yeN$$9fxOwOvSi~>ZZ3T zY?S(Ddk9=`G%I%%J2*-8TGLG+WkdXAKj2tr2a5%+ax)t?^G+S&CF^HT?nD<18q*=_ z=fQi&QTLHI=p?GRkb_+dNy*^%(p)hNkEtq16ySADTa1*YoCKPthyx(gCX3W5qNrTI^| za+H=n1sH2h3SXA^Vr=7Q%_<`ZWXoA&y zxE@YMrfLYUThG6i(lVilaIT6#Ki36BsOu-Ik1;$)9dS5LV(KRsO9w;?PQ(5nO8JsC z8w-PPTp5U)M$Vs zrQ|^z8|Erw9IPIEqJRZW84w`2=VyOOx|7R! zQ2T%vy0laJt#8$Q@>5~%Ib_yPu( zMbygox~gTqYKm@NIp3eiJl>yAvDh92j|FR44wh3?O1Xfs2Ba3c1J*ylUWrWB!~tFK zDLJ?wU`{9_R)QT90cLOEs9K`)=cs?n*{=Q5a*!>2-`A3Ye4j%}b zwRX-;mFxF;{*;F|M*ECyrLftv3v7s;3E~>6cgLp`Cix%G({4$TJ!SCuVO@f|7UqVf z8sf@P1&5!qhu+So(BLiZ%sJ3F3Jgd7Q?3_PZ4tC*YkB3J~0G|ElJRLWEz{4I8yK!KG2xqnm?gy9TWqKex~&yF%&3KhRn)Utg>^$J!o+g%L^ zj|=#$m#xq4x!nxhm^PKDG|YV)yKJ&PIdP9vB&W_wlexUnPqTVV!lS(&|LmxA(ikn8 zvMn_R0g^>q;H@(yiOo2(tDtDM?5SBcl&|^JLb;+f%2K}+%kHfa9EM_udqmv@CCcIa zu~Zh-P2j*&mfFN**4!bd%J@#G4p0l!Z2zQOg(U6ZYI|U9AsogOJ2XdM{Se|oFY;~Z zN5mC*quGLLVH~RMx;+|nqxp;pKxErO;w?Ei0S4I1L^m+T)lPndKGlo*Mwa@C6x|li zstby;p;vyygdx?B1wSZ*n*9Z35wQ|Ok>9nZ77%8`wj}r`$Cm91dl9c}l3Y{lBGg9` zMKoj$(?3=dxjWxC&H)Qby{pd!sZOXF(-fNcblY_qgs*Bn4QqoR z4CkiEfbn8O1U2Dc3eL^H4(~kBe>#wVD}b=y`ZhkvX#TVUpcVMq4H1aD3dMCYGDc$Y zS#xsRgUOAPZ6osWUH@X7KAe!{)9+n;NJ);XyraOhp5{flM`=)5FfWTcyw%xL2z8Cy z7@QCKhpvd7Y--IELl^chN{9Gl7;d?dW|QdG>j!>3dp8yT^HGxz;`_0KXYwbz90bsx z>VJy93BVQ3Yc~F&f1-{3EsH6FrXkimpGDXTMk#`B9X(Ux@WZMOKApK<{ej%>yU z4S2vfywTs@e+v&W7^O{NW<~Z7M35JX67cH_az7P@c;tLfntdEkN-PwnrOF$}(wgug zrz(PYOqR}u2`d}+j$j8Bupb_Bn+t(-P0mMEhh)Fsb7EFc%DLhhKGgLEq9_P8ww2BT z3O@-ctXe|7;;S06r`LaZlLwkB3@~PyCmKX+i64D7_hfTQkE|j5(kC%(nwL|^_g0)9 zc6`eshL3k#UsO0AH=efaz6cEI_%(O9Xf0S*;sKMNEBDj-I*8^fZ0|~Byb}vxy8;{a zRD;;-a}^IkP(Hw14<2pCQaL24zJ@4qw6213zJO@?gx-WQjtgeq7|4Huc6Nil`p&Q! z^aODQ!@t*gqj2wn7(3@-V{e`_=Y@aisNcZ#$us=bKzAbVGxtzQ$NX&Z#_?7gu47cH zCC^Qy_+y8enFa(qI2SPM=fMI#J~$zcaa}v!>g(uiety)cTW5;a(KM?T_!N?{L-_kA zr7uvSFld$E!iO#+FoCbFoW_bnIt`?IPle<#yvuCJO>G@i(M{iaCFgli@mzE{bg2>M zm^HqWYXeckKTP+3Fslr6M~jNWr%KLV%h#c&8H6P88gh>&{RTztx(WwK@x2-8IRz@= zT6{s*WPv|rGp>8fnx(-_K#!NQ;3{Y-|RW!ZpWLX};&V88JfA9y5!_^N( zJ2$2$gy)s<%;wc|BW)a-Efbw8A)A8tS03QtEl=iioieEX3Z>zrFBZ!7ME(($eCdW; zFuTG3%7#3a^qUj)_0voLlWimW1@#J25RRA0IppUGLK+(CYrQPoO{;Rar;fim>r&*rOi)aJ zJ#rD~gc5ZW&58}`qQ*H|K**Pa@WQEVn^1+d2U&$qa}nbx%7+DzQdn}g!|t{V)JRTQ zeUMVNp=yv4I)%VXkP=b_#UmAs)2$C$f&i)B?o6A#4WGacO=pP=^X?mOnzL z(xG1ztrZvV>PrH%HNSAop8!9}H68!@PBIP%qM9RRBKl+OW>h_LHVLxT7phOXL>foQ z-@P0_Gl7McmU-;zVo z2Xep5gkcJ46b{U;1WGCIPJw)uvH#qp!ePkKqq*;_&}rbaG@c}!?CV-Uv}1GTff~#6 zjlItuK{K*6wb1mySqsoPXK%}}Zro`powb6&M1T7ZVL@l6I~1q&3VK0dcI0v9$zz=$ zx#ecFS;{g_9NuFpXBsd)c3~LyQ>3qz2B$C6`DJ0~06}ggOIt>Pabn)UfJX3sg;s24 zB_%plRiI7)6U|tT6ArzR7n4%mIF(v>07_Bi>>@Iwxw~gthI6{WJ`LN&n#D$U&uQd1 zojpGZQ|-*z#YPj%wjdbAN*x_O=BKGrAsaU;iro6O)th`OHTd1+tJMVx>*R=o()t4g z#274DSXT&8)sw>$LI0YzY^pld+^_tzCRZpp_}D1%wyX*rr3~FVyC?RKax6h!-)q3U z=%o%FUXI0hoSEUP_kNM+ z&4z6Ppyl5$T0}K1QQi0=O>y^G>|V~^H_>HV|C$EWZ;!fDU0Kg5n)?+<{AKd^kT}?S zGbWzNid>Aj7c5slB!YQdzj(5lKeav&*&#G{kkPg;S0_Z8$x;Q-;K@T`t0|Ju3Q{Af zWLBUl=-1XsCRQqWCN@O}XuW8@f#T37%0HCLR>L95Q1>AB4zFa2e+PyDo7_nBnaYpGr4|TjaQw}ewX!6{QnO$6UeUaVg6_D>irjLru-j7=GVsn zY|QYqFa*rxaCHbr;!LSp%&>-7YUtN6Vc3N?A-g$L?AH49T;`Vv^w55y{w$7@j6|@Y zNl5djQKn956k9W}E>;HnoOUwh^RlF0tCinC^11FQd%xoG`uRL1^nE`p1d=oKj||_H zA;L@m6m5kp#c?zt-9#*uVgo`4U4x$h5CP{|YmlG~-5u4B6CP4n>!BDZjjDl;+eJh1 zQ~iqG&tw+F=qtO;gm(ASEVk0{Q#_iHaz-^u*lmqER_7-g#v+T@l{4|vN%>1UpfxnR zBL3DH;Sf%>TL5ZA%l818YEhe ziREaC0Y!u5+(#Cl77>MPVX6K10*D#`EAIFG22>~Wa~7x4wv|c!wPgt}_ZtTlsBKi| z$hCDtI#}E+8|ZT4?#lES90O3C>G^7^*7Z=(t@=Nyw1D%WoYrJv(Ao>2*YwQzVW04` z#r~M-w8TR;rhsZ|1*Bwmw-upCeco-jIFn5_E=W+R!n``wVPQ?y;^|A_bLT9LY-!Ei zLqAZIsOw2PcU_+?D!@;a0xJmmKCZ`;tO)B<)TS*qwqL=_c7dfj3GeCGp`@INdkVYR ziB=HSK)^q=31`)4w^K1dlz7*m`M#xad#Uu6bV7It30>UUD@Vo+Z65Icb%sSs%yZQD zD!OLKW}ZCsx2{_9AS6tMzkGLqyKXNWm-41DY~(g1EZ$6040oY>!*5VnC!8dXE3I1QRC^P_nmzYsowjotNn+ zJXD1n5d6>fg&?4A7wM%aNHKj0(xGH{N`KuoCP(=#nL5T)@1(nQM>}|u?xf;+I+bB$ zllkdmjZcO8xQV4|XK-1koMnMFEjL4pmdx~h#y!2?=%zD_uiUyks>=(U@yYXw_Jn(t zjbn4jNQWqZ?Z5zFX!?#dSI`^6!}TN=DSE-1(4gJ-i&?^AlWS=77@*xG{TJ8C)>O3; z%VG6zx!Y*(`R~B{#K3J|Foe&A@IIcGT`k*o{VWn~^fx(^vZiL=4PWO|K%@+s8*GTil;SD@o2&!*DiSBM)eBJ+UdGv5{H;-t2 zqJJK_+Y>VaNmdLlHCkt@pu_m%teqLw!oOLW|MJp(XaRvO*?Mv1oDc5Yb2p7$cx6sg z@Q(a92d7nC2kFU5&Hl4RV~n6Rgi+l5mc6sYCT@hE|M!MCeO865j43WEJYh ztP*;cRpk?C7Q!|g4stalMQxLZDj3BwZEC#9b;Had!9@y*I>u*RsmCL#yW^$ti(PN_ zT9^0A<~>auRaev$G`VN$8&&4ek1w%0zavVRlI1^Z+nJIjr<&AVupZ1q=L=SAt}%Gj z6{AMq2BTRb-uVR4xjg?*RNQ@^!B)|``+s9#QyxIw9Beibd1dTX9yNWL#U}vm60?vh z(o7bJ7IOw3Rv&4y(jrHAnq}9~YLilxBsk*s@+orYHb@|I&}O^H1&g&jnE z*$nKe$dcIJS=s`ElNdiwBG37FI=k`+Oa9S#@PJo$zV@_)YB)Th zv8?=7Sh=Gq{Sau@ir>N>acQ1EMx^ZeJqnaXGJFUMe~XTjXjW-^%_{Kg&PSHr^R=6vEudcf4EHgTWbVkdzpB~!vvK8sqNuXc zB$e4>Q)rI;sgo`@$)_iFKG+yts=5zbi#j&)iM9UHLh%nx@T!TQhSL|j?44CCDGLaM z^9LtdCp?4W*XaB7c-ViyeqfRQX7^bY`Ca%>kXMt38%)R_iD3#p7h1L{JMY~QBG)ug z0x|vmGRI!>=rXDVqg3b1-(Ad8j#B;clxxa5 z^o`kXkpF(PIx?8d+2I;RFc6T#WWjJbK#$u(FJE1xn@lsLbrz14I07>z8XZ@RTw1{s)GX=!N^0%4{rmj{_`&!{++h^p%%mdyWN{<-IAOZyEt)ap0M2?- zSf6_|}ApK-Rc4_8EeIUy=e{n~6=>G|TYp!E782s&2?*BU=~k z-$XPBof#@jdbNdnvD6$!uNk`fF{nEGBZ)oQo0AEgRzV&OOx@Z+zS9jpUQ*%4!s@9} zyr;4q@BVsEMvWapyYX7|nT=v?RZ|%@@yd=7Vg~H&(!w~qLO)$vcOUUuAP9P26q$tG zg&)Bb9}PcQM1B`XEL+bO8`6N_XF=WRa9V)4Kr>h0`%!p-qf&qd&5!gT1ocykF zP&e2J-Kr1j%`6PLxPohW0Zj$@xS`23`^s=LUd04K{{`jCF0Hvpi5+T{+_9)a%;>~G zat#|NjM%xu=F`#=4Aeyppl|?@r9Ah(a%fgXki~VPs?zjwi^0lea&D6seZ8y5a*C(f z>~*%H^=DaCmhV#GC-1-xPe;F!DpPFlcWUR0jq;r2-w#P2{CZ_+c=p2Xn}}D)H-~wf zq-n$T;JH;Q@4|)`#BQRK3lX*&1kqtiN3ML%1<%qI747|JqPl@`GmWip%(m z&o={7zLak$c{4XdfAfcfugh~UzXERH{`B zwcAlKf7wGS*kex7heKz#ZAJ2iJ#CHcV6KlLh-^`gi-}O7^bz!*64w%4aFOD-kOZ#j zxN=LW1`b@p*9XHd%E3}|8d^qOXYZYmI$Nr#@IeJdkvJZ=Zw#OGS*%Nq*@FoT>qfc- zKV=KTctMDdDsicvgnNgUFpJ-TTq2QdJJH0v@n@6@oF{*QHcdqR07EDq8QJ;qUtu#F z4g`chxgmfc*?1Q!`7@RfP~DJ3|60bZCW{_y&j@KPM&$V6*SDEuoJ|gqrRUgezr~8YMq2;q4=A3q3z^fj~Jf-9gneTuskK(XVI3x`)Q7oP_6(k z@b!KU2jb>UYz7@ob&{Bf(nl(#7#2c-qoa?w2V3jvM~*pxPY3!0G{EDmaMwaP2k)20 z=)H&!gDi93vG!{pQ#)^(oV5LA!)?F`Yw+8uET&8A)L2^3U6QU_w&PgZ9LFmSkZQs0 zOeK3rGQoYq2*XR>zF9$u`&osMp1p3Ipn0yxJ3wQi?X*1J>7m7-HHJF9!qL)Mpc|&$ z7L$}efvht}w8-!YbeeEnm^N+Rjpc8$Ds1W2RK|uW)=MZQHPptP6pJ_ztxM!gH!;I6 zP8HVZdhRAVEGop!U_)+o;6-yf+_msz0_6d9rB(l@i}Ma^Vrly@E}Z}gH6er!3P@2v zN~i{;DIf^Ppny`8P!&Pxgh)LE1zdVl550-fLhnUE6jWL$fl#b8D~I}GKF)bxzWryO z=QsE4%r#rCo!ObE)Yb&E($qv!|x zDha<(&^i+vT#veJmR&q79*^~yB#juo>RXgn@@z|K{;Jbi4hFX#Q>LCgF6_(x%wfhk zk@%yq!17gWBxhe6m zu+h~!>qp=9w3k}GahAs}rRv9*u5Sg8%whp`|`{O91b+Xk2PqUz`;_ z{O5Xaw~9Va*A}uE(|FxCq)hLOt-(8lLZGnQaw0v4KLr+6g0%~&rVc^G)E2%vkGz3$ zqdlEhHb^-N8UBsJ8R`nLjul05?>-kiurYfpcyFA_ZvW(O;gxU6f@N-kBPx9KmIzKn zajA`8)?A3Dnc4-1mPx!f*)@@iy*JqL>5J1rOwi&jeKngI%ttrH@fLSvP!4N~ujyc> zX_ZUkS~I@JD!4%N&7wWm>Z+P_m+&6zsz~Ral=oM42d;t@S&W$gB+4MLC__ZYa=Bwo zp~CwO*&>hIVjH-kl{7`zJ9cSnO<3C^PFpoWr!HKyDg4(9)pPjZ$Uf=6qm}dA&#Fd4 zeOecPC^8Hg<+Vael8vi`zE||&qgMqs!Pgz38$yI~74aQ{?N|uaDAHdnjk|`um$g!B zx<^kY#A=hH$aL3wT>ztr2x%bRG-*ykCOL>v0zaWlhqNK)e#!=?h?c2ch|8D<_J;TE z3zmF(9=FYMPvY|`odM9`^2DNb$RwAyu;jLxCi9P-2vkfr7lMsoknJTz z(!>5~xbmUz=a0|u`xDtb>MNL^fUkS9g(g8`Nr^9Vd!(QkO&hgD>#9^=kwNeW4o zJBjR*8a8uHdQ=!_SkJ~N+W65X)I)CT0S=}QN~{d~L)s25Iy&uxw}u3M8oTAsJ0i3<%b`NjKz{dl*?&f=?IVXMDxx4mxK8X3dy2!@-Viy305jZfVXi{t`fP%%3Ey^{&+ z4`#2$!gJE-&*9HwlwuuO4OvK??5BHK^b?pJQ@WzN3`$_g6aAAXSz|ERsACZUvXT5+ zLY>M1sTR2qN42p2NL>i^eSBam3OWmKZWf(8qq8d|vR8^~>;1;<;53>h)hs?|b7TVL zw(eo#))lzNOBO8!MlO8tWW>l;xjoVD6vdjhnR#l^)$Mz!g>Qna>eLMFp$|M(ZpOc zAsbMp_1c+*aCB*15lVYPc-SlERsZIX$j4|IBE#6A=FFF6urvwx3%@$uL(LYOe)73~ zcTgLW9#rl9!91-!?OxOixIk2AuHu&uJsQ<+dZI(ly)P~gq)TQZXDV%*Ms`d(tqotM zXQIx_=ls%9YMc%#(B$n>V^IB)$6%RV}*e`RvASI7WC~JsTsFsEfok% zX`nKs!W_R`eTb$~yzw%9nA+@O)s;jUKeF0x*rE z*>ho0Rbh`Y_Hq69EScklULzX2BN{4R*{75m*XRYZe4zSmTzG8KvfOlPfiU%Fr%}wc zsXxt>GKUrN=s#aWY6-e{b_*$O!uW8lb!HzUCzOQWZnKZiijauaS1KOzGo%o|b!LC)Hv972QWY&#Nd@A=Mk0UM>{h_>`A4c`epgx~nk0q)y2x zBQMB~cswB^l^fp_{YjOz&!w3-uXIOTe4gPiC3A7vIe&lz_X~XJJ(+Cdur!piQ)ih1 zf33Qgn{PO{>Qo$mL0x`MTVQoQK3;dWI3Bw8I9~UbWaFlliBVC|%hD|fgLX>BCJe!}w(s^r%oe+NQE@P)p^_U@w!WdYQiIGCOi?j!1WkP9lr3@Frj0F8pMN#F zElyv!x(a0DlQi$cKegXF#sAi`$$O`l^HZ-jWHd$KW1yDCo|T3G2C9AQ652xe#r#I+ zh2ySIuXr@S$?F?^cr}MN?#SMy7pp69|{Fqdj#JU42>&~=Jnk{sp1B8Xl!{Ze?FLsAcQ+PFDF)`z#2 ziWrT<`&%mB&$G>LZ!xIml9ChA9tY}SllBW3&%kGpXUj+6PM^;{Z>*?)OA)~|dw{N183#zD_F z$mov)2B)t~PMq^J6|jh_x_h@(wBt2X!jin>z|0hpXq@>B#guKe`0%XSYX$$}87rjQqiMlh|HVe~LVXj%rk)9= z(A7_R@n$-)&?C0$v;jF_DQgdg=ttLr-kd(H$Gflf_gTo4KAf{$*XZqrf4AOaKH8n8 zesnkLES0i>35mkT9e>i+xd4)6ApVxwL?8U0TK;VhOD=|p+?li4M(l*~mlwWlj1%I% zbLC7%B=c?pxh&Cswvg@U%zVtiUr&uui8p=EdYC;bbU{+Ln-g0WGoKFT4M^t1KRo|8 z8yxu^V%!_iYOC~flTmVBj1-OtLL}5L?iQChijeKnlC6^NC217V{K~iz_!Ssx&tJ#m9cs)E1jRgi8;tZocfM@m~RcU+++rUM0BVHMWkA z<0C#-le#-#|1Z{5)QCEW96bSeFo6U)KCqPq1{O`jP=`XS>_^M^=g23RGarDzBd$oJ z{u@Mtj!x_!YCp{k(z(t-0pP3Lr9ooWls6KNA8uWiVnh>Z%E2!%JtHNei4X5J^G zQ2+fSLPw{5h-WdQL0Wbk;0Lla>d-9vA&}SN0OSD?b1=|l5(#+!L6b<%LNqBK2V?)I zNIoI#GA+}5iWz)`;{iFQWPw1314$Qn=L#lFSpX_HaCXWD2*rVF)0#l}zIR(0gw4P} z(lioK^VoL)Trvv8&YT9qd}!vYFenWiok0RKw`dY4MHP??+&3jaHwql} z@07=W*fGt2+O?nN6QDsfsEuL()P)|Hj3AWA0itJNs6%79L*+`sY4FZHL2!Zs18ZiH z07Dc_`ZjwCb?9sEP`TQeeMlFySb%}x91`G7pp{X~76g~)WC5NBG*_>P2~>H=Por>D zB!EcySFWI<0qOLAU6TSX8l^ms1f((#WNzC11S$RBOCXkWkjV~G=FtG`5zWOv=4HCH4Ee&F+Fwk!i2{5*UiHlf3rVA7s(xUbJ z`{DnsYo{ChF|0|;$XP-HL%m?b(pf;f4@AB@2Fkx@;Z&wmrt8}O&~@$m-8cUMZ39{l diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8f6e03af5..0f80bbf51 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Mon Jun 08 20:22:21 EEST 2020 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 83f2acfdc..4f906e0c8 100755 --- a/gradlew +++ b/gradlew @@ -82,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -129,6 +130,7 @@ fi if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -154,19 +156,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -175,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 24467a141..ac1b06f93 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -51,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -61,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index 2b5fcd095..ce9c47f32 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -20,7 +20,7 @@ plugins { id 'com.jfrog.artifactory' id 'com.jfrog.bintray' id 'io.morethan.jmhreport' - id 'me.champeau.gradle.jmh' + id 'me.champeau.jmh' id 'io.github.reyerizo.gradle.jcstress' } diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 919a82abb..cc614a0ac 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -41,6 +41,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-params' + testRuntimeOnly 'org.bouncycastle:bcpkix-jdk15on' testRuntimeOnly 'ch.qos.logback:logback-classic' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' testRuntimeOnly 'io.netty:netty-tcnative-boringssl-static' + os_suffix From a3d5ea332e0976a449cfcc9c2d2c501c5d884cde Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 27 Mar 2021 23:58:02 +0200 Subject: [PATCH 036/116] adds ReconnectMono stress tests Signed-off-by: Oleh Dokuka --- .../rsocket/core/ReconnectMonoStressTest.java | 599 ++++++++++++++++++ .../io/rsocket/core/ReconnectMonoTests.java | 12 +- .../rsocket/core/ResolvingOperatorTests.java | 336 ++++++---- 3 files changed, 809 insertions(+), 138 deletions(-) create mode 100644 rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java new file mode 100644 index 000000000..e01b1d704 --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java @@ -0,0 +1,599 @@ +/* + * Copyright 2015-Present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import static io.rsocket.core.ResolvingOperator.EMPTY_SUBSCRIBED; +import static io.rsocket.core.ResolvingOperator.EMPTY_UNSUBSCRIBED; +import static io.rsocket.core.ResolvingOperator.READY; +import static io.rsocket.core.ResolvingOperator.TERMINATED; +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; + +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.function.BiConsumer; +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.IIIIIII_Result; +import org.openjdk.jcstress.infra.results.IIIIII_Result; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; + +public abstract class ReconnectMonoStressTest { + + abstract static class BaseStressTest { + + final StressSubscription stressSubscription = new StressSubscription<>(); + + final Mono source = source(); + + final StressSubscriber stressSubscriber = new StressSubscriber<>(); + + volatile int onValueExpire; + + static final AtomicIntegerFieldUpdater ON_VALUE_EXPIRE = + AtomicIntegerFieldUpdater.newUpdater(BaseStressTest.class, "onValueExpire"); + + volatile int onValueReceived; + + static final AtomicIntegerFieldUpdater ON_VALUE_RECEIVED = + AtomicIntegerFieldUpdater.newUpdater(BaseStressTest.class, "onValueReceived"); + final ReconnectMono reconnectMono = + new ReconnectMono<>( + source, + (__) -> ON_VALUE_EXPIRE.incrementAndGet(BaseStressTest.this), + (__, ___) -> ON_VALUE_RECEIVED.incrementAndGet(BaseStressTest.this)); + + abstract Mono source(); + + int state() { + final BiConsumer[] subscribers = reconnectMono.resolvingInner.subscribers; + if (subscribers == EMPTY_UNSUBSCRIBED) { + return 0; + } else if (subscribers == EMPTY_SUBSCRIBED) { + return 1; + } else if (subscribers == READY) { + return 2; + } else if (subscribers == TERMINATED) { + return 3; + } else { + return 4; + } + } + } + + @JCStressTest + @Outcome( + id = {"1, 0, 0, 1, 1, 0, 3"}, + expect = ACCEPTABLE, + desc = "Disposed before value is delivered") + @Outcome( + id = {"0, 0, 0, 1, 1, 0, 3"}, + expect = ACCEPTABLE, + desc = "Disposed after onComplete but before value is delivered") + @Outcome( + id = {"0, 1, 1, 0, 1, 1, 3"}, + expect = ACCEPTABLE, + desc = "Disposed after value is delivered") + @State + public static class ExpireValueOnRacingDisposeAndNext extends BaseStressTest { + + { + reconnectMono.subscribe(stressSubscriber); + } + + @Override + Mono source() { + return new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + stressSubscription.subscribe(actual); + } + }; + } + + @Actor + void sendNext() { + stressSubscription.actual.onNext("value"); + stressSubscription.actual.onComplete(); + } + + @Actor + void dispose() { + reconnectMono.dispose(); + } + + @Arbiter + public void arbiter(IIIIIII_Result r) { + r.r1 = stressSubscription.cancelled ? 1 : 0; + r.r2 = stressSubscriber.onNextCalls; + r.r3 = stressSubscriber.onCompleteCalls; + r.r4 = stressSubscriber.onErrorCalls; + r.r5 = onValueExpire; + r.r6 = onValueReceived; + r.r7 = state(); + } + } + + @JCStressTest + @Outcome( + id = {"1, 0, 0, 1, 1, 0, 3"}, + expect = ACCEPTABLE, + desc = "Disposed before error is delivered") + @Outcome( + id = {"0, 0, 0, 1, 1, 0, 3"}, + expect = ACCEPTABLE, + desc = "Disposed after onError") + @State + public static class ExpireValueOnRacingDisposeAndError extends BaseStressTest { + + { + Hooks.onErrorDropped(t -> {}); + reconnectMono.subscribe(stressSubscriber); + } + + @Override + Mono source() { + return new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + stressSubscription.subscribe(actual); + } + }; + } + + @Actor + void sendNext() { + stressSubscription.actual.onNext("value"); + stressSubscription.actual.onError(new RuntimeException("boom")); + } + + @Actor + void dispose() { + reconnectMono.dispose(); + } + + @Arbiter + public void arbiter(IIIIIII_Result r) { + Hooks.resetOnErrorDropped(); + + r.r1 = stressSubscription.cancelled ? 1 : 0; + r.r2 = stressSubscriber.onNextCalls; + r.r3 = stressSubscriber.onCompleteCalls; + r.r4 = stressSubscriber.onErrorCalls; + r.r5 = onValueExpire; + r.r6 = onValueReceived; + r.r7 = state(); + } + } + + @JCStressTest + @Outcome( + id = {"0, 1, 1, 0, 0, 1, 2"}, + expect = ACCEPTABLE, + desc = "Invalidate happens before value is delivered") + @Outcome( + id = {"0, 1, 1, 0, 1, 1, 0"}, + expect = ACCEPTABLE, + desc = "Invalidate happens after value is delivered") + @State + public static class ExpireValueOnRacingInvalidateAndNextComplete extends BaseStressTest { + + { + reconnectMono.subscribe(stressSubscriber); + } + + @Override + Mono source() { + return new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + stressSubscription.subscribe(actual); + } + }; + } + + @Actor + void sendNext() { + stressSubscription.actual.onNext("value"); + stressSubscription.actual.onComplete(); + } + + @Actor + void invalidate() { + reconnectMono.invalidate(); + } + + @Arbiter + public void arbiter(IIIIIII_Result r) { + r.r1 = stressSubscription.cancelled ? 1 : 0; + r.r2 = stressSubscriber.onNextCalls; + r.r3 = stressSubscriber.onCompleteCalls; + r.r4 = stressSubscriber.onErrorCalls; + r.r5 = onValueExpire; + r.r6 = onValueReceived; + r.r7 = state(); + } + } + + @JCStressTest + @Outcome( + id = {"0, 1, 1, 0, 1, 1, 0"}, + expect = ACCEPTABLE) + @State + public static class ExpireValueOnceOnRacingInvalidateAndInvalidate extends BaseStressTest { + + { + reconnectMono.subscribe(stressSubscriber); + } + + @Override + Mono source() { + return new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + stressSubscription.subscribe(actual); + stressSubscription.actual.onNext("value"); + stressSubscription.actual.onComplete(); + } + }; + } + + @Actor + void invalidate1() { + reconnectMono.invalidate(); + } + + @Actor + void invalidate2() { + reconnectMono.invalidate(); + } + + @Arbiter + public void arbiter(IIIIIII_Result r) { + r.r1 = stressSubscription.cancelled ? 1 : 0; + r.r2 = stressSubscriber.onNextCalls; + r.r3 = stressSubscriber.onCompleteCalls; + r.r4 = stressSubscriber.onErrorCalls; + r.r5 = onValueExpire; + r.r6 = onValueReceived; + r.r7 = state(); + } + } + + @JCStressTest + @Outcome( + id = {"0, 1, 1, 0, 1, 1, 3"}, + expect = ACCEPTABLE) + @State + public static class ExpireValueOnceOnRacingInvalidateAndDispose extends BaseStressTest { + + { + reconnectMono.subscribe(stressSubscriber); + } + + @Override + Mono source() { + return new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + stressSubscription.subscribe(actual); + stressSubscription.actual.onNext("value"); + stressSubscription.actual.onComplete(); + } + }; + } + + @Actor + void invalidate() { + reconnectMono.invalidate(); + } + + @Actor + void dispose() { + reconnectMono.dispose(); + } + + @Arbiter + public void arbiter(IIIIIII_Result r) { + r.r1 = stressSubscription.cancelled ? 1 : 0; + r.r2 = stressSubscriber.onNextCalls; + r.r3 = stressSubscriber.onCompleteCalls; + r.r4 = stressSubscriber.onErrorCalls; + r.r5 = onValueExpire; + r.r6 = onValueReceived; + r.r7 = state(); + } + } + + @JCStressTest + @Outcome( + id = {"1, 0, 2, 2, 0, 1"}, + expect = ACCEPTABLE) + @State + public static class DeliversValueToAllSubscribersUnderRace extends BaseStressTest { + + final StressSubscriber stressSubscriber2 = new StressSubscriber<>(); + + { + reconnectMono.subscribe(stressSubscriber); + } + + @Override + Mono source() { + return new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + stressSubscription.subscribe(actual); + } + }; + } + + @Actor + void sendNextAndComplete() { + stressSubscription.actual.onNext("value"); + stressSubscription.actual.onComplete(); + } + + @Actor + void secondSubscribe() { + reconnectMono.subscribe(stressSubscriber2); + } + + @Arbiter + public void arbiter(IIIIII_Result r) { + r.r1 = stressSubscription.requestsCount; + r.r2 = stressSubscription.cancelled ? 1 : 0; + r.r3 = stressSubscriber.onNextCalls + stressSubscriber2.onNextCalls; + r.r4 = stressSubscriber.onCompleteCalls + stressSubscriber2.onCompleteCalls; + r.r5 = onValueExpire; + r.r6 = onValueReceived; + } + } + + @JCStressTest + @Outcome( + id = {"2, 0, 1, 1, 1, 1, 4"}, + expect = ACCEPTABLE, + desc = "Second Subscriber subscribed after invalidate") + @Outcome( + id = {"1, 0, 2, 2, 1, 1, 0"}, + expect = ACCEPTABLE, + desc = "Second Subscriber subscribed before invalidate and received value") + @State + public static class InvalidateAndSubscribeUnderRace extends BaseStressTest { + + final StressSubscriber stressSubscriber2 = new StressSubscriber<>(); + + { + reconnectMono.subscribe(stressSubscriber); + stressSubscription.actual.onNext("value"); + stressSubscription.actual.onComplete(); + } + + @Override + Mono source() { + return new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + stressSubscription.subscribe(actual); + } + }; + } + + @Actor + void invalidate() { + reconnectMono.invalidate(); + } + + @Actor + void secondSubscribe() { + reconnectMono.subscribe(stressSubscriber2); + } + + @Arbiter + public void arbiter(IIIIIII_Result r) { + r.r1 = stressSubscription.subscribes; + r.r2 = stressSubscription.cancelled ? 1 : 0; + r.r3 = stressSubscriber.onNextCalls + stressSubscriber2.onNextCalls; + r.r4 = stressSubscriber.onCompleteCalls + stressSubscriber2.onCompleteCalls; + r.r5 = onValueExpire; + r.r6 = onValueReceived; + r.r7 = state(); + } + } + + @JCStressTest + @Outcome( + id = {"2, 0, 2, 1, 2, 2"}, + expect = ACCEPTABLE, + desc = "Subscribed again after invalidate") + @Outcome( + id = {"1, 0, 1, 1, 1, 0"}, + expect = ACCEPTABLE, + desc = "Subscribed before invalidate") + @State + public static class InvalidateAndBlockUnderRace extends BaseStressTest { + + String receivedValue; + + { + reconnectMono.subscribe(stressSubscriber); + } + + @Override + Mono source() { + return new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + stressSubscription.subscribe(actual); + actual.onNext("value" + stressSubscription.subscribes); + actual.onComplete(); + } + }; + } + + @Actor + void invalidate() { + reconnectMono.invalidate(); + } + + @Actor + void secondSubscribe() { + receivedValue = reconnectMono.block(); + } + + @Arbiter + public void arbiter(IIIIII_Result r) { + r.r1 = stressSubscription.subscribes; + r.r2 = stressSubscription.cancelled ? 1 : 0; + r.r3 = receivedValue.equals("value1") ? 1 : receivedValue.equals("value2") ? 2 : -1; + r.r4 = onValueExpire; + r.r5 = onValueReceived; + r.r6 = state(); + } + } + + @JCStressTest + @Outcome( + id = {"1, 0, 1, 0, 1, 2"}, + expect = ACCEPTABLE) + @State + public static class TwoSubscribesRace extends BaseStressTest { + + StressSubscriber stressSubscriber2 = new StressSubscriber<>(); + + @Override + Mono source() { + return new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + stressSubscription.subscribe(actual); + actual.onNext("value" + stressSubscription.subscribes); + actual.onComplete(); + } + }; + } + + @Actor + void subscribe1() { + reconnectMono.subscribe(stressSubscriber); + } + + @Actor + void subscribe2() { + reconnectMono.subscribe(stressSubscriber2); + } + + @Arbiter + public void arbiter(IIIIII_Result r) { + r.r1 = stressSubscription.subscribes; + r.r2 = stressSubscription.cancelled ? 1 : 0; + r.r3 = stressSubscriber.values.get(0).equals(stressSubscriber2.values.get(0)) ? 1 : 2; + r.r4 = onValueExpire; + r.r5 = onValueReceived; + r.r6 = state(); + } + } + + @JCStressTest + @Outcome( + id = {"1, 0, 1, 0, 1, 2"}, + expect = ACCEPTABLE) + @State + public static class SubscribeBlockRace extends BaseStressTest { + + String receivedValue; + + @Override + Mono source() { + return new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + stressSubscription.subscribe(actual); + actual.onNext("value" + stressSubscription.subscribes); + actual.onComplete(); + } + }; + } + + @Actor + void block() { + receivedValue = reconnectMono.block(); + } + + @Actor + void subscribe() { + reconnectMono.subscribe(stressSubscriber); + } + + @Arbiter + public void arbiter(IIIIII_Result r) { + r.r1 = stressSubscription.subscribes; + r.r2 = stressSubscription.cancelled ? 1 : 0; + r.r3 = receivedValue.equals(stressSubscriber.values.get(0)) ? 1 : 2; + r.r4 = onValueExpire; + r.r5 = onValueReceived; + r.r6 = state(); + } + } + + @JCStressTest + @Outcome( + id = {"1, 0, 1, 0, 1, 2"}, + expect = ACCEPTABLE) + @State + public static class TwoBlocksRace extends BaseStressTest { + + String receivedValue1; + String receivedValue2; + + @Override + Mono source() { + return new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + stressSubscription.subscribe(actual); + actual.onNext("value" + stressSubscription.subscribes); + actual.onComplete(); + } + }; + } + + @Actor + void block1() { + receivedValue1 = reconnectMono.block(); + } + + @Actor + void block2() { + receivedValue2 = reconnectMono.block(); + } + + @Arbiter + public void arbiter(IIIIII_Result r) { + r.r1 = stressSubscription.subscribes; + r.r2 = stressSubscription.cancelled ? 1 : 0; + r.r3 = receivedValue1.equals(receivedValue2) ? 1 : 2; + r.r4 = onValueExpire; + r.r5 = onValueReceived; + r.r6 = state(); + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java index 0ec058af2..4aaa2bf52 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java @@ -918,10 +918,14 @@ public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidates() { for (int i = 0; i < 10000; i++) { final TestPublisher cold = TestPublisher.createCold(); cold.next("value"); + cold.complete(); final int timeout = 10; final ReconnectMono reconnectMono = - cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + cold.flux() + .takeLast(1) + .next() + .as(source -> new ReconnectMono<>(source, onExpire(), onValue())); StepVerifier.create(reconnectMono.subscribeOn(Schedulers.boundedElastic())) .expectSubscription() @@ -937,16 +941,18 @@ public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidates() { Assertions.assertThat(expired).hasSize(1).containsOnly("value"); Assertions.assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); + cold.next("value2"); + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.boundedElastic())) .expectSubscription() - .expectNext("value") + .expectNext("value2") .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)); + .containsOnly(Tuples.of("value", reconnectMono), Tuples.of("value2", reconnectMono)); Assertions.assertThat(cold.subscribeCount()).isEqualTo(2); diff --git a/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java b/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java index 608e1a336..1cd08fd67 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java @@ -36,30 +36,31 @@ import org.mockito.Mockito; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; import reactor.core.publisher.Hooks; -import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Operators; +import reactor.core.publisher.Sinks; import reactor.test.StepVerifier; import reactor.test.util.RaceTestUtils; -import reactor.util.retry.Retry; public class ResolvingOperatorTests { - private Queue retries = new ConcurrentLinkedQueue<>(); - @Test public void shouldExpireValueOnRacingDisposeAndComplete() { for (int i = 0; i < 10000; i++) { final int index = i; - MonoProcessor processor = MonoProcessor.create(); + AssertSubscriber subscriber = AssertSubscriber.create(); + subscriber.onSubscribe(Operators.emptySubscription()); BiConsumer consumer = (v, t) -> { if (t != null) { - processor.onError(t); + subscriber.onError(t); return; } - processor.onNext(v); + subscriber.onNext(v); + subscriber.onComplete(); }; ResolvingTest.create() @@ -75,13 +76,15 @@ public void shouldExpireValueOnRacingDisposeAndComplete() { .ifResolvedAssertEqual("value" + index) .assertIsDisposed(); - if (processor.isError()) { - Assertions.assertThat(processor.getError()) + subscriber.assertTerminated(); + + if (!subscriber.errors().isEmpty()) { + Assertions.assertThat(subscriber.errors().get(0)) .isInstanceOf(CancellationException.class) .hasMessage("Disposed"); } else { - Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + Assertions.assertThat(subscriber.values()).containsExactly("value" + i); } } } @@ -91,26 +94,30 @@ public void shouldNotifyAllTheSubscribersUnderRacingBetweenSubscribeAndComplete( for (int i = 0; i < 10000; i++) { final String valueToSend = "value" + i; - MonoProcessor processor = MonoProcessor.create(); + AssertSubscriber subscriber = AssertSubscriber.create(); + subscriber.onSubscribe(Operators.emptySubscription()); BiConsumer consumer = (v, t) -> { if (t != null) { - processor.onError(t); + subscriber.onError(t); return; } - processor.onNext(v); + subscriber.onNext(v); + subscriber.onComplete(); }; - MonoProcessor processor2 = MonoProcessor.create(); + AssertSubscriber subscriber2 = AssertSubscriber.create(); + subscriber2.onSubscribe(Operators.emptySubscription()); BiConsumer consumer2 = (v, t) -> { if (t != null) { - processor2.onError(t); + subscriber2.onError(t); return; } - processor2.onNext(v); + subscriber2.onNext(v); + subscriber2.onComplete(); }; ResolvingTest.create() @@ -122,10 +129,7 @@ public void shouldNotifyAllTheSubscribersUnderRacingBetweenSubscribeAndComplete( self -> { RaceTestUtils.race(() -> self.complete(valueToSend), () -> self.observe(consumer)); - StepVerifier.create(processor) - .expectNext(valueToSend) - .expectComplete() - .verify(Duration.ofMillis(10)); + subscriber.await(Duration.ofMillis(10)).assertValues(valueToSend).assertComplete(); }) .assertDisposeCalled(0) .assertReceivedExactly(valueToSend) @@ -133,10 +137,7 @@ public void shouldNotifyAllTheSubscribersUnderRacingBetweenSubscribeAndComplete( .thenAddObserver(consumer2) .assertPendingSubscribers(0); - StepVerifier.create(processor2) - .expectNext(valueToSend) - .expectComplete() - .verify(Duration.ofMillis(10)); + subscriber2.await(Duration.ofMillis(10)).assertValues(valueToSend).assertComplete(); } } @@ -146,26 +147,30 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() final String valueToSend = "value" + i; final String valueToSend2 = "value2" + i; - MonoProcessor processor = MonoProcessor.create(); + AssertSubscriber subscriber = AssertSubscriber.create(); + subscriber.onSubscribe(Operators.emptySubscription()); BiConsumer consumer = (v, t) -> { if (t != null) { - processor.onError(t); + subscriber.onError(t); return; } - processor.onNext(v); + subscriber.onNext(v); + subscriber.onComplete(); }; - MonoProcessor processor2 = MonoProcessor.create(); + AssertSubscriber subscriber2 = AssertSubscriber.create(); + subscriber2.onSubscribe(Operators.emptySubscription()); BiConsumer consumer2 = (v, t) -> { if (t != null) { - processor2.onError(t); + subscriber2.onError(t); return; } - processor2.onNext(v); + subscriber2.onNext(v); + subscriber2.onComplete(); }; ResolvingTest.create() @@ -178,10 +183,7 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() self -> { self.complete(valueToSend); - StepVerifier.create(processor) - .expectNext(valueToSend) - .expectComplete() - .verify(Duration.ofMillis(10)); + subscriber.await(Duration.ofMillis(10)).assertValues(valueToSend).assertComplete(); }) .assertReceivedExactly(valueToSend) .then( @@ -190,7 +192,7 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() self::invalidate, () -> { self.observe(consumer2); - if (!processor2.isTerminated()) { + if (!subscriber2.isTerminated()) { self.complete(valueToSend2); } })) @@ -207,17 +209,18 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() .assertDisposeCalled(0) .then( self -> - StepVerifier.create(processor2) - .expectNextMatches( - (v) -> { + subscriber2 + .await(Duration.ofMillis(100)) + .assertValueCount(1) + .assertValuesWith( + v -> { if (self.subscribers == ResolvingOperator.READY) { - return v.equals(valueToSend2); + Assertions.assertThat(v).isEqualTo(valueToSend2); } else { - return v.equals(valueToSend); + Assertions.assertThat(v).isEqualTo(valueToSend); } }) - .expectComplete() - .verify(Duration.ofMillis(100))); + .assertComplete()); } } @@ -227,26 +230,30 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates( final String valueToSend = "value" + i; final String valueToSend2 = "value_to_possibly_expire" + i; - MonoProcessor processor = MonoProcessor.create(); + AssertSubscriber subscriber = AssertSubscriber.create(); + subscriber.onSubscribe(Operators.emptySubscription()); BiConsumer consumer = (v, t) -> { if (t != null) { - processor.onError(t); + subscriber.onError(t); return; } - processor.onNext(v); + subscriber.onNext(v); + subscriber.onComplete(); }; - MonoProcessor processor2 = MonoProcessor.create(); + AssertSubscriber subscriber2 = AssertSubscriber.create(); + subscriber2.onSubscribe(Operators.emptySubscription()); BiConsumer consumer2 = (v, t) -> { if (t != null) { - processor2.onError(t); + subscriber2.onError(t); return; } - processor2.onNext(v); + subscriber2.onNext(v); + subscriber2.onComplete(); }; ResolvingTest.create() @@ -259,10 +266,7 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates( self -> { self.complete(valueToSend); - StepVerifier.create(processor) - .expectNext(valueToSend) - .expectComplete() - .verify(Duration.ofMillis(10)); + subscriber.await(Duration.ofMillis(100)).assertValues(valueToSend).assertComplete(); }) .assertReceivedExactly(valueToSend) .then( @@ -272,7 +276,7 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates( self::invalidate, () -> { self.observe(consumer2); - if (!processor2.isTerminated()) { + if (!subscriber2.isTerminated()) { self.complete(valueToSend2); } })) @@ -292,7 +296,7 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates( .haveAtMost( 2, new Condition<>( - new Predicate() { + new Predicate() { int time = 0; @Override @@ -309,22 +313,19 @@ public boolean test(Object s) { .assertPendingSubscribers(0) .assertDisposeCalled(0) .then( - new Consumer>() { - @Override - public void accept(ResolvingTest self) { - StepVerifier.create(processor2) - .expectNextMatches( - (v) -> { + self -> + subscriber2 + .await(Duration.ofMillis(100)) + .assertValueCount(1) + .assertValuesWith( + v -> { if (self.subscribers == ResolvingOperator.READY) { - return v.equals(valueToSend2); + Assertions.assertThat(v).isEqualTo(valueToSend2); } else { - return v.equals(valueToSend) || v.equals(valueToSend2); + Assertions.assertThat(v).isIn(valueToSend, valueToSend2); } }) - .expectComplete() - .verify(Duration.ofMillis(100)); - } - }); + .assertComplete()); } } @@ -334,15 +335,17 @@ public void shouldNotExpireNewlyResolvedValueIfBlockIsRacingWithInvalidate() { final String valueToSend = "value" + i; final String valueToSend2 = "value2" + i; - MonoProcessor processor = MonoProcessor.create(); + AssertSubscriber subscriber = AssertSubscriber.create(); + subscriber.onSubscribe(Operators.emptySubscription()); BiConsumer consumer = (v, t) -> { if (t != null) { - processor.onError(t); + subscriber.onError(t); return; } - processor.onNext(v); + subscriber.onNext(v); + subscriber.onComplete(); }; ResolvingTest.create() @@ -355,10 +358,7 @@ public void shouldNotExpireNewlyResolvedValueIfBlockIsRacingWithInvalidate() { self -> { self.complete(valueToSend); - StepVerifier.create(processor) - .expectNext(valueToSend) - .expectComplete() - .verify(Duration.ofMillis(10)); + subscriber.await(Duration.ofMillis(10)).assertValues(valueToSend).assertComplete(); }) .assertReceivedExactly(valueToSend) .then( @@ -395,26 +395,30 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribers() { for (int i = 0; i < 10000; i++) { final String valueToSend = "value" + i; - MonoProcessor processor = MonoProcessor.create(); + AssertSubscriber subscriber = AssertSubscriber.create(); + subscriber.onSubscribe(Operators.emptySubscription()); BiConsumer consumer = (v, t) -> { if (t != null) { - processor.onError(t); + subscriber.onError(t); return; } - processor.onNext(v); + subscriber.onNext(v); + subscriber.onComplete(); }; - MonoProcessor processor2 = MonoProcessor.create(); + AssertSubscriber subscriber2 = AssertSubscriber.create(); + subscriber2.onSubscribe(Operators.emptySubscription()); BiConsumer consumer2 = (v, t) -> { if (t != null) { - processor2.onError(t); + subscriber2.onError(t); return; } - processor2.onNext(v); + subscriber2.onNext(v); + subscriber2.onComplete(); }; ResolvingTest.create() @@ -434,11 +438,11 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribers() { .assertDisposeCalled(0) .then( self -> { - Assertions.assertThat(processor.isTerminated()).isTrue(); - Assertions.assertThat(processor2.isTerminated()).isTrue(); + Assertions.assertThat(subscriber.isTerminated()).isTrue(); + Assertions.assertThat(subscriber2.isTerminated()).isTrue(); - Assertions.assertThat(processor.peek()).isEqualTo(valueToSend); - Assertions.assertThat(processor2.peek()).isEqualTo(valueToSend); + Assertions.assertThat(subscriber.values()).containsExactly(valueToSend); + Assertions.assertThat(subscriber2.values()).containsExactly(valueToSend); Assertions.assertThat(self.subscribers).isEqualTo(ResolvingOperator.READY); @@ -452,17 +456,20 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { for (int i = 0; i < 10000; i++) { final String valueToSend = "value" + i; - MonoProcessor processor = MonoProcessor.create(); + AssertSubscriber subscriber = AssertSubscriber.create(); + subscriber.onSubscribe(Operators.emptySubscription()); - MonoProcessor processor2 = MonoProcessor.create(); + AssertSubscriber subscriber2 = AssertSubscriber.create(); + subscriber2.onSubscribe(Operators.emptySubscription()); BiConsumer consumer2 = (v, t) -> { if (t != null) { - processor2.onError(t); + subscriber2.onError(t); return; } - processor2.onNext(v); + subscriber2.onNext(v); + subscriber2.onComplete(); }; ResolvingTest.create() @@ -474,7 +481,11 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { .then( self -> RaceTestUtils.race( - () -> processor.onNext(self.block(null)), () -> self.observe(consumer2))) + () -> { + subscriber.onNext(self.block(null)); + subscriber.onComplete(); + }, + () -> self.observe(consumer2))) .assertSubscribeCalled(1) .assertPendingSubscribers(0) .assertReceivedExactly(valueToSend) @@ -482,11 +493,11 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { .assertDisposeCalled(0) .then( self -> { - Assertions.assertThat(processor.isTerminated()).isTrue(); - Assertions.assertThat(processor2.isTerminated()).isTrue(); + Assertions.assertThat(subscriber.isTerminated()).isTrue(); + Assertions.assertThat(subscriber2.isTerminated()).isTrue(); - Assertions.assertThat(processor.peek()).isEqualTo(valueToSend); - Assertions.assertThat(processor2.peek()).isEqualTo(valueToSend); + Assertions.assertThat(subscriber.values()).containsExactly(valueToSend); + Assertions.assertThat(subscriber2.values()).containsExactly(valueToSend); Assertions.assertThat(self.subscribers).isEqualTo(ResolvingOperator.READY); @@ -501,8 +512,11 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenBlocks() { for (int i = 0; i < 10000; i++) { final String valueToSend = "value" + i; - MonoProcessor processor = MonoProcessor.create(); - MonoProcessor processor2 = MonoProcessor.create(); + AssertSubscriber subscriber = AssertSubscriber.create(); + subscriber.onSubscribe(Operators.emptySubscription()); + + AssertSubscriber subscriber2 = AssertSubscriber.create(); + subscriber2.onSubscribe(Operators.emptySubscription()); ResolvingTest.create() .assertNothingExpired() @@ -513,8 +527,14 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenBlocks() { .then( self -> RaceTestUtils.race( - () -> processor.onNext(self.block(timeout)), - () -> processor2.onNext(self.block(timeout)))) + () -> { + subscriber.onNext(self.block(timeout)); + subscriber.onComplete(); + }, + () -> { + subscriber2.onNext(self.block(timeout)); + subscriber2.onComplete(); + })) .assertSubscribeCalled(1) .assertPendingSubscribers(0) .assertReceivedExactly(valueToSend) @@ -522,11 +542,11 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenBlocks() { .assertDisposeCalled(0) .then( self -> { - Assertions.assertThat(processor.isTerminated()).isTrue(); - Assertions.assertThat(processor2.isTerminated()).isTrue(); + Assertions.assertThat(subscriber.isTerminated()).isTrue(); + Assertions.assertThat(subscriber2.isTerminated()).isTrue(); - Assertions.assertThat(processor.peek()).isEqualTo(valueToSend); - Assertions.assertThat(processor2.peek()).isEqualTo(valueToSend); + Assertions.assertThat(subscriber.values()).containsExactly(valueToSend); + Assertions.assertThat(subscriber2.values()).containsExactly(valueToSend); Assertions.assertThat(self.subscribers).isEqualTo(ResolvingOperator.READY); @@ -541,25 +561,30 @@ public void shouldExpireValueOnRacingDisposeAndError() { Hooks.onErrorDropped(t -> {}); RuntimeException runtimeException = new RuntimeException("test"); for (int i = 0; i < 10000; i++) { - MonoProcessor processor = MonoProcessor.create(); + AssertSubscriber subscriber = AssertSubscriber.create(); + subscriber.onSubscribe(Operators.emptySubscription()); BiConsumer consumer = (v, t) -> { if (t != null) { - processor.onError(t); + subscriber.onError(t); return; } - processor.onNext(v); + subscriber.onNext(v); + subscriber.onComplete(); }; - MonoProcessor processor2 = MonoProcessor.create(); + + AssertSubscriber subscriber2 = AssertSubscriber.create(); + subscriber2.onSubscribe(Operators.emptySubscription()); BiConsumer consumer2 = (v, t) -> { if (t != null) { - processor2.onError(t); + subscriber2.onError(t); return; } - processor2.onNext(v); + subscriber2.onNext(v); + subscriber2.onComplete(); }; ResolvingTest.create() @@ -583,8 +608,9 @@ public void shouldExpireValueOnRacingDisposeAndError() { }) .thenAddObserver(consumer2); - StepVerifier.create(processor) - .expectErrorSatisfies( + subscriber + .await(Duration.ofMillis(10)) + .assertErrorWith( t -> { if (t instanceof CancellationException) { Assertions.assertThat(t) @@ -593,11 +619,11 @@ public void shouldExpireValueOnRacingDisposeAndError() { } else { Assertions.assertThat(t).isInstanceOf(RuntimeException.class).hasMessage("test"); } - }) - .verify(Duration.ofMillis(10)); + }); - StepVerifier.create(processor2) - .expectErrorSatisfies( + subscriber2 + .await(Duration.ofMillis(10)) + .assertErrorWith( t -> { if (t instanceof CancellationException) { Assertions.assertThat(t) @@ -606,8 +632,7 @@ public void shouldExpireValueOnRacingDisposeAndError() { } else { Assertions.assertThat(t).isInstanceOf(RuntimeException.class).hasMessage("test"); } - }) - .verify(Duration.ofMillis(10)); + }); // no way to guarantee equality because of racing // Assertions.assertThat(processor.getError()) @@ -656,9 +681,10 @@ public void shouldThrowOnBlockingIfHasAlreadyTerminated() { static Stream, Publisher>> innerCases() { return Stream.of( (self) -> { - final MonoProcessor processor = MonoProcessor.create(); + final Sinks.One processor = Sinks.unsafe().one(); final ResolvingOperator.DeferredResolution operator = - new ResolvingOperator.DeferredResolution(self, processor) { + new ResolvingOperator.DeferredResolution( + self, new SinkOneSubscriber(processor)) { @Override public void accept(String v, Throwable t) { if (t != null) { @@ -669,14 +695,21 @@ public void accept(String v, Throwable t) { onNext(v); } }; - return processor.doOnSubscribe(s -> self.observe(operator)).doOnCancel(operator::cancel); + return processor + .asMono() + .doOnSubscribe(s -> self.observe(operator)) + .doOnCancel(operator::cancel); }, (self) -> { - final MonoProcessor processor = MonoProcessor.create(); + final Sinks.One processor = Sinks.unsafe().one(); + final SinkOneSubscriber subscriber = new SinkOneSubscriber(processor); final ResolvingOperator.MonoDeferredResolutionOperator operator = - new ResolvingOperator.MonoDeferredResolutionOperator<>(self, processor); - processor.onSubscribe(operator); - return processor.doOnSubscribe(s -> self.observe(operator)).doOnCancel(operator::cancel); + new ResolvingOperator.MonoDeferredResolutionOperator<>(self, subscriber); + subscriber.onSubscribe(operator); + return processor + .asMono() + .doOnSubscribe(s -> self.observe(operator)) + .doOnCancel(operator::cancel); }); } @@ -729,12 +762,12 @@ public void shouldExpireValueOnDispose( public void shouldNotifyAllTheSubscribers( Function, Publisher> caseProducer) { - final MonoProcessor sub1 = MonoProcessor.create(); - final MonoProcessor sub2 = MonoProcessor.create(); - final MonoProcessor sub3 = MonoProcessor.create(); - final MonoProcessor sub4 = MonoProcessor.create(); + AssertSubscriber sub1 = AssertSubscriber.create(); + AssertSubscriber sub2 = AssertSubscriber.create(); + AssertSubscriber sub3 = AssertSubscriber.create(); + AssertSubscriber sub4 = AssertSubscriber.create(); - final ArrayList> processors = new ArrayList<>(200); + final ArrayList> processors = new ArrayList<>(200); ResolvingTest.create() .assertDisposeCalled(0) @@ -754,8 +787,8 @@ public void shouldNotifyAllTheSubscribers( .then( self -> { for (int i = 0; i < 100; i++) { - final MonoProcessor subA = MonoProcessor.create(); - final MonoProcessor subB = MonoProcessor.create(); + AssertSubscriber subA = AssertSubscriber.create(); + AssertSubscriber subB = AssertSubscriber.create(); processors.add(subA); processors.add(subB); RaceTestUtils.race( @@ -765,20 +798,20 @@ public void shouldNotifyAllTheSubscribers( }) .assertSubscribeCalled(1) .assertPendingSubscribers(204) - .then(self -> sub1.dispose()) + .then(self -> sub1.cancel()) .assertPendingSubscribers(203) .then( self -> { String valueToSend = "value"; self.complete(valueToSend); - Assertions.assertThatThrownBy(sub1::peek).isInstanceOf(CancellationException.class); - Assertions.assertThat(sub2.peek()).isEqualTo(valueToSend); - Assertions.assertThat(sub3.peek()).isEqualTo(valueToSend); - Assertions.assertThat(sub4.peek()).isEqualTo(valueToSend); + Assertions.assertThat(sub1.isTerminated()).isFalse(); + Assertions.assertThat(sub2.values()).containsExactly(valueToSend); + Assertions.assertThat(sub3.values()).containsExactly(valueToSend); + Assertions.assertThat(sub4.values()).containsExactly(valueToSend); - for (MonoProcessor sub : processors) { - Assertions.assertThat(sub.peek()).isEqualTo(valueToSend); + for (AssertSubscriber sub : processors) { + Assertions.assertThat(sub.values()).containsExactly(valueToSend); Assertions.assertThat(sub.isTerminated()).isTrue(); } }) @@ -959,4 +992,37 @@ protected void doOnDispose() { onDisposeCalls.incrementAndGet(); } } + + private static class SinkOneSubscriber implements CoreSubscriber { + + private final Sinks.One processor; + private boolean valueReceived; + + public SinkOneSubscriber(Sinks.One processor) { + this.processor = processor; + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(String s) { + valueReceived = true; + processor.tryEmitValue(s); + } + + @Override + public void onError(Throwable t) { + processor.tryEmitError(t); + } + + @Override + public void onComplete() { + if (!valueReceived) { + processor.tryEmitEmpty(); + } + } + } } From 2816a79bc6dd3a0930ee3271865d85c2e55b0918 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 17 May 2021 01:33:18 +0300 Subject: [PATCH 037/116] adds UnboundedProcessor stress tests and fixes Signed-off-by: Oleh Dokuka --- .../UnboundedProcessorStressTest.java | 1323 +++++++++++++++++ .../rsocket/internal/UnboundedProcessor.java | 581 +++++--- .../internal/UnboundedProcessorTest.java | 2 - 3 files changed, 1701 insertions(+), 205 deletions(-) create mode 100644 rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java diff --git a/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java new file mode 100644 index 000000000..39ed2e4cb --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java @@ -0,0 +1,1323 @@ +package io.rsocket.internal; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.rsocket.core.StressSubscriber; +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.Expect; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.LLLL_Result; +import org.openjdk.jcstress.infra.results.LLL_Result; +import org.openjdk.jcstress.infra.results.L_Result; +import reactor.core.Fuseable; + +public abstract class UnboundedProcessorStressTest { + + final UnboundedProcessor unboundedProcessor = new UnboundedProcessor(); + + @JCStressTest + @Outcome( + id = { + "0, 1, 0", + "1, 1, 0", + "2, 1, 0", + "3, 1, 0", + "4, 1, 0", + + // dropped error scenarios + "0, 4, 0", + "1, 4, 0", + "2, 4, 0", + "3, 4, 0", + "4, 4, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onComplete() before dispose() || onError()") + @Outcome( + id = { + "0, 2, 0", "1, 2, 0", "2, 2, 0", "3, 2, 0", "4, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onError() before dispose() || onComplete()") + @Outcome( + id = { + "0, 2, 0", + "1, 2, 0", + "2, 2, 0", + "3, 2, 0", + "4, 2, 0", + // dropped error + "0, 5, 0", + "1, 5, 0", + "2, 5, 0", + "3, 5, 0", + "4, 5, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "dispose() before onError() || onComplete()") + @Outcome( + id = { + "0, 0, 0", + "1, 0, 0", + "2, 0, 0", + "3, 0, 0", + "4, 0, 0", + // interleave with error or complete happened first but dispose suppressed them + "0, 3, 0", + "1, 3, 0", + "2, 3, 0", + "3, 3, 0", + "4, 3, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "cancel() before or interleave with dispose() || onError() || onComplete()") + @State + public static class SmokeStressTest extends UnboundedProcessorStressTest { + + static final RuntimeException testException = new RuntimeException("test"); + + final StressSubscriber stressSubscriber = new StressSubscriber<>(0, Fuseable.NONE); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + { + unboundedProcessor.subscribe(stressSubscriber); + } + + @Actor + public void request() { + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + } + + @Actor + public void cancel() { + stressSubscriber.cancel(); + } + + @Actor + public void dispose() { + unboundedProcessor.dispose(); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Actor + public void complete() { + unboundedProcessor.onComplete(); + } + + @Actor + public void error() { + unboundedProcessor.onError(testException); + } + + @Arbiter + public void arbiter(LLL_Result r) { + r.r1 = stressSubscriber.onNextCalls; + r.r2 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = { + "0, 1, 0", + "1, 1, 0", + "2, 1, 0", + "3, 1, 0", + "4, 1, 0", + + // dropped error scenarios + "0, 4, 0", + "1, 4, 0", + "2, 4, 0", + "3, 4, 0", + "4, 4, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onComplete() before dispose() || onError()") + @Outcome( + id = { + "0, 2, 0", "1, 2, 0", "2, 2, 0", "3, 2, 0", "4, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onError() before dispose() || onComplete()") + @Outcome( + id = { + "0, 2, 0", + "1, 2, 0", + "2, 2, 0", + "3, 2, 0", + "4, 2, 0", + // dropped error + "0, 5, 0", + "1, 5, 0", + "2, 5, 0", + "3, 5, 0", + "4, 5, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "dispose() before onError() || onComplete()") + @Outcome( + id = { + "0, 0, 0", + "1, 0, 0", + "2, 0, 0", + "3, 0, 0", + "4, 0, 0", + // interleave with error or complete happened first but dispose suppressed them + "0, 3, 0", + "1, 3, 0", + "2, 3, 0", + "3, 3, 0", + "4, 3, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "cancel() before or interleave with dispose() || onError() || onComplete()") + @State + public static class SmokeFusedStressTest extends UnboundedProcessorStressTest { + + static final RuntimeException testException = new RuntimeException("test"); + + final StressSubscriber stressSubscriber = new StressSubscriber<>(0, Fuseable.ANY); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + { + unboundedProcessor.subscribe(stressSubscriber); + } + + @Actor + public void request() { + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + } + + @Actor + public void cancel() { + stressSubscriber.cancel(); + } + + @Actor + public void dispose() { + unboundedProcessor.dispose(); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Actor + public void complete() { + unboundedProcessor.onComplete(); + } + + @Actor + public void error() { + unboundedProcessor.onError(testException); + } + + @Arbiter + public void arbiter(LLL_Result r) { + r.r1 = stressSubscriber.onNextCalls; + r.r2 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = { + "0, 1, 0", + "1, 1, 0", + "2, 1, 0", + "3, 1, 0", + "4, 1, 0", + + // dropped error scenarios + "0, 4, 0", + "1, 4, 0", + "2, 4, 0", + "3, 4, 0", + "4, 4, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onComplete() before dispose() || onError()") + @Outcome( + id = { + "0, 2, 0", "1, 2, 0", "2, 2, 0", "3, 2, 0", "4, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onError() before dispose() || onComplete()") + @Outcome( + id = { + "0, 2, 0", + "1, 2, 0", + "2, 2, 0", + "3, 2, 0", + "4, 2, 0", + // dropped error + "0, 5, 0", + "1, 5, 0", + "2, 5, 0", + "3, 5, 0", + "4, 5, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "dispose() before onError() || onComplete()") + @State + public static class Smoke2StressTest extends UnboundedProcessorStressTest { + + static final RuntimeException testException = new RuntimeException("test"); + + final StressSubscriber stressSubscriber = new StressSubscriber<>(0, Fuseable.NONE); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + @Actor + public void subscribeAndRequest() { + unboundedProcessor.subscribe(stressSubscriber); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + } + + @Actor + public void dispose() { + unboundedProcessor.dispose(); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Actor + public void complete() { + unboundedProcessor.onComplete(); + } + + @Actor + public void error() { + unboundedProcessor.onError(testException); + } + + @Arbiter + public void arbiter(LLL_Result r) { + r.r1 = stressSubscriber.onNextCalls; + r.r2 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = { + "0, 1, 0", + "1, 1, 0", + "2, 1, 0", + "3, 1, 0", + "4, 1, 0", + + // dropped error scenarios + "0, 4, 0", + "1, 4, 0", + "2, 4, 0", + "3, 4, 0", + "4, 4, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onComplete() before dispose() || onError()") + @Outcome( + id = { + "0, 2, 0", "1, 2, 0", "2, 2, 0", "3, 2, 0", "4, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onError() before dispose() || onComplete()") + @Outcome( + id = { + "0, 2, 0", + "1, 2, 0", + "2, 2, 0", + "3, 2, 0", + "4, 2, 0", + // dropped error + "0, 5, 0", + "1, 5, 0", + "2, 5, 0", + "3, 5, 0", + "4, 5, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "dispose() before onError() || onComplete()") + @State + public static class Smoke2FusedStressTest extends UnboundedProcessorStressTest { + + static final RuntimeException testException = new RuntimeException("test"); + + final StressSubscriber stressSubscriber = new StressSubscriber<>(0, Fuseable.ANY); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + @Actor + public void subscribeAndRequest() { + unboundedProcessor.subscribe(stressSubscriber); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + } + + @Actor + public void dispose() { + unboundedProcessor.dispose(); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Actor + public void complete() { + unboundedProcessor.onComplete(); + } + + @Actor + public void error() { + unboundedProcessor.onError(testException); + } + + @Arbiter + public void arbiter(LLL_Result r) { + r.r1 = stressSubscriber.onNextCalls; + r.r2 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = { + "0, 1, 0", + "1, 1, 0", + "2, 1, 0", + "3, 1, 0", + "4, 1, 0", + + // dropped error scenarios + "0, 4, 0", + "1, 4, 0", + "2, 4, 0", + "3, 4, 0", + "4, 4, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onComplete() before dispose() || onError()") + @Outcome( + id = { + "0, 2, 0", "1, 2, 0", "2, 2, 0", "3, 2, 0", "4, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onError() before dispose() || onComplete()") + @Outcome( + id = { + "0, 2, 0", + "1, 2, 0", + "2, 2, 0", + "3, 2, 0", + "4, 2, 0", + // dropped error + "0, 5, 0", + "1, 5, 0", + "2, 5, 0", + "3, 5, 0", + "4, 5, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "dispose() before onError() || onComplete()") + @Outcome( + id = { + "0, 0, 0", + "1, 0, 0", + "2, 0, 0", + "3, 0, 0", + "4, 0, 0", + // interleave with error or complete happened first but dispose suppressed them + "0, 3, 0", + "1, 3, 0", + "2, 3, 0", + "3, 3, 0", + "4, 3, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "cancel() before or interleave with dispose() || onError() || onComplete()") + @State + public static class Smoke21FusedStressTest extends UnboundedProcessorStressTest { + + static final RuntimeException testException = new RuntimeException("test"); + + final StressSubscriber stressSubscriber = new StressSubscriber<>(0, Fuseable.ANY); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + @Actor + public void subscribeAndRequest() { + unboundedProcessor.subscribe(stressSubscriber); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + } + + @Actor + public void cancel() { + stressSubscriber.cancel(); + } + + @Actor + public void dispose() { + unboundedProcessor.dispose(); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Actor + public void complete() { + unboundedProcessor.onComplete(); + } + + @Actor + public void error() { + unboundedProcessor.onError(testException); + } + + @Arbiter + public void arbiter(LLL_Result r) { + r.r1 = stressSubscriber.onNextCalls; + r.r2 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = { + "-2954361355555045376, 4, 2, 0", + "-3242591731706757120, 4, 2, 0", + "-4107282860161892352, 4, 2, 0", + "-4395513236313604096, 4, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> dispose() before anything") + @Outcome( + id = { + "-2954361355555045376, 4, 0, 0", // here, dispose is earlier, but it was late to deliver + // error signal in the drainLoop + "-7566047373982433280, 4, 0, 0", + "-7854277750134145024, 4, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> cancel() before anything") + @Outcome( + id = { + "-2954361355555045376, 3, 2, 0", + "-3242591731706757120, 3, 2, 0", + "-4107282860161892352, 3, 2, 0", + "-4395513236313604096, 3, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> dispose() before anything") + @Outcome( + id = { + "-2954361355555045376, 3, 0, 0", // here, dispose is earlier, but it was late to deliver + // error signal in the drainLoop + "-7566047373982433280, 3, 0, 0", + "-7854277750134145024, 3, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> cancel() before anything") + @Outcome( + id = { + "-2954361355555045376, 2, 2, 0", + "-3242591731706757120, 2, 2, 0", + "-4107282860161892352, 2, 2, 0", + "-4395513236313604096, 2, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> dispose() before anything") + @Outcome( + id = { + "-2954361355555045376, 2, 0, 0", // here, dispose is earlier, but it was late to deliver + // error signal in the drainLoop + "-7566047373982433280, 2, 0, 0", + "-7854277750134145024, 2, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> cancel() before anything") + @Outcome( + id = { + "-2954361355555045376, 1, 2, 0", + "-3242591731706757120, 1, 2, 0", + "-4107282860161892352, 1, 2, 0", + "-4395513236313604096, 1, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1) -> dispose() before anything") + @Outcome( + id = { + "-2954361355555045376, 1, 0, 0", // here, dispose is earlier, but it was late to deliver + // error signal in the drainLoop + "-7566047373982433280, 1, 0, 0", + "-7854277750134145024, 1, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1) -> cancel() before anything") + @Outcome( + id = { + "-2954361355555045376, 0, 2, 0", + "-3242591731706757120, 0, 2, 0", + "-4107282860161892352, 0, 2, 0", + "-4395513236313604096, 0, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "dispose() before anything") + @Outcome( + id = { + "-2954361355555045376, 0, 0, 0", // here, dispose is earlier, but it was late to deliver + // error signal in the drainLoop + "-7566047373982433280, 0, 0, 0", + "-7854277750134145024, 0, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1) -> cancel() before anything") + @State + public static class RequestVsCancelVsOnNextVsDisposeStressTest + extends UnboundedProcessorStressTest { + + final StressSubscriber stressSubscriber = new StressSubscriber<>(0, Fuseable.NONE); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + { + unboundedProcessor.subscribe(stressSubscriber); + } + + @Actor + public void request() { + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + } + + @Actor + public void cancel() { + stressSubscriber.cancel(); + } + + @Actor + public void dispose() { + unboundedProcessor.dispose(); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Arbiter + public void arbiter(LLLL_Result r) { + r.r1 = unboundedProcessor.state; + r.r2 = stressSubscriber.onNextCalls; + r.r3 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = { + "-3242591731706757120, 4, 2, 0", + "-4107282860161892352, 4, 2, 0", + "-4395513236313604096, 4, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> dispose() before anything") + @Outcome( + id = { + "-7854277750134145024, 4, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> cancel() before anything") + @Outcome( + id = { + "-3242591731706757120, 3, 2, 0", + "-4107282860161892352, 3, 2, 0", + "-4395513236313604096, 3, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> dispose() before anything") + @Outcome( + id = { + "-7854277750134145024, 3, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> cancel() before anything") + @Outcome( + id = { + "-3242591731706757120, 2, 2, 0", + "-4107282860161892352, 2, 2, 0", + "-4395513236313604096, 2, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> dispose() before anything") + @Outcome( + id = { + "-7854277750134145024, 2, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> cancel() before anything") + @Outcome( + id = { + "-3242591731706757120, 1, 2, 0", + "-4107282860161892352, 1, 2, 0", + "-4395513236313604096, 1, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1) -> dispose() before anything") + @Outcome( + id = { + "-7854277750134145024, 1, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1) -> cancel() before anything") + @Outcome( + id = { + "-3242591731706757120, 0, 2, 0", + "-4107282860161892352, 0, 2, 0", + "-4395513236313604096, 0, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "dispose() before anything") + @Outcome( + id = { + "-7854277750134145024, 0, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1) -> cancel() before anything") + @State + public static class RequestVsCancelVsOnNextVsDisposeFusedStressTest + extends UnboundedProcessorStressTest { + + final StressSubscriber stressSubscriber = new StressSubscriber<>(0, Fuseable.ANY); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + { + unboundedProcessor.subscribe(stressSubscriber); + } + + @Actor + public void request() { + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + } + + @Actor + public void cancel() { + stressSubscriber.cancel(); + } + + @Actor + public void dispose() { + unboundedProcessor.dispose(); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Arbiter + public void arbiter(LLLL_Result r) { + r.r1 = unboundedProcessor.state; + r.r2 = stressSubscriber.onNextCalls; + r.r3 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = { + "-2954361355555045376, 4, 2, 0", + "-3242591731706757120, 4, 2, 0", + "-4107282860161892352, 4, 2, 0", + "-4395513236313604096, 4, 2, 0", + "-4539628424389459968, 4, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> dispose() before anything") + @Outcome( + id = { + "-2954361355555045376, 4, 0, 0", // here, dispose is earlier, but it was late to deliver + // error signal in the drainLoop + "-7566047373982433280, 4, 0, 0", + "-7854277750134145024, 4, 0, 0", + "-4539628424389459968, 4, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> cancel() before anything") + @Outcome( + id = { + "-2954361355555045376, 3, 2, 0", + "-3242591731706757120, 3, 2, 0", + "-4107282860161892352, 3, 2, 0", + "-4395513236313604096, 3, 2, 0", + "-4539628424389459968, 3, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> dispose() before anything") + @Outcome( + id = { + "-2954361355555045376, 3, 0, 0", // here, dispose is earlier, but it was late to deliver + // error signal in the drainLoop + "-7566047373982433280, 3, 0, 0", + "-7854277750134145024, 3, 0, 0", + "-4539628424389459968, 3, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> cancel() before anything") + @Outcome( + id = { + "-2954361355555045376, 2, 2, 0", + "-3242591731706757120, 2, 2, 0", + "-4107282860161892352, 2, 2, 0", + "-4395513236313604096, 2, 2, 0", + "-4539628424389459968, 2, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> dispose() before anything") + @Outcome( + id = { + "-2954361355555045376, 2, 0, 0", // here, dispose is earlier, but it was late to deliver + // error signal in the drainLoop + "-7566047373982433280, 2, 0, 0", + "-7854277750134145024, 2, 0, 0", + "-4539628424389459968, 2, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> cancel() before anything") + @Outcome( + id = { + "-2954361355555045376, 1, 2, 0", + "-3242591731706757120, 1, 2, 0", + "-4107282860161892352, 1, 2, 0", + "-4395513236313604096, 1, 2, 0", + "-4539628424389459968, 1, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1) -> dispose() before anything") + @Outcome( + id = { + "-2954361355555045376, 1, 0, 0", // here, dispose is earlier, but it was late to deliver + // error signal in the drainLoop + "-7566047373982433280, 1, 0, 0", + "-7854277750134145024, 1, 0, 0", + "-4539628424389459968, 1, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1) -> cancel() before anything") + @Outcome( + id = { + "-2954361355555045376, 0, 2, 0", + "-3242591731706757120, 0, 2, 0", + "-4107282860161892352, 0, 2, 0", + "-4395513236313604096, 0, 2, 0", + "-4539628424389459968, 0, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "dispose() before anything") + @Outcome( + id = { + "-2954361355555045376, 0, 0, 0", // here, dispose is earlier, but it was late to deliver + // error signal in the drainLoop + "-7566047373982433280, 0, 0, 0", + "-7854277750134145024, 0, 0, 0", + "-4539628424389459968, 0, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1) -> cancel() before anything") + @State + public static class SubscribeWithFollowingRequestsVsOnNextVsDisposeStressTest + extends UnboundedProcessorStressTest { + + final StressSubscriber stressSubscriber = new StressSubscriber<>(0, Fuseable.NONE); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + @Actor + public void subscribeAndRequest() { + unboundedProcessor.subscribe(stressSubscriber); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + } + + @Actor + public void dispose() { + unboundedProcessor.dispose(); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Arbiter + public void arbiter(LLLL_Result r) { + r.r1 = unboundedProcessor.state; + r.r2 = stressSubscriber.onNextCalls; + r.r3 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = { + "-3242591731706757120, 4, 2, 0", + "-4107282860161892352, 4, 2, 0", + "-4395513236313604096, 4, 2, 0", + "-4539628424389459968, 4, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> dispose() before anything") + @Outcome( + id = { + "-7854277750134145024, 4, 0, 0", + "-4539628424389459968, 4, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> cancel() before anything") + @Outcome( + id = { + "-3242591731706757120, 3, 2, 0", + "-4107282860161892352, 3, 2, 0", + "-4395513236313604096, 3, 2, 0", + "-4539628424389459968, 3, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> dispose() before anything") + @Outcome( + id = { + "-7854277750134145024, 3, 0, 0", + "-4539628424389459968, 3, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> cancel() before anything") + @Outcome( + id = { + "-3242591731706757120, 2, 2, 0", + "-4107282860161892352, 2, 2, 0", + "-4395513236313604096, 2, 2, 0", + "-4539628424389459968, 2, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> dispose() before anything") + @Outcome( + id = { + "-7854277750134145024, 2, 0, 0", + "-4539628424389459968, 2, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1, buf2) -> cancel() before anything") + @Outcome( + id = { + "-3242591731706757120, 1, 2, 0", + "-4107282860161892352, 1, 2, 0", + "-4395513236313604096, 1, 2, 0", + "-4539628424389459968, 1, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1) -> dispose() before anything") + @Outcome( + id = { + "-7854277750134145024, 1, 0, 0", + "-4539628424389459968, 1, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1) -> cancel() before anything") + @Outcome( + id = { + "-3242591731706757120, 0, 2, 0", + "-4107282860161892352, 0, 2, 0", + "-4395513236313604096, 0, 2, 0", + "-4539628424389459968, 0, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "dispose() before anything") + @Outcome( + id = { + "-7854277750134145024, 0, 0, 0", + "-4539628424389459968, 0, 0, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "next(buf1) -> cancel() before anything") + @State + public static class SubscribeWithFollowingRequestsVsOnNextVsDisposeFusedStressTest + extends UnboundedProcessorStressTest { + + final StressSubscriber stressSubscriber = new StressSubscriber<>(0, Fuseable.ANY); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + @Actor + public void subscribeAndRequest() { + unboundedProcessor.subscribe(stressSubscriber); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + } + + @Actor + public void dispose() { + unboundedProcessor.dispose(); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Arbiter + public void arbiter(LLLL_Result r) { + r.r1 = unboundedProcessor.state; + r.r2 = stressSubscriber.onNextCalls; + r.r3 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = {"-4539628424389459968, 0, 2, 0", "-3386706919782612992, 0, 2, 0"}, + expect = Expect.ACCEPTABLE, + desc = "dispose() before anything") + @Outcome( + id = {"-4395513236313604096, 0, 2, 0"}, + expect = Expect.ACCEPTABLE, + desc = "subscribe() -> dispose() before anything") + @Outcome( + id = {"-3242591731706757120, 0, 2, 0", "-3242591731706757120, 0, 0, 0"}, + expect = Expect.ACCEPTABLE, + desc = "subscribe() -> (dispose() || cancel())") + @Outcome( + id = {"-7854277750134145024, 0, 0, 0"}, + expect = Expect.ACCEPTABLE, + desc = "subscribe() -> cancel() before anything") + @State + public static class SubscribeWithFollowingCancelVsOnNextVsDisposeStressTest + extends UnboundedProcessorStressTest { + + final StressSubscriber stressSubscriber = new StressSubscriber<>(0, Fuseable.NONE); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + @Actor + public void subscribeAndCancel() { + unboundedProcessor.subscribe(stressSubscriber); + stressSubscriber.cancel(); + } + + @Actor + public void dispose() { + unboundedProcessor.dispose(); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Arbiter + public void arbiter(LLLL_Result r) { + r.r1 = unboundedProcessor.state; + r.r2 = stressSubscriber.onNextCalls; + r.r3 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = {"-4539628424389459968, 0, 2, 0", "-3386706919782612992, 0, 2, 0"}, + expect = Expect.ACCEPTABLE, + desc = "dispose() before anything") + @Outcome( + id = {"-4395513236313604096, 0, 2, 0"}, + expect = Expect.ACCEPTABLE, + desc = "subscribe() -> dispose() before anything") + @Outcome( + id = {"-3242591731706757120, 0, 2, 0", "-3242591731706757120, 0, 0, 0"}, + expect = Expect.ACCEPTABLE, + desc = "subscribe() -> (dispose() || cancel())") + @Outcome( + id = {"-7854277750134145024, 0, 0, 0"}, + expect = Expect.ACCEPTABLE, + desc = "subscribe() -> cancel() before anything") + @State + public static class SubscribeWithFollowingCancelVsOnNextVsDisposeFusedStressTest + extends UnboundedProcessorStressTest { + + final StressSubscriber stressSubscriber = new StressSubscriber<>(0, Fuseable.ANY); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + @Actor + public void subscribeAndCancel() { + unboundedProcessor.subscribe(stressSubscriber); + stressSubscriber.cancel(); + } + + @Actor + public void dispose() { + unboundedProcessor.dispose(); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Arbiter + public void arbiter(LLLL_Result r) { + r.r1 = unboundedProcessor.state; + r.r2 = stressSubscriber.onNextCalls; + r.r3 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = {"1"}, + expect = Expect.ACCEPTABLE) + @State + public static class SubscribeVsSubscribeStressTest extends UnboundedProcessorStressTest { + + final StressSubscriber stressSubscriber1 = new StressSubscriber<>(0, Fuseable.NONE); + final StressSubscriber stressSubscriber2 = new StressSubscriber<>(0, Fuseable.NONE); + + @Actor + public void subscribe1() { + unboundedProcessor.subscribe(stressSubscriber1); + } + + @Actor + public void subscribe2() { + unboundedProcessor.subscribe(stressSubscriber2); + } + + @Arbiter + public void arbiter(L_Result r) { + r.r1 = stressSubscriber1.onErrorCalls + stressSubscriber2.onErrorCalls; + } + } +} 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 d84546944..9e7500465 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -51,18 +51,24 @@ public final class UnboundedProcessor extends FluxProcessor Throwable error; CoreSubscriber actual; - static final long FLAG_TERMINATED = + static final long FLAG_FINALIZED = 0b1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; static final long FLAG_DISPOSED = 0b0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; - static final long FLAG_CANCELLED = + static final long FLAG_TERMINATED = 0b0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; - static final long FLAG_SUBSCRIBER_READY = + static final long FLAG_CANCELLED = 0b0001_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; - static final long FLAG_SUBSCRIBED_ONCE = + static final long FLAG_HAS_VALUE = 0b0000_1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long FLAG_HAS_REQUEST = + 0b0000_0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long FLAG_SUBSCRIBER_READY = + 0b0000_0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long FLAG_SUBSCRIBED_ONCE = + 0b0000_0001_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; static final long MAX_WIP_VALUE = - 0b0000_0111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111L; + 0b0000_0000_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111L; volatile long state; @@ -110,140 +116,183 @@ public Object scanUnsafe(Attr key) { } public void onNextPrioritized(ByteBuf t) { - if (this.done) { + if (this.done || this.cancelled) { release(t); return; } - if (this.cancelled) { + + if (!this.priorityQueue.offer(t)) { + onError(Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext())); release(t); return; } - if (!this.priorityQueue.offer(t)) { - Throwable ex = - Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); - onError(Operators.onOperatorError(null, ex, t, currentContext())); - release(t); + final long previousState = markValueAdded(this); + if (isFinalized(previousState)) { + this.clearSafely(); return; } - drain(); + if (isSubscriberReady(previousState)) { + if (this.outputFused) { + // fast path for fusion + this.actual.onNext(null); + return; + } + + if (isWorkInProgress(previousState) + || isCancelled(previousState) + || isDisposed(previousState) + || isTerminated(previousState)) { + return; + } + + if (hasRequest(previousState)) { + drainRegular(previousState); + } + } } @Override public void onNext(ByteBuf t) { - if (this.done) { + if (this.done || this.cancelled) { release(t); return; } - if (this.cancelled) { + + if (!this.queue.offer(t)) { + onError(Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext())); release(t); return; } - if (!this.queue.offer(t)) { - Throwable ex = - Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); - onError(Operators.onOperatorError(null, ex, t, currentContext())); - release(t); + final long previousState = markValueAdded(this); + if (isFinalized(previousState)) { + this.clearSafely(); return; } - drain(); + if (isSubscriberReady(previousState)) { + if (this.outputFused) { + // fast path for fusion + this.actual.onNext(null); + return; + } + + if (isWorkInProgress(previousState) + || isCancelled(previousState) + || isDisposed(previousState) + || isTerminated(previousState)) { + return; + } + + if (hasRequest(previousState)) { + drainRegular(previousState); + } + } } @Override public void onError(Throwable t) { - if (this.done) { + if (this.done || this.cancelled) { Operators.onErrorDropped(t, currentContext()); return; } - if (this.cancelled) { - return; - } this.error = t; this.done = true; - drain(); - } - - @Override - public void onComplete() { - if (this.done) { + final long previousState = markTerminatedOrFinalized(this); + if (isFinalized(previousState) + || isDisposed(previousState) + || isCancelled(previousState) + || isTerminated(previousState)) { + Operators.onErrorDropped(t, currentContext()); return; } - this.done = true; + if (isSubscriberReady(previousState)) { + if (this.outputFused) { + // fast path for fusion scenario + this.actual.onError(t); + return; + } - drain(); - } + if (isWorkInProgress(previousState)) { + return; + } - void drain() { - long previousState = wipIncrement(this); - if (isTerminated(previousState)) { - this.clearSafely(); - return; + if (!hasValue(previousState)) { + // fast path no-values scenario + this.actual.onError(t); + return; + } + + if (hasRequest(previousState)) { + drainRegular(previousState); + } } + } - if (isWorkInProgress(previousState)) { + @Override + public void onComplete() { + if (this.done || this.cancelled) { return; } - long expectedState = previousState + 1; - for (; ; ) { - if (isSubscriberReady(expectedState)) { - final boolean outputFused = this.outputFused; - final CoreSubscriber a = this.actual; + this.done = true; - if (outputFused) { - drainFused(expectedState, a); - } else { - if (isCancelled(expectedState)) { - clearAndTerminate(this); - return; - } + final long previousState = markTerminatedOrFinalized(this); + if (isFinalized(previousState) + || isDisposed(previousState) + || isCancelled(previousState) + || isTerminated(previousState)) { + return; + } - if (isDisposed(expectedState)) { - clearAndTerminate(this); - a.onError(new CancellationException("Disposed")); - return; - } + if (isSubscriberReady(previousState)) { + if (this.outputFused) { + // fast path for fusion scenario + this.actual.onComplete(); + return; + } - drainRegular(expectedState, a); - } + if (isWorkInProgress(previousState)) { return; - } else { - if (isCancelled(expectedState) || isDisposed(expectedState)) { - clearAndTerminate(this); - return; - } } - expectedState = wipRemoveMissing(this, expectedState); - if (!isWorkInProgress(expectedState)) { + if (!hasValue(previousState)) { + this.actual.onComplete(); return; } + + if (hasRequest(previousState)) { + drainRegular(previousState); + } } } - void drainRegular(long expectedState, CoreSubscriber a) { + void drainRegular(long previousState) { + final CoreSubscriber a = this.actual; final Queue q = this.queue; final Queue pq = this.priorityQueue; + long expectedState = previousState + 1; for (; ; ) { long r = this.requested; long e = 0L; + boolean empty = false; + boolean done; while (r != e) { // done has to be read before queue.poll to ensure there was no racing: // Thread1: <#drain>: queue.poll(null) --------------------> this.done(true) // Thread2: ------------------> <#onNext(V)> --> <#onComplete()> - boolean done = this.done; + done = this.done; ByteBuf t = pq.poll(); - boolean empty = t == null; + empty = t == null; if (empty) { t = q.poll(); @@ -270,57 +319,26 @@ void drainRegular(long expectedState, CoreSubscriber a) { // done has to be read before queue.isEmpty to ensure there was no racing: // Thread1: <#drain>: queue.isEmpty(true) --------------------> this.done(true) // Thread2: --------------------> <#onNext(V)> ---> <#onComplete()> - if (checkTerminated(this.done, q.isEmpty() && pq.isEmpty(), a)) { + done = this.done; + empty = q.isEmpty() && pq.isEmpty(); + + if (checkTerminated(done, empty, a)) { return; } } if (e != 0 && r != Long.MAX_VALUE) { - REQUESTED.addAndGet(this, -e); - } - - expectedState = wipRemoveMissing(this, expectedState); - if (isCancelled(expectedState)) { - clearAndTerminate(this); - return; - } - - if (isDisposed(expectedState)) { - clearAndTerminate(this); - a.onError(new CancellationException("Disposed")); - return; + r = REQUESTED.addAndGet(this, -e); } - if (!isWorkInProgress(expectedState)) { - break; - } - } - } - - void drainFused(long expectedState, CoreSubscriber a) { - for (; ; ) { - // done has to be read before queue.poll to ensure there was no racing: - // Thread1: <#drain>: queue.poll(null) --------------------> this.done(true) - boolean d = this.done; - - a.onNext(null); - - if (d) { - Throwable ex = this.error; - if (ex != null) { - a.onError(ex); - } else { - a.onComplete(); - } - return; - } - - expectedState = wipRemoveMissing(this, expectedState); + expectedState = markWorkDone(this, expectedState, r > 0, !empty); if (isCancelled(expectedState)) { + clearAndFinalize(this); return; } if (isDisposed(expectedState)) { + clearAndFinalize(this); a.onError(new CancellationException("Disposed")); return; } @@ -334,18 +352,18 @@ void drainFused(long expectedState, CoreSubscriber a) { boolean checkTerminated(boolean done, boolean empty, CoreSubscriber a) { final long state = this.state; if (isCancelled(state)) { - clearAndTerminate(this); + clearAndFinalize(this); return true; } if (isDisposed(state)) { - clearAndTerminate(this); + clearAndFinalize(this); a.onError(new CancellationException("Disposed")); return true; } if (done && empty) { - clearAndTerminate(this); + clearAndFinalize(this); Throwable e = this.error; if (e != null) { a.onError(e); @@ -361,7 +379,7 @@ boolean checkTerminated(boolean done, boolean empty, CoreSubscriber actual) { Objects.requireNonNull(actual, "subscribe"); - if (markSubscribedOnce(this)) { - actual.onSubscribe(this); - this.actual = actual; - long previousState = markSubscriberReady(this); + long previousState = markSubscribedOnce(this); + if (isSubscribedOnce(previousState)) { + Operators.error( + actual, new IllegalStateException("UnboundedProcessor allows only a single Subscriber")); + return; + } + + if (isDisposed(previousState)) { + Operators.error(actual, new CancellationException("Disposed")); + return; + } + + actual.onSubscribe(this); + this.actual = actual; + + previousState = markSubscriberReady(this); + + if (this.outputFused) { if (isCancelled(previousState)) { return; } + if (isDisposed(previousState)) { actual.onError(new CancellationException("Disposed")); return; } - if (isWorkInProgress(previousState)) { - return; + + if (hasValue(previousState)) { + actual.onNext(null); } - drain(); - } else { - Operators.error( - actual, new IllegalStateException("UnboundedProcessor allows only a single Subscriber")); + + if (isTerminated(previousState)) { + final Throwable e = this.error; + if (e != null) { + actual.onError(e); + } else { + actual.onComplete(); + } + } + return; + } + + if (isCancelled(previousState)) { + clearAndFinalize(this); + } + + if (isDisposed(previousState)) { + clearAndFinalize(this); + actual.onError(new CancellationException("Disposed")); + return; + } + + if (!hasValue(previousState)) { + if (isTerminated(previousState)) { + clearAndFinalize(this); + final Throwable e = this.error; + if (e != null) { + actual.onError(e); + } else { + actual.onComplete(); + } + } + return; + } + + if (hasRequest(previousState)) { + drainRegular(previousState); } } @Override public void request(long n) { if (Operators.validate(n)) { + if (this.outputFused) { + final long state = this.state; + if (isSubscriberReady(state)) { + this.actual.onNext(null); + } + return; + } + Operators.addCap(REQUESTED, this, n); - drain(); + + final long previousState = markRequestAdded(this); + if (isWorkInProgress(previousState) + || isFinalized(previousState) + || isCancelled(previousState) + || isDisposed(previousState)) { + return; + } + + if (isSubscriberReady(previousState) && hasValue(previousState)) { + drainRegular(previousState); + } } } @@ -415,15 +501,15 @@ public void cancel() { this.cancelled = true; final long previousState = markCancelled(this); - if (isTerminated(previousState) + if (isWorkInProgress(previousState) + || isFinalized(previousState) || isCancelled(previousState) - || isDisposed(previousState) - || isWorkInProgress(previousState)) { + || isDisposed(previousState)) { return; } - if (!isSubscriberReady(previousState) || !this.outputFused) { - clearAndTerminate(this); + if (!isSubscribedOnce(previousState) || !this.outputFused) { + clearAndFinalize(this); } } @@ -432,22 +518,31 @@ public void dispose() { this.cancelled = true; final long previousState = markDisposed(this); - if (isTerminated(previousState) + if (isWorkInProgress(previousState) + || isFinalized(previousState) || isCancelled(previousState) - || isDisposed(previousState) - || isWorkInProgress(previousState)) { + || isDisposed(previousState)) { + return; + } + + if (!isSubscribedOnce(previousState)) { + clearAndFinalize(this); return; } if (!isSubscriberReady(previousState)) { - clearAndTerminate(this); return; } if (!this.outputFused) { - clearAndTerminate(this); + clearAndFinalize(this); + this.actual.onError(new CancellationException("Disposed")); + return; + } + + if (!isTerminated(previousState)) { + this.actual.onError(new CancellationException("Disposed")); } - this.actual.onError(new CancellationException("Disposed")); } @Override @@ -479,7 +574,7 @@ public boolean isEmpty() { */ @Override public void clear() { - clearAndTerminate(this); + clearAndFinalize(this); } void clearSafely() { @@ -523,15 +618,12 @@ public int requestFusion(int requestedMode) { @Override public boolean isDisposed() { - final long state = this.state; - return isTerminated(state) || isCancelled(state) || isDisposed(state) || this.done; + return isFinalized(this.state); } @Override public boolean isTerminated() { - //noinspection unused - final long state = this.state; - return this.done; + return this.done || isTerminated(this.state); } @Override @@ -569,29 +661,26 @@ static void release(ByteBuf byteBuf) { /** * Sets {@link #FLAG_SUBSCRIBED_ONCE} flag if it was not set before and if flags {@link - * #FLAG_TERMINATED}, {@link #FLAG_CANCELLED} or {@link #FLAG_DISPOSED} are unset + * #FLAG_FINALIZED}, {@link #FLAG_CANCELLED} or {@link #FLAG_DISPOSED} are unset * * @return {@code true} if {@link #FLAG_SUBSCRIBED_ONCE} was successfully set */ - static boolean markSubscribedOnce(UnboundedProcessor instance) { + static long markSubscribedOnce(UnboundedProcessor instance) { for (; ; ) { - long state = instance.state; + final long state = instance.state; - if ((state & FLAG_TERMINATED) == FLAG_TERMINATED - || (state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE - || (state & FLAG_CANCELLED) == FLAG_CANCELLED - || (state & FLAG_DISPOSED) == FLAG_DISPOSED) { - return false; + if (isSubscribedOnce(state)) { + return state; } if (STATE.compareAndSet(instance, state, state | FLAG_SUBSCRIBED_ONCE)) { - return true; + return state; } } } /** - * Sets {@link #FLAG_SUBSCRIBER_READY} flag if flags {@link #FLAG_TERMINATED}, {@link + * Sets {@link #FLAG_SUBSCRIBER_READY} flag if flags {@link #FLAG_FINALIZED}, {@link * #FLAG_CANCELLED} or {@link #FLAG_DISPOSED} are unset * * @return previous state @@ -600,100 +689,165 @@ static long markSubscriberReady(UnboundedProcessor instance) { for (; ; ) { long state = instance.state; - if ((state & FLAG_TERMINATED) == FLAG_TERMINATED - || (state & FLAG_CANCELLED) == FLAG_CANCELLED - || (state & FLAG_DISPOSED) == FLAG_DISPOSED) { + if (isFinalized(state) || isCancelled(state) || isDisposed(state)) { return state; } - if (STATE.compareAndSet(instance, state, state | FLAG_SUBSCRIBER_READY)) { + long nextState = state; + if (!instance.outputFused) { + if ((!hasValue(state) && isTerminated(state)) || (hasRequest(state) && hasValue(state))) { + nextState = addWork(state); + } + } + + if (STATE.compareAndSet(instance, state, nextState | FLAG_SUBSCRIBER_READY)) { return state; } } } /** - * Sets {@link #FLAG_CANCELLED} flag if it was not set before and if flag {@link #FLAG_TERMINATED} - * is unset. Also, this method increments number of work in progress (WIP) + * Sets {@link #FLAG_HAS_REQUEST} flag if it was not set before and if flags {@link + * #FLAG_FINALIZED}, {@link #FLAG_CANCELLED}, {@link #FLAG_DISPOSED} are unset. Also, this method + * increments number of work in progress (WIP) * * @return previous state */ - static long markCancelled(UnboundedProcessor instance) { + static long markRequestAdded(UnboundedProcessor instance) { for (; ; ) { long state = instance.state; - if ((state & FLAG_TERMINATED) == FLAG_TERMINATED - || (state & FLAG_CANCELLED) == FLAG_CANCELLED) { + if (isFinalized(state) || isCancelled(state) || isDisposed(state)) { return state; } - long nextState = state + 1; - if ((nextState & MAX_WIP_VALUE) == 0) { - nextState = state; + long nextState = state; + if (isSubscriberReady(state) && hasValue(state)) { + nextState = addWork(state); } - if (STATE.compareAndSet(instance, state, nextState | FLAG_CANCELLED)) { + if (STATE.compareAndSet(instance, state, nextState | FLAG_HAS_REQUEST)) { return state; } } } /** - * Sets {@link #FLAG_DISPOSED} flag if it was not set before and if flags {@link - * #FLAG_TERMINATED}, {@link #FLAG_CANCELLED} are unset. Also, this method increments number of - * work in progress (WIP) + * Sets {@link #FLAG_HAS_VALUE} flag if it was not set before and if flags {@link + * #FLAG_FINALIZED}, {@link #FLAG_CANCELLED}, {@link #FLAG_DISPOSED} are unset. Also, this method + * increments number of work in progress (WIP) if {@link #FLAG_HAS_REQUEST} is set * * @return previous state */ - static long markDisposed(UnboundedProcessor instance) { + static long markValueAdded(UnboundedProcessor instance) { for (; ; ) { - long state = instance.state; + final long state = instance.state; - if ((state & FLAG_TERMINATED) == FLAG_TERMINATED - || (state & FLAG_CANCELLED) == FLAG_CANCELLED - || (state & FLAG_DISPOSED) == FLAG_DISPOSED) { + if (isFinalized(state)) { return state; } - long nextState = state + 1; - if ((nextState & MAX_WIP_VALUE) == 0) { - nextState = state; + long nextState = state; + if (isWorkInProgress(state)) { + nextState = addWork(state); + } else if (isSubscriberReady(state)) { + if (instance.outputFused) { + // fast path for fusion scenario + return state; + } + + if (hasRequest(state)) { + nextState = addWork(state); + } } - if (STATE.compareAndSet(instance, state, nextState | FLAG_DISPOSED)) { + if (STATE.compareAndSet(instance, state, nextState | FLAG_HAS_VALUE)) { return state; } } } /** - * Increments the amount of work in progress (max value is {@link #MAX_WIP_VALUE} on the given - * state. Fails if flag {@link #FLAG_TERMINATED} is set. + * Sets {@link #FLAG_TERMINATED} flag if it was not set before and if flags {@link + * #FLAG_FINALIZED}, {@link #FLAG_CANCELLED}, {@link #FLAG_DISPOSED} are unset. Also, this method + * increments number of work in progress (WIP) * * @return previous state */ - static long wipIncrement(UnboundedProcessor instance) { + static long markTerminatedOrFinalized(UnboundedProcessor instance) { for (; ; ) { - long state = instance.state; + final long state = instance.state; - if ((state & FLAG_TERMINATED) == FLAG_TERMINATED) { + if (isFinalized(state) || isTerminated(state) || isCancelled(state) || isDisposed(state)) { + return state; + } + + long nextState = state; + if (isSubscriberReady(state) && !instance.outputFused) { + if (!hasValue(state)) { + // fast path for no values and no work in progress + nextState = FLAG_FINALIZED; + } else if (hasRequest(state)) { + nextState = addWork(state); + } + } + + if (STATE.compareAndSet(instance, state, nextState | FLAG_TERMINATED)) { + return state; + } + } + } + + /** + * Sets {@link #FLAG_CANCELLED} flag if it was not set before and if flag {@link #FLAG_FINALIZED} + * is unset. Also, this method increments number of work in progress (WIP) + * + * @return previous state + */ + static long markCancelled(UnboundedProcessor instance) { + for (; ; ) { + final long state = instance.state; + + if (isFinalized(state) || isCancelled(state)) { + return state; + } + + final long nextState = addWork(state); + if (STATE.compareAndSet(instance, state, nextState | FLAG_CANCELLED)) { return state; } + } + } - final long nextState = state + 1; - if ((nextState & MAX_WIP_VALUE) == 0) { + /** + * Sets {@link #FLAG_DISPOSED} flag if it was not set before and if flags {@link #FLAG_FINALIZED}, + * {@link #FLAG_CANCELLED} are unset. Also, this method increments number of work in progress + * (WIP) + * + * @return previous state + */ + static long markDisposed(UnboundedProcessor instance) { + for (; ; ) { + final long state = instance.state; + + if (isFinalized(state) || isCancelled(state) || isDisposed(state)) { return state; } - if (STATE.compareAndSet(instance, state, nextState)) { + final long nextState = addWork(state); + if (STATE.compareAndSet(instance, state, nextState | FLAG_DISPOSED)) { return state; } } } + static long addWork(long state) { + return (state & MAX_WIP_VALUE) == MAX_WIP_VALUE ? state : state + 1; + } + /** * Decrements the amount of work in progress by the given amount on the given state. Fails if flag - * is {@link #FLAG_TERMINATED} is set or if fusion disabled and flags {@link #FLAG_CANCELLED} or + * is {@link #FLAG_FINALIZED} is set or if fusion disabled and flags {@link #FLAG_CANCELLED} or * {@link #FLAG_DISPOSED} are set. * *

    Note, if fusion is enabled, the decrement should work if flags {@link #FLAG_CANCELLED} or @@ -702,39 +856,47 @@ static long wipIncrement(UnboundedProcessor instance) { * * @return state after changing WIP or current state if update failed */ - static long wipRemoveMissing(UnboundedProcessor instance, long previousState) { - long missed = previousState & MAX_WIP_VALUE; + static long markWorkDone( + UnboundedProcessor instance, long expectedState, boolean hasRequest, boolean hasValue) { + final long expectedMissed = expectedState & MAX_WIP_VALUE; for (; ; ) { - long state = instance.state; + final long state = instance.state; + final long missed = state & MAX_WIP_VALUE; - if ((state & FLAG_TERMINATED) == FLAG_TERMINATED) { + if (missed != expectedMissed) { return state; } - if (((state & FLAG_SUBSCRIBER_READY) != FLAG_SUBSCRIBER_READY || !instance.outputFused) - && ((state & FLAG_CANCELLED) == FLAG_CANCELLED - || (state & FLAG_DISPOSED) == FLAG_DISPOSED)) { + if (isFinalized(state) || isCancelled(state) || isDisposed(state)) { return state; } - final long nextState = state - missed; - if (STATE.compareAndSet(instance, state, nextState)) { + final long nextState = state - expectedMissed; + if (STATE.compareAndSet( + instance, + state, + nextState ^ (hasRequest ? 0 : FLAG_HAS_REQUEST) ^ (hasValue ? 0 : FLAG_HAS_VALUE))) { return nextState; } } } /** - * Set flag {@link #FLAG_TERMINATED} and {@link #release(ByteBuf)} all the elements from {@link + * Set flag {@link #FLAG_FINALIZED} and {@link #release(ByteBuf)} all the elements from {@link * #queue} and {@link #priorityQueue}. * *

    This method may be called concurrently only if the given {@link UnboundedProcessor} has no - * output fusion ({@link #outputFused} {@code == true}). Otherwise this method MUST be called once - * and only by the downstream calling method {@link #clear()} + * output fusion ({@link #outputFused} {@code == true}). Otherwise this method MUST only by the + * downstream calling method {@link #clear()} */ - static void clearAndTerminate(UnboundedProcessor instance) { + static void clearAndFinalize(UnboundedProcessor instance) { for (; ; ) { - long state = instance.state; + final long state = instance.state; + + if (isFinalized(state)) { + instance.clearSafely(); + return; + } if (!isSubscriberReady(state) || !instance.outputFused) { instance.clearSafely(); @@ -742,16 +904,21 @@ static void clearAndTerminate(UnboundedProcessor instance) { instance.clearUnsafely(); } - if ((state & FLAG_TERMINATED) == FLAG_TERMINATED) { - return; - } - - if (STATE.compareAndSet(instance, state, (state & ~MAX_WIP_VALUE) | FLAG_TERMINATED)) { + if (STATE.compareAndSet( + instance, state, (state & ~MAX_WIP_VALUE & ~FLAG_HAS_VALUE) | FLAG_FINALIZED)) { break; } } } + static boolean hasValue(long state) { + return (state & FLAG_HAS_VALUE) == FLAG_HAS_VALUE; + } + + static boolean hasRequest(long state) { + return (state & FLAG_HAS_REQUEST) == FLAG_HAS_REQUEST; + } + static boolean isCancelled(long state) { return (state & FLAG_CANCELLED) == FLAG_CANCELLED; } @@ -768,7 +935,15 @@ static boolean isTerminated(long state) { return (state & FLAG_TERMINATED) == FLAG_TERMINATED; } + static boolean isFinalized(long state) { + return (state & FLAG_FINALIZED) == FLAG_FINALIZED; + } + static boolean isSubscriberReady(long state) { return (state & FLAG_SUBSCRIBER_READY) == FLAG_SUBSCRIBER_READY; } + + static boolean isSubscribedOnce(long state) { + return (state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE; + } } 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 3f5194ff6..a5772e020 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java @@ -28,7 +28,6 @@ import java.time.Duration; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import reactor.core.Fuseable; @@ -190,7 +189,6 @@ public void smokeTest1(boolean withFusionEnabled) { name = "Ensures that racing between onNext | dispose | subscribe | request(n) | terminal will not cause any issues and leaks; mode[fusionEnabled={0}]") @ValueSource(booleans = {true, false}) - @Disabled("hard to support in 1.0.x") public void smokeTest2(boolean withFusionEnabled) { final LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); From 627f5900963d97d0c0826f796ac478f9710dab8d Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 19 May 2021 15:30:54 +0300 Subject: [PATCH 038/116] ensures InMemoryResumableFramesStore does not retain not resumable frames (#1009) Signed-off-by: Oleh Dokuka --- .../resume/InMemoryResumableFramesStore.java | 36 ++- .../resume/InMemoryResumeStoreTest.java | 218 ++++++++++-------- 2 files changed, 149 insertions(+), 105 deletions(-) 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 f0a370ae6..03516af92 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java @@ -234,10 +234,12 @@ public void onComplete() { public void onNext(ByteBuf frame) { final int state; final boolean isResumable = isResumableFrame(frame); + boolean canBeStore = isResumable; if (isResumable) { final ArrayList frames = cachedFrames; - int incomingFrameSize = frame.readableBytes(); + final int incomingFrameSize = frame.readableBytes(); final int cacheLimit = this.cacheLimit; + if (cacheLimit != Integer.MAX_VALUE) { long availableSize = cacheLimit - cacheSize; if (availableSize < incomingFrameSize) { @@ -256,17 +258,27 @@ public void onNext(ByteBuf frame) { } } CACHE_SIZE.addAndGet(this, -removedBytes); - POSITION.addAndGet(this, removedBytes); + + canBeStore = availableSize >= incomingFrameSize; + POSITION.addAndGet(this, removedBytes + (canBeStore ? 0 : incomingFrameSize)); + } else { + canBeStore = true; } + } else { + canBeStore = true; } - synchronized (this) { - state = this.state; - if (state != 2) { - frames.add(frame); + + state = this.state; + if (canBeStore) { + synchronized (this) { + if (state != 2) { + frames.add(frame); + } + } + + if (cacheLimit != Integer.MAX_VALUE) { + CACHE_SIZE.addAndGet(this, incomingFrameSize); } - } - if (cacheLimit != Integer.MAX_VALUE) { - CACHE_SIZE.addAndGet(this, incomingFrameSize); } } else { state = this.state; @@ -274,8 +286,8 @@ public void onNext(ByteBuf frame) { final CoreSubscriber actual = this.actual; if (state == 1) { - actual.onNext(frame.retain()); - } else if (!isResumable || state == 2) { + actual.onNext(isResumable && canBeStore ? frame.retainedSlice() : frame); + } else if (!isResumable || !canBeStore || state == 2) { frame.release(); } } @@ -302,7 +314,7 @@ public void subscribe(CoreSubscriber actual) { actual.onSubscribe(this); synchronized (this) { for (final ByteBuf frame : cachedFrames) { - actual.onNext(frame.retain()); + actual.onNext(frame.retainedSlice()); } } diff --git a/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java b/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java index e0374eede..a595faa86 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java @@ -1,93 +1,125 @@ -// 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); -// } -// } +package io.rsocket.resume; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.Arrays; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +public class InMemoryResumeStoreTest { + + @Test + void saveNonResumableFrame() { + InMemoryResumableFramesStore store = inMemoryStore(25); + ByteBuf frame1 = fakeConnectionFrame(10); + ByteBuf frame2 = fakeConnectionFrame(35); + store.saveFrames(Flux.just(frame1, frame2)).block(); + assertThat(store.cachedFrames.size()).isZero(); + assertThat(store.cacheSize).isZero(); + assertThat(store.position).isZero(); + assertThat(frame1.refCnt()).isZero(); + assertThat(frame2.refCnt()).isZero(); + } + + @Test + void saveWithoutTailRemoval() { + InMemoryResumableFramesStore store = inMemoryStore(25); + ByteBuf frame = fakeResumableFrame(10); + store.saveFrames(Flux.just(frame)).block(); + assertThat(store.cachedFrames.size()).isEqualTo(1); + assertThat(store.cacheSize).isEqualTo(frame.readableBytes()); + assertThat(store.position).isZero(); + assertThat(frame.refCnt()).isOne(); + } + + @Test + void saveRemoveOneFromTail() { + InMemoryResumableFramesStore store = inMemoryStore(25); + ByteBuf frame1 = fakeResumableFrame(20); + ByteBuf frame2 = fakeResumableFrame(10); + store.saveFrames(Flux.just(frame1, frame2)).block(); + assertThat(store.cachedFrames.size()).isOne(); + assertThat(store.cacheSize).isEqualTo(frame2.readableBytes()); + assertThat(store.position).isEqualTo(frame1.readableBytes()); + assertThat(frame1.refCnt()).isZero(); + assertThat(frame2.refCnt()).isOne(); + } + + @Test + void saveRemoveTwoFromTail() { + InMemoryResumableFramesStore store = inMemoryStore(25); + ByteBuf frame1 = fakeResumableFrame(10); + ByteBuf frame2 = fakeResumableFrame(10); + ByteBuf frame3 = fakeResumableFrame(20); + store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); + assertThat(store.cachedFrames.size()).isOne(); + assertThat(store.cacheSize).isEqualTo(frame3.readableBytes()); + assertThat(store.position).isEqualTo(size(frame1, frame2)); + assertThat(frame1.refCnt()).isZero(); + assertThat(frame2.refCnt()).isZero(); + assertThat(frame3.refCnt()).isOne(); + } + + @Test + void saveBiggerThanStore() { + InMemoryResumableFramesStore store = inMemoryStore(25); + ByteBuf frame1 = fakeResumableFrame(10); + ByteBuf frame2 = fakeResumableFrame(10); + ByteBuf frame3 = fakeResumableFrame(30); + store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); + assertThat(store.cachedFrames.size()).isZero(); + assertThat(store.cacheSize).isZero(); + assertThat(store.position).isEqualTo(size(frame1, frame2, frame3)); + assertThat(frame1.refCnt()).isZero(); + assertThat(frame2.refCnt()).isZero(); + assertThat(frame3.refCnt()).isZero(); + } + + @Test + void releaseFrames() { + InMemoryResumableFramesStore store = inMemoryStore(100); + ByteBuf frame1 = fakeResumableFrame(10); + ByteBuf frame2 = fakeResumableFrame(10); + ByteBuf frame3 = fakeResumableFrame(30); + store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); + store.releaseFrames(20); + assertThat(store.cachedFrames.size()).isOne(); + assertThat(store.cacheSize).isEqualTo(frame3.readableBytes()); + assertThat(store.position).isEqualTo(size(frame1, frame2)); + assertThat(frame1.refCnt()).isZero(); + assertThat(frame2.refCnt()).isZero(); + assertThat(frame3.refCnt()).isOne(); + } + + @Test + void receiveImpliedPosition() { + InMemoryResumableFramesStore store = inMemoryStore(100); + ByteBuf frame1 = fakeResumableFrame(10); + ByteBuf frame2 = fakeResumableFrame(30); + store.resumableFrameReceived(frame1); + store.resumableFrameReceived(frame2); + assertThat(store.frameImpliedPosition()).isEqualTo(size(frame1, frame2)); + } + + 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 fakeResumableFrame(int size) { + byte[] bytes = new byte[size]; + Arrays.fill(bytes, (byte) 7); + return Unpooled.wrappedBuffer(bytes); + } + + private static ByteBuf fakeConnectionFrame(int size) { + byte[] bytes = new byte[size]; + Arrays.fill(bytes, (byte) 0); + return Unpooled.wrappedBuffer(bytes); + } +} From 28ce349853b2a3a9a23e84c87f2481d99a6f80e6 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 19 May 2021 20:53:43 +0300 Subject: [PATCH 039/116] fixes WeightedLoadbalanceStrategy test Signed-off-by: Oleh Dokuka --- .../io/rsocket/loadbalance/LoadbalanceTest.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java index 8d55b1422..7d575a515 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java @@ -132,10 +132,14 @@ public void shouldDeliverAllTheRequestsWithWeightedStrategy() throws Interrupted source, WeightedLoadbalanceStrategy.builder() .weightedStatsResolver( - rsocket -> - ((PooledRSocket) rsocket).target() == target1 - ? weightedRSocket1 - : weightedRSocket2) + rsocket -> { + if (rsocket instanceof TestRSocket) { + return (WeightedRSocket) ((TestRSocket) rsocket).source(); + } + return ((PooledRSocket) rsocket).target() == target1 + ? weightedRSocket1 + : weightedRSocket2; + }) .build()); RaceTestUtils.race( @@ -315,6 +319,10 @@ public Mono onClose() { public void dispose() { sink.tryEmitEmpty(); } + + public RSocket source() { + return source; + } } private static class WeightedRSocket extends BaseWeightedStats implements RSocket { From 291108376dea4153937c6bc756c1f46e59c2084a Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 24 May 2021 23:40:14 +0300 Subject: [PATCH 040/116] adds fusion support for jcstress test StressSubscriber Signed-off-by: Oleh Dokuka --- .../io/rsocket/core/StressSubscriber.java | 319 ++++++++++++++++-- 1 file changed, 297 insertions(+), 22 deletions(-) diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscriber.java b/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscriber.java index 1b7c050bc..31fd44374 100644 --- a/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscriber.java +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscriber.java @@ -15,14 +15,21 @@ */ package io.rsocket.core; +import static reactor.core.publisher.Operators.addCap; + import java.util.ArrayList; import java.util.List; +import java.util.Queue; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; import reactor.core.publisher.Operators; import reactor.util.context.Context; @@ -35,23 +42,31 @@ enum Operation { ON_SUBSCRIBE } - final long initRequest; - final Context context; + final int requestedFusionMode; - volatile Subscription subscription; - - @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater S = - AtomicReferenceFieldUpdater.newUpdater( - StressSubscriber.class, Subscription.class, "subscription"); + int fusionMode; + Subscription subscription; public Throwable error; + public boolean done; public List droppedErrors = new CopyOnWriteArrayList<>(); public List values = new ArrayList<>(); + volatile long requested; + + @SuppressWarnings("rawtypes") + static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(StressSubscriber.class, "requested"); + + volatile int wip; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(StressSubscriber.class, "wip"); + public volatile Operation guard; @SuppressWarnings("rawtypes") @@ -68,6 +83,8 @@ enum Operation { public volatile int onNextCalls; + public BlockingQueue q = new LinkedBlockingDeque<>(); + @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater ON_NEXT_CALLS = AtomicIntegerFieldUpdater.newUpdater(StressSubscriber.class, "onNextCalls"); @@ -98,7 +115,7 @@ enum Operation { /** Build a {@link StressSubscriber} that makes an unbounded request upon subscription. */ public StressSubscriber() { - this(Long.MAX_VALUE); + this(Long.MAX_VALUE, Fuseable.NONE); } /** @@ -108,16 +125,24 @@ public StressSubscriber() { * @param initRequest the requested amount upon subscription, or zero to disable initial request */ public StressSubscriber(long initRequest) { - this.initRequest = initRequest; + this(initRequest, Fuseable.NONE); + } + + /** + * Build a {@link StressSubscriber} that requests the provided amount in {@link + * #onSubscribe(Subscription)}. Use {@code 0} to avoid any initial request upon subscription. + * + * @param initRequest the requested amount upon subscription, or zero to disable initial request + */ + public StressSubscriber(long initRequest, int requestedFusionMode) { + this.requestedFusionMode = requestedFusionMode; this.context = Operators.enableOnDiscard( Context.of( "reactor.onErrorDropped.local", - (Consumer) - throwable -> { - droppedErrors.add(throwable); - }), + (Consumer) throwable -> droppedErrors.add(throwable)), (__) -> ON_NEXT_DISCARDED.incrementAndGet(this)); + REQUESTED.lazySet(this, initRequest | Long.MIN_VALUE); } @Override @@ -129,12 +154,47 @@ public Context currentContext() { public void onSubscribe(Subscription subscription) { if (!GUARD.compareAndSet(this, null, Operation.ON_SUBSCRIBE)) { concurrentOnSubscribe = true; + subscription.cancel(); } else { - boolean wasSet = Operators.setOnce(S, this, subscription); + final boolean isValid = Operators.validate(this.subscription, subscription); + if (isValid) { + this.subscription = subscription; + } GUARD.compareAndSet(this, Operation.ON_SUBSCRIBE, null); - if (wasSet) { - if (initRequest > 0) { - subscription.request(initRequest); + + if (this.requestedFusionMode > 0 && subscription instanceof Fuseable.QueueSubscription) { + final int m = + ((Fuseable.QueueSubscription) subscription).requestFusion(this.requestedFusionMode); + final long requested = this.requested; + this.fusionMode = m; + if (m != Fuseable.NONE) { + if (requested == Long.MAX_VALUE) { + subscription.cancel(); + } + drain(); + return; + } + } + + if (isValid) { + long delivered = 0; + for (; ; ) { + long s = requested; + if (s == Long.MAX_VALUE) { + subscription.cancel(); + break; + } + + long r = s & Long.MAX_VALUE; + long toRequest = r - delivered; + if (toRequest > 0) { + subscription.request(toRequest); + delivered = r; + } + + if (REQUESTED.compareAndSet(this, s, 0)) { + break; + } } } } @@ -143,6 +203,11 @@ public void onSubscribe(Subscription subscription) { @Override public void onNext(T value) { + if (fusionMode == Fuseable.ASYNC) { + drain(); + return; + } + if (!GUARD.compareAndSet(this, null, Operation.ON_NEXT)) { concurrentOnNext = true; } else { @@ -160,7 +225,13 @@ public void onError(Throwable throwable) { GUARD.compareAndSet(this, Operation.ON_ERROR, null); } error = throwable; + done = true; + q.offer(throwable); ON_ERROR_CALLS.incrementAndGet(this); + + if (fusionMode == Fuseable.ASYNC) { + drain(); + } } @Override @@ -170,19 +241,223 @@ public void onComplete() { } else { GUARD.compareAndSet(this, Operation.ON_COMPLETE, null); } + done = true; ON_COMPLETE_CALLS.incrementAndGet(this); + + if (fusionMode == Fuseable.ASYNC) { + drain(); + } } public void request(long n) { if (Operators.validate(n)) { - Subscription s = this.subscription; - if (s != null) { - s.request(n); + for (; ; ) { + final long s = this.requested; + if (s == 0) { + this.subscription.request(n); + return; + } + + if ((s & Long.MIN_VALUE) != Long.MIN_VALUE) { + return; + } + + final long r = s & Long.MAX_VALUE; + if (r == Long.MAX_VALUE) { + return; + } + + final long u = addCap(r, n); + if (REQUESTED.compareAndSet(this, s, u | Long.MIN_VALUE)) { + if (this.fusionMode != Fuseable.NONE) { + drain(); + } + return; + } } } } public void cancel() { - Operators.terminate(S, this); + for (; ; ) { + long s = this.requested; + if (s == 0) { + this.subscription.cancel(); + return; + } + + if (REQUESTED.compareAndSet(this, s, Long.MAX_VALUE)) { + if (this.fusionMode != Fuseable.NONE) { + drain(); + } + return; + } + } + } + + @SuppressWarnings("unchecked") + private void drain() { + final int previousState = markWorkAdded(); + if (isFinalized(previousState)) { + ((Queue) this.subscription).clear(); + return; + } + + if (isWorkInProgress(previousState)) { + return; + } + + final Subscription s = this.subscription; + final Queue q = (Queue) s; + + int expectedState = previousState + 1; + for (; ; ) { + long r = this.requested & Long.MAX_VALUE; + long e = 0L; + + while (r != e) { + // done has to be read before queue.poll to ensure there was no racing: + // Thread1: <#drain>: queue.poll(null) --------------------> this.done(true) + // Thread2: ------------------> <#onNext(V)> --> <#onComplete()> + boolean done = this.done; + + final T t = q.poll(); + final boolean empty = t == null; + + if (checkTerminated(done, empty)) { + if (!empty) { + values.add(t); + } + return; + } + + if (empty) { + break; + } + + values.add(t); + + e++; + } + + if (r == e) { + // done has to be read before queue.isEmpty to ensure there was no racing: + // Thread1: <#drain>: queue.isEmpty(true) --------------------> this.done(true) + // Thread2: --------------------> <#onNext(V)> ---> <#onComplete()> + boolean done = this.done; + boolean empty = q.isEmpty(); + + if (checkTerminated(done, empty)) { + return; + } + } + + if (e != 0) { + ON_NEXT_CALLS.addAndGet(this, (int) e); + if (r != Long.MAX_VALUE) { + produce(e); + } + } + + expectedState = markWorkDone(expectedState); + if (!isWorkInProgress(expectedState)) { + return; + } + } + } + + boolean checkTerminated(boolean done, boolean empty) { + final long state = this.requested; + if (state == Long.MAX_VALUE) { + this.subscription.cancel(); + clearAndFinalize(); + return true; + } + + if (done && empty) { + clearAndFinalize(); + return true; + } + + return false; + } + + final void produce(long produced) { + for (; ; ) { + final long s = this.requested; + + if ((s & Long.MIN_VALUE) != Long.MIN_VALUE) { + return; + } + + final long r = s & Long.MAX_VALUE; + if (r == Long.MAX_VALUE) { + return; + } + + final long u = r - produced; + if (REQUESTED.compareAndSet(this, s, u | Long.MIN_VALUE)) { + return; + } + } + } + + @SuppressWarnings("unchecked") + final void clearAndFinalize() { + final Queue q = (Queue) this.subscription; + for (; ; ) { + final int state = this.wip; + + q.clear(); + + if (WIP.compareAndSet(this, state, Integer.MIN_VALUE)) { + return; + } + } + } + + final int markWorkAdded() { + for (; ; ) { + final int state = this.wip; + + if (isFinalized(state)) { + return state; + } + + int nextState = state + 1; + if ((nextState & Integer.MAX_VALUE) == 0) { + return state; + } + + if (WIP.compareAndSet(this, state, nextState)) { + return state; + } + } + } + + final int markWorkDone(int expectedState) { + for (; ; ) { + final int state = this.wip; + + if (expectedState != state) { + return state; + } + + if (isFinalized(state)) { + return state; + } + + if (WIP.compareAndSet(this, state, 0)) { + return 0; + } + } + } + + static boolean isFinalized(int state) { + return state == Integer.MIN_VALUE; + } + + static boolean isWorkInProgress(int state) { + return (state & Integer.MAX_VALUE) > 0; } } From 040278ac3efe0779f057226079362e9629f323b1 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 1 Jun 2021 10:10:02 +0300 Subject: [PATCH 041/116] makes numbers of repeats of race tests as env constant (#1015) --- .../java/io/rsocket/RaceTestConstants.java | 6 +++ .../io/rsocket/core/RSocketRequesterTest.java | 7 ++-- .../io/rsocket/core/RSocketResponderTest.java | 13 ++++--- .../io/rsocket/core/ReconnectMonoTests.java | 37 ++++++++++--------- .../rsocket/core/ResolvingOperatorTests.java | 34 +++++++++-------- .../io/rsocket/exceptions/ExceptionsTest.java | 3 +- .../internal/UnboundedProcessorTest.java | 15 ++++---- 7 files changed, 65 insertions(+), 50 deletions(-) create mode 100644 rsocket-core/src/test/java/io/rsocket/RaceTestConstants.java diff --git a/rsocket-core/src/test/java/io/rsocket/RaceTestConstants.java b/rsocket-core/src/test/java/io/rsocket/RaceTestConstants.java new file mode 100644 index 000000000..d30f1415e --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/RaceTestConstants.java @@ -0,0 +1,6 @@ +package io.rsocket; + +public class RaceTestConstants { + public static final int REPEATS = + Integer.parseInt(System.getProperty("rsocket.test.race.repeats", "1000")); +} 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 b5a3dcb83..f93d55570 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.util.ReferenceCounted; import io.rsocket.Payload; import io.rsocket.RSocket; +import io.rsocket.RaceTestConstants; import io.rsocket.TestScheduler; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.CustomRSocketException; @@ -404,7 +405,7 @@ static Stream>> prepareCalls() { public void checkNoLeaksOnRacing( Function> initiator, BiConsumer, ClientSocketRule> runner) { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { ClientSocketRule clientSocketRule = new ClientSocketRule(); try { clientSocketRule @@ -987,7 +988,7 @@ public void ensuresCorrectOrderOfStreamIdIssuingInCaseOfRacing( FrameType interactionType2) { Assumptions.assumeThat(interactionType1).isNotEqualTo(METADATA_PUSH); Assumptions.assumeThat(interactionType2).isNotEqualTo(METADATA_PUSH); - for (int i = 1; i < 10000; i += 4) { + for (int i = 1; i < RaceTestConstants.REPEATS; i += 4) { Payload payload = DefaultPayload.create("test", "test"); Publisher publisher1 = interaction1.apply(rule, payload); Publisher publisher2 = interaction2.apply(rule, payload); @@ -1072,7 +1073,7 @@ public void shouldTerminateAllStreamsIfThereRacingBetweenDisposeAndRequests( BiFunction> interaction2, FrameType interactionType1, FrameType interactionType2) { - for (int i = 1; i < 10000; i++) { + for (int i = 1; i < RaceTestConstants.REPEATS; i++) { Payload payload1 = ByteBufPayload.create("test", "test"); Payload payload2 = ByteBufPayload.create("test", "test"); AssertSubscriber assertSubscriber1 = AssertSubscriber.create(); 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 76691adce..2c1335fcb 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -39,6 +39,7 @@ import io.netty.util.ReferenceCounted; import io.rsocket.Payload; import io.rsocket.RSocket; +import io.rsocket.RaceTestConstants; import io.rsocket.frame.CancelFrameCodec; import io.rsocket.frame.ErrorFrameCodec; import io.rsocket.frame.FrameHeaderCodec; @@ -247,7 +248,7 @@ protected void hookOnSubscribe(Subscription subscription) { @Test public void checkNoLeaksOnRacingCancelFromRequestChannelAndNextFromUpstream() { ByteBufAllocator allocator = rule.alloc(); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { AssertSubscriber assertSubscriber = AssertSubscriber.create(); rule.setAcceptingSocket( @@ -301,7 +302,7 @@ public Flux requestChannel(Publisher payloads) { public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChannelTest() { Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { AssertSubscriber assertSubscriber = AssertSubscriber.create(); FluxSink[] sinks = new FluxSink[1]; @@ -340,7 +341,7 @@ public Flux requestChannel(Publisher payloads) { public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChannelTest1() { Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { AssertSubscriber assertSubscriber = AssertSubscriber.create(); FluxSink[] sinks = new FluxSink[1]; @@ -382,7 +383,7 @@ public Flux requestChannel(Publisher payloads) { checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromUpstreamOnErrorFromRequestChannelTest1() { Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { FluxSink[] sinks = new FluxSink[1]; AssertSubscriber assertSubscriber = AssertSubscriber.create(); rule.setAcceptingSocket( @@ -474,7 +475,7 @@ public Flux requestChannel(Publisher payloads) { public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestStreamTest1() { Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { FluxSink[] sinks = new FluxSink[1]; rule.setAcceptingSocket( @@ -509,7 +510,7 @@ public Flux requestStream(Payload payload) { public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestResponseTest1() { Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { Operators.MonoSubscriber[] sources = new Operators.MonoSubscriber[1]; rule.setAcceptingSocket( diff --git a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java index 88ac062d1..85b1d577d 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java @@ -18,6 +18,7 @@ import static org.junit.Assert.assertEquals; +import io.rsocket.RaceTestConstants; import io.rsocket.internal.subscriber.AssertSubscriber; import java.io.IOException; import java.time.Duration; @@ -60,7 +61,7 @@ public class ReconnectMonoTests { public void shouldExpireValueOnRacingDisposeAndNext() { Hooks.onErrorDropped(t -> {}); Hooks.onNextDropped(System.out::println); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final int index = i; final CoreSubscriber[] monoSubscribers = new CoreSubscriber[1]; Subscription mockSubscription = Mockito.mock(Subscription.class); @@ -108,7 +109,7 @@ public void subscribe(CoreSubscriber actual) { @Test public void shouldNotifyAllTheSubscribersUnderRacingBetweenSubscribeAndComplete() { Hooks.onErrorDropped(t -> {}); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestPublisher cold = TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); @@ -151,7 +152,7 @@ public void shouldNotifyAllTheSubscribersUnderRacingBetweenSubscribeAndComplete( @Test public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() { Hooks.onErrorDropped(t -> {}); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final int index = i; final TestPublisher cold = TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); @@ -214,7 +215,7 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() @Test public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates() { Hooks.onErrorDropped(t -> {}); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final int index = i; final TestPublisher cold = TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); @@ -281,7 +282,7 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates( @Test public void shouldNotExpireNewlyResolvedValueIfBlockIsRacingWithInvalidate() { Hooks.onErrorDropped(t -> {}); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final int index = i; final Mono source = Mono.fromSupplier( @@ -347,7 +348,7 @@ public String get() { @Test public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribers() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestPublisher cold = TestPublisher.createCold(); cold.next("value" + i); @@ -394,7 +395,7 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribers() { @Test public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { Duration timeout = Duration.ofMillis(100); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestPublisher cold = TestPublisher.createCold(); cold.next("value" + i); @@ -441,7 +442,7 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { @Test public void shouldEstablishValueOnceInCaseOfRacingBetweenBlocks() { Duration timeout = Duration.ofMillis(100); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestPublisher cold = TestPublisher.createCold(); cold.next("value" + i); @@ -486,7 +487,7 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenBlocks() { @Test public void shouldExpireValueOnRacingDisposeAndNoValueComplete() { Hooks.onErrorDropped(t -> {}); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestPublisher cold = TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); @@ -524,7 +525,7 @@ public void shouldExpireValueOnRacingDisposeAndNoValueComplete() { @Test public void shouldExpireValueOnRacingDisposeAndComplete() { Hooks.onErrorDropped(t -> {}); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestPublisher cold = TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); @@ -564,7 +565,7 @@ public void shouldExpireValueOnRacingDisposeAndComplete() { public void shouldExpireValueOnRacingDisposeAndError() { Hooks.onErrorDropped(t -> {}); RuntimeException runtimeException = new RuntimeException("test"); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestPublisher cold = TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); @@ -610,7 +611,7 @@ public void shouldExpireValueOnRacingDisposeAndError() { public void shouldExpireValueOnRacingDisposeAndErrorWithNoBackoff() { Hooks.onErrorDropped(t -> {}); RuntimeException runtimeException = new RuntimeException("test"); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestPublisher cold = TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); @@ -886,7 +887,7 @@ public void shouldNotifyAllTheSubscribers() { final ArrayList> processors = new ArrayList<>(200); - for (int i = 0; i < 100; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final MonoProcessor subA = MonoProcessor.create(); final MonoProcessor subB = MonoProcessor.create(); processors.add(subA); @@ -894,11 +895,13 @@ public void shouldNotifyAllTheSubscribers() { RaceTestUtils.race(() -> reconnectMono.subscribe(subA), () -> reconnectMono.subscribe(subB)); } - Assertions.assertThat(reconnectMono.resolvingInner.subscribers).hasSize(204); + Assertions.assertThat(reconnectMono.resolvingInner.subscribers) + .hasSize(RaceTestConstants.REPEATS * 2 + 4); sub1.dispose(); - Assertions.assertThat(reconnectMono.resolvingInner.subscribers).hasSize(203); + Assertions.assertThat(reconnectMono.resolvingInner.subscribers) + .hasSize(RaceTestConstants.REPEATS * 2 + 3); publisher.next("value"); @@ -917,7 +920,7 @@ public void shouldNotifyAllTheSubscribers() { @Test public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidates() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestPublisher cold = TestPublisher.createCold(); cold.next("value"); final int timeout = 10; @@ -959,7 +962,7 @@ public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidates() { @Test public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidateAndDispose() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestPublisher cold = TestPublisher.createCold(); cold.next("value"); final int timeout = 10000; diff --git a/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java b/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java index 608e1a336..15bc0a143 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java @@ -16,6 +16,7 @@ package io.rsocket.core; +import io.rsocket.RaceTestConstants; import io.rsocket.internal.subscriber.AssertSubscriber; import java.time.Duration; import java.util.ArrayList; @@ -48,7 +49,7 @@ public class ResolvingOperatorTests { @Test public void shouldExpireValueOnRacingDisposeAndComplete() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final int index = i; MonoProcessor processor = MonoProcessor.create(); @@ -88,7 +89,7 @@ public void shouldExpireValueOnRacingDisposeAndComplete() { @Test public void shouldNotifyAllTheSubscribersUnderRacingBetweenSubscribeAndComplete() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final String valueToSend = "value" + i; MonoProcessor processor = MonoProcessor.create(); @@ -142,7 +143,7 @@ public void shouldNotifyAllTheSubscribersUnderRacingBetweenSubscribeAndComplete( @Test public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final String valueToSend = "value" + i; final String valueToSend2 = "value2" + i; @@ -223,7 +224,7 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() @Test public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final String valueToSend = "value" + i; final String valueToSend2 = "value_to_possibly_expire" + i; @@ -330,7 +331,7 @@ public void accept(ResolvingTest self) { @Test public void shouldNotExpireNewlyResolvedValueIfBlockIsRacingWithInvalidate() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final String valueToSend = "value" + i; final String valueToSend2 = "value2" + i; @@ -392,7 +393,7 @@ public void shouldNotExpireNewlyResolvedValueIfBlockIsRacingWithInvalidate() { @Test public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribers() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final String valueToSend = "value" + i; MonoProcessor processor = MonoProcessor.create(); @@ -449,7 +450,7 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribers() { @Test public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final String valueToSend = "value" + i; MonoProcessor processor = MonoProcessor.create(); @@ -498,7 +499,7 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { @Test public void shouldEstablishValueOnceInCaseOfRacingBetweenBlocks() { Duration timeout = Duration.ofMillis(100); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final String valueToSend = "value" + i; MonoProcessor processor = MonoProcessor.create(); @@ -540,7 +541,7 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenBlocks() { public void shouldExpireValueOnRacingDisposeAndError() { Hooks.onErrorDropped(t -> {}); RuntimeException runtimeException = new RuntimeException("test"); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { MonoProcessor processor = MonoProcessor.create(); BiConsumer consumer = (v, t) -> { @@ -734,7 +735,8 @@ public void shouldNotifyAllTheSubscribers( final MonoProcessor sub3 = MonoProcessor.create(); final MonoProcessor sub4 = MonoProcessor.create(); - final ArrayList> processors = new ArrayList<>(200); + final ArrayList> processors = + new ArrayList<>(RaceTestConstants.REPEATS * 2); ResolvingTest.create() .assertDisposeCalled(0) @@ -753,7 +755,7 @@ public void shouldNotifyAllTheSubscribers( .assertPendingSubscribers(4) .then( self -> { - for (int i = 0; i < 100; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final MonoProcessor subA = MonoProcessor.create(); final MonoProcessor subB = MonoProcessor.create(); processors.add(subA); @@ -764,9 +766,9 @@ public void shouldNotifyAllTheSubscribers( } }) .assertSubscribeCalled(1) - .assertPendingSubscribers(204) + .assertPendingSubscribers(RaceTestConstants.REPEATS * 2 + 4) .then(self -> sub1.dispose()) - .assertPendingSubscribers(203) + .assertPendingSubscribers(RaceTestConstants.REPEATS * 2 + 3) .then( self -> { String valueToSend = "value"; @@ -789,7 +791,7 @@ public void shouldNotifyAllTheSubscribers( @Test public void shouldBeSerialIfRacyMonoInner() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { long[] requested = new long[] {0}; Subscription mockSubscription = Mockito.mock(Subscription.class); Mockito.doAnswer( @@ -825,7 +827,7 @@ public void accept(Object o, Object o2) {} @Test public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidates() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { ResolvingTest.create() .assertNothingExpired() .assertNothingReceived() @@ -839,7 +841,7 @@ public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidates() { @Test public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidateAndDispose() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { ResolvingTest.create() .assertNothingExpired() .assertNothingReceived() 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 b09548245..db5c47179 100644 --- a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java @@ -31,6 +31,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.UnpooledByteBufAllocator; +import io.rsocket.RaceTestConstants; import io.rsocket.frame.ErrorFrameCodec; import java.util.concurrent.ThreadLocalRandom; import org.junit.jupiter.api.DisplayName; @@ -201,7 +202,7 @@ void fromUnsupportedSetupException() { @DisplayName("from returns CustomRSocketException") @Test void fromCustomRSocketException() { - for (int i = 0; i < 1000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { int randomCode = ThreadLocalRandom.current().nextBoolean() ? ThreadLocalRandom.current() 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 5177a65be..b0c6c6fd8 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java @@ -23,6 +23,7 @@ import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; +import io.rsocket.RaceTestConstants; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.internal.subscriber.AssertSubscriber; import java.time.Duration; @@ -89,7 +90,7 @@ public void testOnNextAfterSubscribeN(int n) { public void testPrioritizedSending(boolean fusedCase) { UnboundedProcessor processor = new UnboundedProcessor<>(); - for (int i = 0; i < 1000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { processor.onNext(Unpooled.EMPTY_BUFFER); } @@ -108,7 +109,7 @@ public void testPrioritizedSending(boolean fusedCase) { public void ensureUnboundedProcessorDisposesQueueProperly(boolean withFusionEnabled) { final LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); final ByteBuf buffer1 = allocator.buffer(1); @@ -146,7 +147,7 @@ public void smokeTest1(boolean withFusionEnabled) { final LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); final RuntimeException runtimeException = new RuntimeException("test"); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); final ByteBuf buffer1 = allocator.buffer(1); @@ -195,7 +196,7 @@ public void smokeTest2(boolean withFusionEnabled) { final LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); final RuntimeException runtimeException = new RuntimeException("test"); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); final ByteBuf buffer1 = allocator.buffer(1); @@ -242,7 +243,7 @@ public void smokeTest3(boolean withFusionEnabled) { final LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); final RuntimeException runtimeException = new RuntimeException("test"); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); final ByteBuf buffer1 = allocator.buffer(1); @@ -285,7 +286,7 @@ public void smokeTest31(boolean withFusionEnabled) { final LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); final RuntimeException runtimeException = new RuntimeException("test"); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); final ByteBuf buffer1 = allocator.buffer(1); @@ -332,7 +333,7 @@ public void ensuresAsyncFusionAndDisposureHasNoDeadlock(boolean withFusionEnable final LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); final ByteBuf buffer1 = allocator.buffer(1); final ByteBuf buffer2 = allocator.buffer(2); From 894aa6d72d89db388d93af8b2b75ce864f76a53f Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Wed, 2 Jun 2021 12:37:43 +0300 Subject: [PATCH 042/116] removes junit 4 and fully migrates to junit 5 (#1016) Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- build.gradle | 2 - rsocket-core/build.gradle | 3 - .../io/rsocket/core/AbstractSocketRule.java | 30 +- .../ClientServerInputMultiplexerTest.java | 58 +-- .../core/DefaultRSocketClientTests.java | 79 +--- .../io/rsocket/core/RSocketReconnectTest.java | 16 +- .../core/RSocketRequesterTerminationTest.java | 68 +++- .../io/rsocket/core/RSocketRequesterTest.java | 23 +- .../io/rsocket/core/RSocketResponderTest.java | 31 +- .../core/RSocketServerFragmentationTest.java | 2 +- .../java/io/rsocket/core/RSocketTest.java | 60 ++- .../io/rsocket/core/ReconnectMonoTests.java | 344 ++++++++---------- .../io/rsocket/core/StreamIdSupplierTest.java | 15 +- .../java/io/rsocket/core/TestingStuff.java | 21 -- .../rsocket/frame/ResumeFrameCodecTest.java | 11 +- .../rsocket/frame/ResumeOkFrameCodecTest.java | 7 +- .../java/io/rsocket/lease/LeaseImplTest.java | 2 - .../metadata/MimeTypeMetadataCodecTest.java | 2 +- .../io/rsocket/util/DefaultPayloadTest.java | 24 +- rsocket-examples/build.gradle | 4 +- .../rsocket/integration/IntegrationTest.java | 44 +-- .../integration/TcpIntegrationTest.java | 39 +- .../rsocket/integration/TestingStreaming.java | 10 +- rsocket-load-balancer/build.gradle | 4 +- .../client/LoadBalancedRSocketMonoTest.java | 26 +- .../rsocket/client/RSocketSupplierTest.java | 17 +- .../io/rsocket/client/TimeoutClientTest.java | 2 +- .../test/java/io/rsocket/stat/MedianTest.java | 9 +- rsocket-test/build.gradle | 3 - .../io/rsocket/test/BaseClientServerTest.java | 110 +++--- .../java/io/rsocket/test/ClientSetupRule.java | 26 +- 31 files changed, 511 insertions(+), 581 deletions(-) delete mode 100644 rsocket-core/src/test/java/io/rsocket/core/TestingStuff.java diff --git a/build.gradle b/build.gradle index 83b20bb50..dcc133833 100644 --- a/build.gradle +++ b/build.gradle @@ -84,8 +84,6 @@ subprojects { entry 'mockito-junit-jupiter' entry 'mockito-core' } - // TODO: Remove after JUnit5 migration - dependency 'junit:junit:4.12' dependency "org.hamcrest:hamcrest-library:${ext['hamcrest.version']}" dependencySet(group: 'org.openjdk.jmh', version: ext['jmh.version']) { entry 'jmh-core' diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index ce9c47f32..ece4b8e4c 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -40,10 +40,7 @@ dependencies { testRuntimeOnly 'ch.qos.logback:logback-classic' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' - // TODO: Remove after JUnit5 migration - testCompileOnly 'junit:junit' testImplementation 'org.hamcrest:hamcrest-library' - testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' jcstressImplementation "ch.qos.logback:logback-classic" } 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 53323a26e..a3e5a62ff 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java +++ b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java @@ -24,12 +24,9 @@ import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestSubscriber; import java.time.Duration; -import org.junit.rules.ExternalResource; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; import org.reactivestreams.Subscriber; -public abstract class AbstractSocketRule extends ExternalResource { +public abstract class AbstractSocketRule { protected TestDuplexConnection connection; protected Subscriber connectSub; @@ -38,22 +35,15 @@ public abstract class AbstractSocketRule extends ExternalReso protected int maxFrameLength = FRAME_LENGTH_MASK; protected int maxInboundPayloadSize = Integer.MAX_VALUE; - @Override - public Statement apply(final Statement base, Description description) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - allocator = - LeaksTrackingByteBufAllocator.instrument( - ByteBufAllocator.DEFAULT, Duration.ofSeconds(5), ""); - connectSub = TestSubscriber.create(); - init(); - base.evaluate(); - } - }; + public void init() { + allocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(5), ""); + connectSub = TestSubscriber.create(); + doInit(); } - protected void init() { + protected void doInit() { if (socket != null) { socket.dispose(); } @@ -66,12 +56,12 @@ protected void init() { public void setMaxInboundPayloadSize(int maxInboundPayloadSize) { this.maxInboundPayloadSize = maxInboundPayloadSize; - init(); + doInit(); } public void setMaxFrameLength(int maxFrameLength) { this.maxFrameLength = maxFrameLength; - init(); + doInit(); } protected abstract T newRSocket(); diff --git a/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java b/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java index fdf312bce..c9ecb6eb6 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java @@ -16,7 +16,7 @@ package io.rsocket.core; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -65,32 +65,32 @@ public void clientSplits() { .subscribe(); source.addToReceivedBuffer(errorFrame(1).retain()); - assertEquals(1, clientFrames.get()); - assertEquals(0, serverFrames.get()); + assertThat(clientFrames.get()).isOne(); + assertThat(serverFrames.get()).isZero(); source.addToReceivedBuffer(errorFrame(1).retain()); - assertEquals(2, clientFrames.get()); - assertEquals(0, serverFrames.get()); + assertThat(clientFrames.get()).isEqualTo(2); + assertThat(serverFrames.get()).isZero(); source.addToReceivedBuffer(leaseFrame().retain()); - assertEquals(3, clientFrames.get()); - assertEquals(0, serverFrames.get()); + assertThat(clientFrames.get()).isEqualTo(3); + assertThat(serverFrames.get()).isZero(); source.addToReceivedBuffer(keepAliveFrame().retain()); - assertEquals(4, clientFrames.get()); - assertEquals(0, serverFrames.get()); + assertThat(clientFrames.get()).isEqualTo(4); + assertThat(serverFrames.get()).isZero(); source.addToReceivedBuffer(errorFrame(2).retain()); - assertEquals(4, clientFrames.get()); - assertEquals(1, serverFrames.get()); + assertThat(clientFrames.get()).isEqualTo(4); + assertThat(serverFrames.get()).isOne(); source.addToReceivedBuffer(errorFrame(0).retain()); - assertEquals(5, clientFrames.get()); - assertEquals(1, serverFrames.get()); + assertThat(clientFrames.get()).isEqualTo(5); + assertThat(serverFrames.get()).isOne(); source.addToReceivedBuffer(metadataPushFrame().retain()); - assertEquals(5, clientFrames.get()); - assertEquals(2, serverFrames.get()); + assertThat(clientFrames.get()).isEqualTo(5); + assertThat(serverFrames.get()).isEqualTo(2); } @Test @@ -110,32 +110,32 @@ public void serverSplits() { .subscribe(); source.addToReceivedBuffer(errorFrame(1).retain()); - assertEquals(1, clientFrames.get()); - assertEquals(0, serverFrames.get()); + assertThat(clientFrames.get()).isEqualTo(1); + assertThat(serverFrames.get()).isZero(); source.addToReceivedBuffer(errorFrame(1).retain()); - assertEquals(2, clientFrames.get()); - assertEquals(0, serverFrames.get()); + assertThat(clientFrames.get()).isEqualTo(2); + assertThat(serverFrames.get()).isZero(); source.addToReceivedBuffer(leaseFrame().retain()); - assertEquals(2, clientFrames.get()); - assertEquals(1, serverFrames.get()); + assertThat(clientFrames.get()).isEqualTo(2); + assertThat(serverFrames.get()).isOne(); source.addToReceivedBuffer(keepAliveFrame().retain()); - assertEquals(2, clientFrames.get()); - assertEquals(2, serverFrames.get()); + assertThat(clientFrames.get()).isEqualTo(2); + assertThat(serverFrames.get()).isEqualTo(2); source.addToReceivedBuffer(errorFrame(2).retain()); - assertEquals(2, clientFrames.get()); - assertEquals(3, serverFrames.get()); + assertThat(clientFrames.get()).isEqualTo(2); + assertThat(serverFrames.get()).isEqualTo(3); source.addToReceivedBuffer(errorFrame(0).retain()); - assertEquals(2, clientFrames.get()); - assertEquals(4, serverFrames.get()); + assertThat(clientFrames.get()).isEqualTo(2); + assertThat(serverFrames.get()).isEqualTo(4); source.addToReceivedBuffer(metadataPushFrame().retain()); - assertEquals(3, clientFrames.get()); - assertEquals(4, serverFrames.get()); + assertThat(clientFrames.get()).isEqualTo(3); + assertThat(serverFrames.get()).isEqualTo(4); } private ByteBuf leaseFrame() { diff --git a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java index 03a8a2eb3..9085f1d8f 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -15,11 +15,6 @@ * limitations under the License. */ -import static io.rsocket.frame.FrameHeaderCodec.frameType; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.Matchers.hasSize; - import io.netty.buffer.ByteBuf; import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; @@ -37,7 +32,6 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Collection; -import java.util.List; import java.util.Map; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicBoolean; @@ -52,7 +46,6 @@ 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 reactor.core.Disposable; import reactor.core.publisher.Flux; @@ -64,6 +57,7 @@ import reactor.test.publisher.TestPublisher; import reactor.test.util.RaceTestUtils; import reactor.util.context.Context; +import reactor.util.context.ContextView; import reactor.util.retry.Retry; public class DefaultRSocketClientTests { @@ -75,13 +69,7 @@ public void setUp() throws Throwable { Hooks.onNextDropped(ReferenceCountUtil::safeRelease); Hooks.onErrorDropped((t) -> {}); rule = new ClientSocketRule(); - rule.apply( - new Statement() { - @Override - public void evaluate() {} - }, - null) - .evaluate(); + rule.init(); } @AfterEach @@ -179,19 +167,12 @@ public void shouldSentFrameOnResolution( @MethodSource("interactions") @SuppressWarnings({"unchecked", "rawtypes"}) public void shouldHaveNoLeaksOnPayloadInCaseOfRacingOfOnNextAndCancel( - BiFunction, Publisher> request, FrameType requestType) - throws Throwable { + BiFunction, Publisher> request, FrameType requestType) { Assumptions.assumeThat(requestType).isNotEqualTo(FrameType.REQUEST_CHANNEL); for (int i = 0; i < RaceTestConstants.REPEATS; i++) { ClientSocketRule rule = new ClientSocketRule(); - rule.apply( - new Statement() { - @Override - public void evaluate() {} - }, - null) - .evaluate(); + rule.init(); Payload payload = ByteBufPayload.create("test", "testMetadata"); TestPublisher testPublisher = TestPublisher.createNoncompliant(TestPublisher.Violation.DEFER_CANCELLATION); @@ -241,19 +222,12 @@ public void evaluate() {} @MethodSource("interactions") @SuppressWarnings({"unchecked", "rawtypes"}) public void shouldHaveNoLeaksOnPayloadInCaseOfRacingOfRequestAndCancel( - BiFunction, Publisher> request, FrameType requestType) - throws Throwable { + BiFunction, Publisher> request, FrameType requestType) { Assumptions.assumeThat(requestType).isNotEqualTo(FrameType.REQUEST_CHANNEL); for (int i = 0; i < RaceTestConstants.REPEATS; i++) { ClientSocketRule rule = new ClientSocketRule(); - rule.apply( - new Statement() { - @Override - public void evaluate() {} - }, - null) - .evaluate(); + rule.init(); ByteBuf dataBuffer = rule.allocator.buffer(); dataBuffer.writeCharSequence("test", CharsetUtil.UTF_8); @@ -311,14 +285,17 @@ public void shouldPropagateDownstreamContext( Payload payload = ByteBufPayload.create(dataBuffer, metadataBuffer); AssertSubscriber assertSubscriber = new AssertSubscriber(Context.of("test", "test")); - Context[] receivedContext = new Context[1]; + ContextView[] receivedContext = new Context[1]; Publisher publisher = request.apply( rule.client, Mono.just(payload) .mergeWith( - Mono.subscriberContext() - .doOnNext(c -> receivedContext[0] = c) + Mono.deferContextual( + c -> { + receivedContext[0] = c; + return Mono.empty(); + }) .then(Mono.empty()))); publisher.subscribe(assertSubscriber); @@ -481,16 +458,11 @@ public void shouldDisposeOriginalSource() { } @Test - public void shouldDisposeOriginalSourceIfRacing() throws Throwable { + public void shouldDisposeOriginalSourceIfRacing() { for (int i = 0; i < RaceTestConstants.REPEATS; i++) { ClientSocketRule rule = new ClientSocketRule(); - rule.apply( - new Statement() { - @Override - public void evaluate() {} - }, - null) - .evaluate(); + + rule.init(); AssertSubscriber assertSubscriber = AssertSubscriber.create(); rule.client.source().subscribe(assertSubscriber); @@ -520,8 +492,8 @@ public static class ClientSocketRule extends AbstractSocketRule producer; @Override - protected void init() { - super.init(); + protected void doInit() { + super.doInit(); delayer = () -> producer.tryEmitValue(socket); producer = Sinks.one(); client = @@ -547,22 +519,5 @@ protected RSocketRequester newRSocket() { __ -> null, null); } - - public int getStreamIdForRequestType(FrameType expectedFrameType) { - assertThat("Unexpected frames sent.", connection.getSent(), hasSize(greaterThanOrEqualTo(1))); - List framesFound = new ArrayList<>(); - for (ByteBuf frame : connection.getSent()) { - FrameType frameType = frameType(frame); - if (frameType == expectedFrameType) { - return FrameHeaderCodec.streamId(frame); - } - framesFound.add(frameType); - } - throw new AssertionError( - "No frames sent with frame type: " - + expectedFrameType - + ", frames found: " - + framesFound); - } } } 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 34810b6bd..8c662d67d 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java @@ -15,7 +15,7 @@ */ package io.rsocket.core; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; import io.rsocket.RSocket; import io.rsocket.test.util.TestClientTransport; @@ -49,7 +49,7 @@ public void shouldBeASharedReconnectableInstanceOfRSocketMono() throws Interrupt RSocket rSocket1 = rSocketMono.block(); RSocket rSocket2 = rSocketMono.block(); - Assertions.assertThat(rSocket1).isEqualTo(rSocket2); + assertThat(rSocket1).isEqualTo(rSocket2); testClientTransport[0].testConnection().dispose(); testClientTransport[0] = new TestClientTransport(); @@ -57,7 +57,7 @@ public void shouldBeASharedReconnectableInstanceOfRSocketMono() throws Interrupt RSocket rSocket3 = rSocketMono.block(); RSocket rSocket4 = rSocketMono.block(); - Assertions.assertThat(rSocket3).isEqualTo(rSocket4).isNotEqualTo(rSocket2); + assertThat(rSocket3).isEqualTo(rSocket4).isNotEqualTo(rSocket2); } @Test @@ -81,7 +81,7 @@ public void shouldBeRetrieableConnectionSharedReconnectableInstanceOfRSocketMono RSocket rSocket1 = rSocketMono.block(); RSocket rSocket2 = rSocketMono.block(); - Assertions.assertThat(rSocket1).isEqualTo(rSocket2); + assertThat(rSocket1).isEqualTo(rSocket2); assertRetries( UncheckedIOException.class, UncheckedIOException.class, @@ -131,17 +131,17 @@ public void shouldBeNotBeASharedReconnectableInstanceOfRSocketMono() { RSocket rSocket1 = rSocketMono.block(); RSocket rSocket2 = rSocketMono.block(); - Assertions.assertThat(rSocket1).isNotEqualTo(rSocket2); + assertThat(rSocket1).isNotEqualTo(rSocket2); } @SafeVarargs private final void assertRetries(Class... exceptions) { - assertEquals(exceptions.length, retries.size()); + assertThat(retries.size()).isEqualTo(exceptions.length); 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()); + assertThat(retryContext.totalRetries()).isEqualTo(index); + assertThat(retryContext.failure().getClass()).isEqualTo(exceptions[index]); index++; } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java index de6f86c57..6ccff3701 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java @@ -8,27 +8,27 @@ import java.time.Duration; import java.util.Arrays; import java.util.function.Function; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +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; -@RunWith(Parameterized.class) public class RSocketRequesterTerminationTest { - @Rule public final ClientSocketRule rule = new ClientSocketRule(); - private Function> interaction; + public final ClientSocketRule rule = new ClientSocketRule(); - public RSocketRequesterTerminationTest(Function> interaction) { - this.interaction = interaction; + @BeforeEach + public void setup() { + rule.init(); } - @Test - public void testCurrentStreamIsTerminatedOnConnectionClose() { + @ParameterizedTest + @MethodSource("rsocketInteractions") + public void testCurrentStreamIsTerminatedOnConnectionClose( + Function> interaction) { RSocketRequester rSocket = rule.socket; Mono.delay(Duration.ofSeconds(1)).doOnNext(v -> rule.connection.dispose()).subscribe(); @@ -38,8 +38,10 @@ public void testCurrentStreamIsTerminatedOnConnectionClose() { .verify(Duration.ofSeconds(5)); } - @Test - public void testSubsequentStreamIsTerminatedAfterConnectionClose() { + @ParameterizedTest + @MethodSource("rsocketInteractions") + public void testSubsequentStreamIsTerminatedAfterConnectionClose( + Function> interaction) { RSocketRequester rSocket = rule.socket; rule.connection.dispose(); @@ -48,14 +50,46 @@ public void testSubsequentStreamIsTerminatedAfterConnectionClose() { .verify(Duration.ofSeconds(5)); } - @Parameterized.Parameters public static Iterable>> rsocketInteractions() { EmptyPayload payload = EmptyPayload.INSTANCE; Publisher payloadStream = Flux.just(payload); - Function> resp = rSocket -> rSocket.requestResponse(payload); - Function> stream = rSocket -> rSocket.requestStream(payload); - Function> channel = rSocket -> rSocket.requestChannel(payloadStream); + Function> resp = + new Function>() { + @Override + public Mono apply(RSocket rSocket) { + return rSocket.requestResponse(payload); + } + + @Override + public String toString() { + return "Request Response"; + } + }; + Function> stream = + new Function>() { + @Override + public Flux apply(RSocket rSocket) { + return rSocket.requestStream(payload); + } + + @Override + public String toString() { + return "Request Stream"; + } + }; + Function> channel = + new Function>() { + @Override + public Flux apply(RSocket rSocket) { + return rSocket.requestChannel(payloadStream); + } + + @Override + public String toString() { + return "Request Channel"; + } + }; return Arrays.asList(resp, stream, channel); } 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 797c36560..f31b74800 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -96,7 +96,6 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; -import org.junit.runners.model.Statement; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -119,13 +118,7 @@ public void setUp() throws Throwable { Hooks.onNextDropped(ReferenceCountUtil::safeRelease); Hooks.onErrorDropped((t) -> {}); rule = new ClientSocketRule(); - rule.apply( - new Statement() { - @Override - public void evaluate() {} - }, - null) - .evaluate(); + rule.init(); } @AfterEach @@ -432,18 +425,8 @@ public void checkNoLeaksOnRacing( BiConsumer, ClientSocketRule> runner) { for (int i = 0; i < RaceTestConstants.REPEATS; i++) { ClientSocketRule clientSocketRule = new ClientSocketRule(); - try { - clientSocketRule - .apply( - new Statement() { - @Override - public void evaluate() {} - }, - null) - .evaluate(); - } catch (Throwable throwable) { - throwable.printStackTrace(); - } + + clientSocketRule.init(); Publisher payloadP = initiator.apply(clientSocketRule); AssertSubscriber assertSubscriber = AssertSubscriber.create(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 efa07213e..4c44d827d 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -88,7 +88,6 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; -import org.junit.runners.model.Statement; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; @@ -107,17 +106,11 @@ public class RSocketResponderTest { ServerSocketRule rule; @BeforeEach - public void setUp() throws Throwable { + public void setUp() { Hooks.onNextDropped(ReferenceCountUtil::safeRelease); Hooks.onErrorDropped(t -> {}); rule = new ServerSocketRule(); - rule.apply( - new Statement() { - @Override - public void evaluate() {} - }, - null) - .evaluate(); + rule.init(); } @AfterEach @@ -129,7 +122,7 @@ public void tearDown() { @Test @Timeout(2_000) @Disabled - public void testHandleKeepAlive() throws Exception { + public void testHandleKeepAlive() { rule.connection.addToReceivedBuffer( KeepAliveFrameCodec.encode(rule.alloc(), true, 0, Unpooled.EMPTY_BUFFER)); ByteBuf sent = rule.connection.awaitFrame(); @@ -143,7 +136,7 @@ public void testHandleKeepAlive() throws Exception { @Test @Timeout(2_000) - public void testHandleResponseFrameNoError() throws Exception { + public void testHandleResponseFrameNoError() { final int streamId = 4; rule.connection.clearSendReceiveBuffers(); final TestPublisher testPublisher = TestPublisher.create(); @@ -165,7 +158,7 @@ public Mono requestResponse(Payload payload) { @Test @Timeout(2_000) - public void testHandlerEmitsError() throws Exception { + public void testHandlerEmitsError() { final int streamId = 4; rule.prefetch = 1; rule.sendRequest(streamId, FrameType.REQUEST_STREAM); @@ -309,9 +302,7 @@ public Flux requestChannel(Publisher payloads) { PayloadFrameCodec.encode(allocator, 1, false, true, true, metadata3, data3); RaceTestUtils.race( - () -> { - rule.connection.addToReceivedBuffer(nextFrame1, nextFrame2, nextFrame3); - }, + () -> rule.connection.addToReceivedBuffer(nextFrame1, nextFrame2, nextFrame3), () -> { assertSubscriber.cancel(); sink.tryEmitEmpty(); @@ -1200,7 +1191,7 @@ public static class ServerSocketRule extends AbstractSocketRule requestResponse(Payload payload) { return Mono.just(payload); } }; - super.init(); + super.doInit(); } public void setAcceptingSocket(RSocket acceptingSocket) { @@ -1216,12 +1207,12 @@ public void setAcceptingSocket(RSocket acceptingSocket) { connection = new TestDuplexConnection(alloc()); connectSub = TestSubscriber.create(); this.prefetch = Integer.MAX_VALUE; - super.init(); + super.doInit(); } public void setRequestInterceptor(RequestInterceptor requestInterceptor) { this.requestInterceptor = requestInterceptor; - super.init(); + super.doInit(); } public void setAcceptingSocket(RSocket acceptingSocket, int prefetch) { @@ -1229,7 +1220,7 @@ public void setAcceptingSocket(RSocket acceptingSocket, int prefetch) { connection = new TestDuplexConnection(alloc()); connectSub = TestSubscriber.create(); this.prefetch = prefetch; - super.init(); + super.doInit(); } @Override 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 073ebfd06..fd588cda3 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java @@ -3,7 +3,7 @@ import io.rsocket.test.util.TestClientTransport; import io.rsocket.test.util.TestServerTransport; import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.Test; public class RSocketServerFragmentationTest { 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 f502f2f88..c9904d583 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -35,11 +35,9 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicReference; import org.assertj.core.api.Assertions; -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.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import org.reactivestreams.Publisher; import reactor.core.Disposable; import reactor.core.Disposables; @@ -51,7 +49,12 @@ public class RSocketTest { - @Rule public final SocketRule rule = new SocketRule(); + public final SocketRule rule = new SocketRule(); + + @BeforeEach + public void setup() { + rule.init(); + } @Test public void rsocketDisposalShouldEndupWithNoErrorsOnClose() { @@ -81,7 +84,8 @@ public boolean isDisposed() { Assertions.assertThat(requestHandlingRSocket.isDisposed()).isTrue(); } - @Test(timeout = 2_000) + @Test + @Timeout(2_000) public void testRequestReplyNoError() { StepVerifier.create(rule.crs.requestResponse(DefaultPayload.create("hello"))) .expectNextCount(1) @@ -89,7 +93,8 @@ public void testRequestReplyNoError() { .verify(); } - @Test(timeout = 2000) + @Test + @Timeout(2000) public void testHandlerEmitsError() { rule.setRequestAcceptor( new RSocket() { @@ -109,7 +114,8 @@ public Mono requestResponse(Payload payload) { .verify(Duration.ofMillis(100)); } - @Test(timeout = 2000) + @Test + @Timeout(2000) public void testHandlerEmitsCustomError() { rule.setRequestAcceptor( new RSocket() { @@ -131,7 +137,8 @@ public Mono requestResponse(Payload payload) { .verify(); } - @Test(timeout = 2000) + @Test + @Timeout(2000) public void testRequestPropagatesCorrectlyForRequestChannel() { rule.setRequestAcceptor( new RSocket() { @@ -140,7 +147,7 @@ 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); + .take(3, true); } }); @@ -154,21 +161,24 @@ public Flux requestChannel(Publisher payloads) { .verify(Duration.ofMillis(5000)); } - @Test(timeout = 2000) - public void testStream() throws Exception { + @Test + @Timeout(2000) + public void testStream() { Flux responses = rule.crs.requestStream(DefaultPayload.create("Payload In")); StepVerifier.create(responses).expectNextCount(10).expectComplete().verify(); } - @Test(timeout = 200000) - public void testChannel() throws Exception { + @Test + @Timeout(200000) + public void testChannel() { Flux requests = Flux.range(0, 10).map(i -> DefaultPayload.create("streaming in -> " + i)); Flux responses = rule.crs.requestChannel(requests); StepVerifier.create(responses).expectNextCount(10).expectComplete().verify(); } - @Test(timeout = 2000) + @Test + @Timeout(2000) public void testErrorPropagatesCorrectly() { AtomicReference error = new AtomicReference<>(); rule.setRequestAcceptor( @@ -487,7 +497,7 @@ void errorFromRequesterPublisher( responderPublisher.assertNoSubscribers(); } - public static class SocketRule extends ExternalResource { + public static class SocketRule { Sinks.Many serverProcessor; Sinks.Many clientProcessor; @@ -500,22 +510,11 @@ public static class SocketRule extends ExternalResource { private LeaksTrackingByteBufAllocator allocator; - @Override - public Statement apply(Statement base, Description description) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - init(); - base.evaluate(); - } - }; - } - public LeaksTrackingByteBufAllocator alloc() { return allocator; } - protected void init() { + public void init() { allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); serverProcessor = Sinks.many().multicast().directBestEffort(); clientProcessor = Sinks.many().multicast().directBestEffort(); @@ -540,8 +539,7 @@ public Mono requestResponse(Payload payload) { @Override public Flux requestStream(Payload payload) { return Flux.range(1, 10) - .map( - i -> DefaultPayload.create("server got -> [" + payload.toString() + "]")); + .map(i -> DefaultPayload.create("server got -> [" + payload + "]")); } @Override diff --git a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java index 47c22cfb2..672141eaa 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java @@ -16,7 +16,7 @@ package io.rsocket.core; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; import io.rsocket.RaceTestConstants; import io.rsocket.internal.subscriber.AssertSubscriber; @@ -34,7 +34,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; @@ -81,8 +81,8 @@ public void subscribe(CoreSubscriber actual) { final AssertSubscriber subscriber = reconnectMono.subscribeWith(new AssertSubscriber<>()); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); RaceTestUtils.race(() -> monoSubscribers[0].onNext("value" + index), reconnectMono::dispose); @@ -96,7 +96,7 @@ public void subscribe(CoreSubscriber actual) { .assertError(CancellationException.class) .assertErrorMessage("ReconnectMono has already been disposed"); - Assertions.assertThat(expired).containsOnly("value" + i); + assertThat(expired).containsOnly("value" + i); } else { subscriber.assertValues("value" + i); } @@ -120,8 +120,8 @@ public void shouldNotifyAllTheSubscribersUnderRacingBetweenSubscribeAndComplete( reconnectMono.subscribeWith(new AssertSubscriber<>()); final AssertSubscriber raceSubscriber = new AssertSubscriber<>(); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); cold.next("value" + i); @@ -131,19 +131,16 @@ public void shouldNotifyAllTheSubscribersUnderRacingBetweenSubscribeAndComplete( subscriber.assertValues("value" + i); raceSubscriber.assertValues("value" + i); - Assertions.assertThat(reconnectMono.resolvingInner.subscribers) - .isEqualTo(ResolvingOperator.READY); + assertThat(reconnectMono.resolvingInner.subscribers).isEqualTo(ResolvingOperator.READY); - Assertions.assertThat( + assertThat( reconnectMono.resolvingInner.add( new ResolvingOperator.MonoDeferredResolutionOperator<>( reconnectMono.resolvingInner, subscriber))) .isEqualTo(ResolvingOperator.READY_STATE); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received) - .hasSize(1) - .containsOnly(Tuples.of("value" + i, reconnectMono)); + assertThat(expired).isEmpty(); + assertThat(received).hasSize(1).containsOnly(Tuples.of("value" + i, reconnectMono)); received.clear(); } @@ -164,8 +161,8 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() reconnectMono.subscribeWith(new AssertSubscriber<>()); final AssertSubscriber raceSubscriber = new AssertSubscriber<>(); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); reconnectMono.resolvingInner.mainSubscriber.onNext("value_to_expire" + i); reconnectMono.resolvingInner.mainSubscriber.onComplete(); @@ -186,20 +183,20 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() raceSubscriber.assertComplete(); String v = raceSubscriber.values().get(0); if (reconnectMono.resolvingInner.subscribers == ResolvingOperator.READY) { - Assertions.assertThat(v).isEqualTo("value_to_not_expire" + index); + assertThat(v).isEqualTo("value_to_not_expire" + index); } else { - Assertions.assertThat(v).isEqualTo("value_to_expire" + index); + assertThat(v).isEqualTo("value_to_expire" + index); } - Assertions.assertThat(expired).hasSize(1).containsOnly("value_to_expire" + i); + assertThat(expired).hasSize(1).containsOnly("value_to_expire" + i); if (reconnectMono.resolvingInner.subscribers == ResolvingOperator.READY) { - Assertions.assertThat(received) + assertThat(received) .hasSize(2) .containsExactly( Tuples.of("value_to_expire" + i, reconnectMono), Tuples.of("value_to_not_expire" + i, reconnectMono)); } else { - Assertions.assertThat(received) + assertThat(received) .hasSize(1) .containsOnly(Tuples.of("value_to_expire" + i, reconnectMono)); } @@ -224,8 +221,8 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates( reconnectMono.subscribeWith(new AssertSubscriber<>()); final AssertSubscriber raceSubscriber = new AssertSubscriber<>(); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); reconnectMono.resolvingInner.mainSubscriber.onNext("value_to_expire" + i); reconnectMono.resolvingInner.mainSubscriber.onComplete(); @@ -246,24 +243,24 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates( subscriber.assertValues("value_to_expire" + i); raceSubscriber.assertComplete(); - Assertions.assertThat(raceSubscriber.values().get(0)) + assertThat(raceSubscriber.values().get(0)) .isIn("value_to_possibly_expire" + index, "value_to_expire" + index); if (expired.size() == 2) { - Assertions.assertThat(expired) + assertThat(expired) .hasSize(2) .containsExactly("value_to_expire" + i, "value_to_possibly_expire" + i); } else { - Assertions.assertThat(expired).hasSize(1).containsOnly("value_to_expire" + i); + assertThat(expired).hasSize(1).containsOnly("value_to_expire" + i); } if (received.size() == 2) { - Assertions.assertThat(received) + assertThat(received) .hasSize(2) .containsExactly( Tuples.of("value_to_expire" + i, reconnectMono), Tuples.of("value_to_possibly_expire" + i, reconnectMono)); } else { - Assertions.assertThat(received) + assertThat(received) .hasSize(1) .containsOnly(Tuples.of("value_to_expire" + i, reconnectMono)); } @@ -299,19 +296,19 @@ public String get() { new ReconnectMono<>( source.subscribeOn(Schedulers.boundedElastic()), onExpire(), onValue()); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); final AssertSubscriber subscriber = reconnectMono.subscribeWith(new AssertSubscriber<>()); subscriber.await().assertComplete(); - Assertions.assertThat(expired).isEmpty(); + assertThat(expired).isEmpty(); RaceTestUtils.race( () -> - Assertions.assertThat(reconnectMono.block()) + assertThat(reconnectMono.block()) .matches( (v) -> v.equals("value_to_not_expire" + index) @@ -322,15 +319,15 @@ public String get() { subscriber.assertValues("value_to_expire" + i); - Assertions.assertThat(expired).hasSize(1).containsOnly("value_to_expire" + i); + assertThat(expired).hasSize(1).containsOnly("value_to_expire" + i); if (reconnectMono.resolvingInner.subscribers == ResolvingOperator.READY) { - Assertions.assertThat(received) + assertThat(received) .hasSize(2) .containsExactly( Tuples.of("value_to_expire" + i, reconnectMono), Tuples.of("value_to_not_expire" + i, reconnectMono)); } else { - Assertions.assertThat(received) + assertThat(received) .hasSize(1) .containsOnly(Tuples.of("value_to_expire" + i, reconnectMono)); } @@ -352,35 +349,32 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribers() { final AssertSubscriber subscriber = new AssertSubscriber<>(); final AssertSubscriber raceSubscriber = new AssertSubscriber<>(); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); - Assertions.assertThat(cold.subscribeCount()).isZero(); + assertThat(cold.subscribeCount()).isZero(); RaceTestUtils.race( () -> reconnectMono.subscribe(subscriber), () -> reconnectMono.subscribe(raceSubscriber)); subscriber.assertTerminated(); - Assertions.assertThat(raceSubscriber.isTerminated()).isTrue(); + assertThat(raceSubscriber.isTerminated()).isTrue(); subscriber.assertValues("value" + i); raceSubscriber.assertValues("value" + i); - Assertions.assertThat(reconnectMono.resolvingInner.subscribers) - .isEqualTo(ResolvingOperator.READY); + assertThat(reconnectMono.resolvingInner.subscribers).isEqualTo(ResolvingOperator.READY); - Assertions.assertThat(cold.subscribeCount()).isOne(); + assertThat(cold.subscribeCount()).isOne(); - Assertions.assertThat( + assertThat( reconnectMono.resolvingInner.add( new ResolvingOperator.MonoDeferredResolutionOperator<>( reconnectMono.resolvingInner, subscriber))) .isEqualTo(ResolvingOperator.READY_STATE); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received) - .hasSize(1) - .containsOnly(Tuples.of("value" + i, reconnectMono)); + assertThat(expired).isEmpty(); + assertThat(received).hasSize(1).containsOnly(Tuples.of("value" + i, reconnectMono)); received.clear(); } @@ -398,10 +392,10 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { final AssertSubscriber subscriber = new AssertSubscriber<>(); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); - Assertions.assertThat(cold.subscribeCount()).isZero(); + assertThat(cold.subscribeCount()).isZero(); String[] values = new String[1]; @@ -412,23 +406,20 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { subscriber.assertTerminated(); subscriber.assertValues("value" + i); - Assertions.assertThat(values).containsExactly("value" + i); + assertThat(values).containsExactly("value" + i); - Assertions.assertThat(reconnectMono.resolvingInner.subscribers) - .isEqualTo(ResolvingOperator.READY); + assertThat(reconnectMono.resolvingInner.subscribers).isEqualTo(ResolvingOperator.READY); - Assertions.assertThat(cold.subscribeCount()).isOne(); + assertThat(cold.subscribeCount()).isOne(); - Assertions.assertThat( + assertThat( reconnectMono.resolvingInner.add( new ResolvingOperator.MonoDeferredResolutionOperator<>( reconnectMono.resolvingInner, subscriber))) .isEqualTo(ResolvingOperator.READY_STATE); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received) - .hasSize(1) - .containsOnly(Tuples.of("value" + i, reconnectMono)); + assertThat(expired).isEmpty(); + assertThat(received).hasSize(1).containsOnly(Tuples.of("value" + i, reconnectMono)); received.clear(); } @@ -444,10 +435,10 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenBlocks() { final ReconnectMono reconnectMono = cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); - Assertions.assertThat(cold.subscribeCount()).isZero(); + assertThat(cold.subscribeCount()).isZero(); String[] values1 = new String[1]; String[] values2 = new String[1]; @@ -456,24 +447,21 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenBlocks() { () -> values1[0] = reconnectMono.block(timeout), () -> values2[0] = reconnectMono.block(timeout)); - Assertions.assertThat(values2).containsExactly("value" + i); - Assertions.assertThat(values1).containsExactly("value" + i); + assertThat(values2).containsExactly("value" + i); + assertThat(values1).containsExactly("value" + i); - Assertions.assertThat(reconnectMono.resolvingInner.subscribers) - .isEqualTo(ResolvingOperator.READY); + assertThat(reconnectMono.resolvingInner.subscribers).isEqualTo(ResolvingOperator.READY); - Assertions.assertThat(cold.subscribeCount()).isOne(); + assertThat(cold.subscribeCount()).isOne(); - Assertions.assertThat( + assertThat( reconnectMono.resolvingInner.add( new ResolvingOperator.MonoDeferredResolutionOperator<>( reconnectMono.resolvingInner, new AssertSubscriber<>()))) .isEqualTo(ResolvingOperator.READY_STATE); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received) - .hasSize(1) - .containsOnly(Tuples.of("value" + i, reconnectMono)); + assertThat(expired).isEmpty(); + assertThat(received).hasSize(1).containsOnly(Tuples.of("value" + i, reconnectMono)); received.clear(); } @@ -492,8 +480,8 @@ public void shouldExpireValueOnRacingDisposeAndNoValueComplete() { final AssertSubscriber subscriber = reconnectMono.subscribeWith(new AssertSubscriber<>()); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); RaceTestUtils.race(cold::complete, reconnectMono::dispose); @@ -502,16 +490,16 @@ public void shouldExpireValueOnRacingDisposeAndNoValueComplete() { Throwable error = subscriber.errors().get(0); if (error instanceof CancellationException) { - Assertions.assertThat(error) + assertThat(error) .isInstanceOf(CancellationException.class) .hasMessage("ReconnectMono has already been disposed"); } else { - Assertions.assertThat(error) + assertThat(error) .isInstanceOf(IllegalStateException.class) .hasMessage("Source completed empty"); } - Assertions.assertThat(expired).isEmpty(); + assertThat(expired).isEmpty(); expired.clear(); received.clear(); @@ -531,8 +519,8 @@ public void shouldExpireValueOnRacingDisposeAndComplete() { final AssertSubscriber subscriber = reconnectMono.subscribeWith(new AssertSubscriber<>()); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); cold.next("value" + i); @@ -541,17 +529,15 @@ public void shouldExpireValueOnRacingDisposeAndComplete() { subscriber.assertTerminated(); if (!subscriber.errors().isEmpty()) { - Assertions.assertThat(subscriber.errors().get(0)) + assertThat(subscriber.errors().get(0)) .isInstanceOf(CancellationException.class) .hasMessage("ReconnectMono has already been disposed"); } else { - Assertions.assertThat(received) - .hasSize(1) - .containsOnly(Tuples.of("value" + i, reconnectMono)); + assertThat(received).hasSize(1).containsOnly(Tuples.of("value" + i, reconnectMono)); subscriber.assertValues("value" + i); } - Assertions.assertThat(expired).hasSize(1).containsOnly("value" + i); + assertThat(expired).hasSize(1).containsOnly("value" + i); expired.clear(); received.clear(); @@ -572,8 +558,8 @@ public void shouldExpireValueOnRacingDisposeAndError() { final AssertSubscriber subscriber = reconnectMono.subscribeWith(new AssertSubscriber<>()); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); cold.next("value" + i); @@ -584,20 +570,18 @@ public void shouldExpireValueOnRacingDisposeAndError() { if (!subscriber.errors().isEmpty()) { Throwable error = subscriber.errors().get(0); if (error instanceof CancellationException) { - Assertions.assertThat(error) + assertThat(error) .isInstanceOf(CancellationException.class) .hasMessage("ReconnectMono has already been disposed"); } else { - Assertions.assertThat(error).isInstanceOf(RuntimeException.class).hasMessage("test"); + assertThat(error).isInstanceOf(RuntimeException.class).hasMessage("test"); } } else { - Assertions.assertThat(received) - .hasSize(1) - .containsOnly(Tuples.of("value" + i, reconnectMono)); + assertThat(received).hasSize(1).containsOnly(Tuples.of("value" + i, reconnectMono)); subscriber.assertValues("value" + i); } - Assertions.assertThat(expired).hasSize(1).containsOnly("value" + i); + assertThat(expired).hasSize(1).containsOnly("value" + i); expired.clear(); received.clear(); @@ -620,8 +604,8 @@ public void shouldExpireValueOnRacingDisposeAndErrorWithNoBackoff() { final AssertSubscriber subscriber = reconnectMono.subscribeWith(new AssertSubscriber<>()); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); cold.next("value" + i); @@ -632,20 +616,16 @@ public void shouldExpireValueOnRacingDisposeAndErrorWithNoBackoff() { if (!subscriber.errors().isEmpty()) { Throwable error = subscriber.errors().get(0); if (error instanceof CancellationException) { - Assertions.assertThat(error) + assertThat(error) .isInstanceOf(CancellationException.class) .hasMessage("ReconnectMono has already been disposed"); } else { - Assertions.assertThat(error) - .matches(Exceptions::isRetryExhausted) - .hasCause(runtimeException); + assertThat(error).matches(Exceptions::isRetryExhausted).hasCause(runtimeException); } - Assertions.assertThat(expired).hasSize(1).containsOnly("value" + i); + assertThat(expired).hasSize(1).containsOnly("value" + i); } else { - Assertions.assertThat(received) - .hasSize(1) - .containsOnly(Tuples.of("value" + i, reconnectMono)); + assertThat(received).hasSize(1).containsOnly(Tuples.of("value" + i, reconnectMono)); subscriber.assertValues("value" + i); } @@ -694,21 +674,20 @@ public void shouldBeScannable() { final Scannable scannableOfReconnect = Scannable.from(reconnectMono); - Assertions.assertThat( + 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(); + assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.TERMINATED)).isEqualTo(false); + assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.ERROR)).isNull(); final AssertSubscriber subscriber = reconnectMono.subscribeWith(new AssertSubscriber<>()); final Scannable scannableOfMonoProcessor = Scannable.from(subscriber); - Assertions.assertThat( + assertThat( (List) scannableOfMonoProcessor .parents() @@ -723,9 +702,8 @@ public void shouldBeScannable() { reconnectMono.dispose(); - Assertions.assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.TERMINATED)) - .isEqualTo(true); - Assertions.assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.ERROR)) + assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.TERMINATED)).isEqualTo(true); + assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.ERROR)) .isInstanceOf(CancellationException.class); } @@ -741,32 +719,32 @@ public void shouldNotExpiredIfNotCompleted() { reconnectMono.subscribe(subscriber); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(subscriber.isTerminated()).isFalse(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(subscriber.isTerminated()).isFalse(); publisher.next("test"); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(subscriber.isTerminated()).isFalse(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(subscriber.isTerminated()).isFalse(); reconnectMono.invalidate(); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(subscriber.isTerminated()).isFalse(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(subscriber.isTerminated()).isFalse(); publisher.assertSubscribers(1); - Assertions.assertThat(publisher.subscribeCount()).isEqualTo(1); + assertThat(publisher.subscribeCount()).isEqualTo(1); publisher.complete(); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).hasSize(1); + assertThat(expired).isEmpty(); + assertThat(received).hasSize(1); subscriber.assertTerminated(); publisher.assertSubscribers(0); - Assertions.assertThat(publisher.subscribeCount()).isEqualTo(1); + assertThat(publisher.subscribeCount()).isEqualTo(1); } @Test @@ -781,20 +759,20 @@ public void shouldNotEmitUntilCompletion() { reconnectMono.subscribe(subscriber); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(subscriber.isTerminated()).isFalse(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(subscriber.isTerminated()).isFalse(); publisher.next("test"); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(subscriber.isTerminated()).isFalse(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(subscriber.isTerminated()).isFalse(); publisher.complete(); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).hasSize(1); + assertThat(expired).isEmpty(); + assertThat(received).hasSize(1); subscriber.assertTerminated(); subscriber.assertValues("test"); } @@ -811,26 +789,26 @@ public void shouldBePossibleToRemoveThemSelvesFromTheList_CancellationTest() { reconnectMono.subscribe(subscriber); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(subscriber.isTerminated()).isFalse(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(subscriber.isTerminated()).isFalse(); publisher.next("test"); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(subscriber.isTerminated()).isFalse(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(subscriber.isTerminated()).isFalse(); subscriber.cancel(); - Assertions.assertThat(reconnectMono.resolvingInner.subscribers) + assertThat(reconnectMono.resolvingInner.subscribers) .isEqualTo(ResolvingOperator.EMPTY_SUBSCRIBED); publisher.complete(); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).hasSize(1); - Assertions.assertThat(subscriber.values()).isEmpty(); + assertThat(expired).isEmpty(); + assertThat(received).hasSize(1); + assertThat(subscriber.values()).isEmpty(); } @Test @@ -849,14 +827,14 @@ public void shouldExpireValueOnDispose() { .expectComplete() .verify(Duration.ofSeconds(timeout)); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).hasSize(1); + assertThat(expired).isEmpty(); + assertThat(received).hasSize(1); reconnectMono.dispose(); - Assertions.assertThat(expired).hasSize(1); - Assertions.assertThat(received).hasSize(1); - Assertions.assertThat(reconnectMono.isDisposed()).isTrue(); + assertThat(expired).hasSize(1); + assertThat(received).hasSize(1); + assertThat(reconnectMono.isDisposed()).isTrue(); StepVerifier.create(reconnectMono.subscribeOn(Schedulers.boundedElastic())) .expectSubscription() @@ -881,7 +859,7 @@ public void shouldNotifyAllTheSubscribers() { reconnectMono.subscribe(sub3); reconnectMono.subscribe(sub4); - Assertions.assertThat(reconnectMono.resolvingInner.subscribers).hasSize(4); + assertThat(reconnectMono.resolvingInner.subscribers).hasSize(4); final ArrayList> subscribers = new ArrayList<>(200); @@ -893,27 +871,25 @@ public void shouldNotifyAllTheSubscribers() { RaceTestUtils.race(() -> reconnectMono.subscribe(subA), () -> reconnectMono.subscribe(subB)); } - Assertions.assertThat(reconnectMono.resolvingInner.subscribers) - .hasSize(RaceTestConstants.REPEATS * 2 + 4); + assertThat(reconnectMono.resolvingInner.subscribers).hasSize(RaceTestConstants.REPEATS * 2 + 4); sub1.cancel(); - Assertions.assertThat(reconnectMono.resolvingInner.subscribers) - .hasSize(RaceTestConstants.REPEATS * 2 + 3); + assertThat(reconnectMono.resolvingInner.subscribers).hasSize(RaceTestConstants.REPEATS * 2 + 3); publisher.next("value"); - Assertions.assertThat(sub1.scan(Scannable.Attr.CANCELLED)).isTrue(); - Assertions.assertThat(sub2.values().get(0)).isEqualTo("value"); - Assertions.assertThat(sub3.values().get(0)).isEqualTo("value"); - Assertions.assertThat(sub4.values().get(0)).isEqualTo("value"); + assertThat(sub1.scan(Scannable.Attr.CANCELLED)).isTrue(); + assertThat(sub2.values().get(0)).isEqualTo("value"); + assertThat(sub3.values().get(0)).isEqualTo("value"); + assertThat(sub4.values().get(0)).isEqualTo("value"); for (AssertSubscriber sub : subscribers) { - Assertions.assertThat(sub.values().get(0)).isEqualTo("value"); - Assertions.assertThat(sub.isTerminated()).isTrue(); + assertThat(sub.values().get(0)).isEqualTo("value"); + assertThat(sub.isTerminated()).isTrue(); } - Assertions.assertThat(publisher.subscribeCount()).isEqualTo(1); + assertThat(publisher.subscribeCount()).isEqualTo(1); } @Test @@ -936,13 +912,13 @@ public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidates() { .expectComplete() .verify(Duration.ofSeconds(timeout)); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); + assertThat(expired).isEmpty(); + 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)); + assertThat(expired).hasSize(1).containsOnly("value"); + assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); cold.next("value2"); @@ -952,12 +928,12 @@ public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidates() { .expectComplete() .verify(Duration.ofSeconds(timeout)); - Assertions.assertThat(expired).hasSize(1).containsOnly("value"); - Assertions.assertThat(received) + assertThat(expired).hasSize(1).containsOnly("value"); + assertThat(received) .hasSize(2) .containsOnly(Tuples.of("value", reconnectMono), Tuples.of("value2", reconnectMono)); - Assertions.assertThat(cold.subscribeCount()).isEqualTo(2); + assertThat(cold.subscribeCount()).isEqualTo(2); expired.clear(); received.clear(); @@ -980,23 +956,23 @@ public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidateAndDispose() { .expectComplete() .verify(Duration.ofSeconds(timeout)); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); + assertThat(expired).isEmpty(); + assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); RaceTestUtils.race(reconnectMono::invalidate, reconnectMono::dispose); - Assertions.assertThat(expired).hasSize(1).containsOnly("value"); - Assertions.assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); + assertThat(expired).hasSize(1).containsOnly("value"); + assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); StepVerifier.create(reconnectMono.subscribeOn(Schedulers.boundedElastic())) .expectSubscription() .expectError(CancellationException.class) .verify(Duration.ofSeconds(timeout)); - Assertions.assertThat(expired).hasSize(1).containsOnly("value"); - Assertions.assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); + assertThat(expired).hasSize(1).containsOnly("value"); + assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); - Assertions.assertThat(cold.subscribeCount()).isEqualTo(1); + assertThat(cold.subscribeCount()).isEqualTo(1); expired.clear(); received.clear(); @@ -1026,8 +1002,8 @@ public void shouldTimeoutRetryWithVirtualTime() { .expectError(TimeoutException.class) .verify(Duration.ofSeconds(timeout)); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); } @Test @@ -1040,7 +1016,7 @@ public void ensuresThatMainSubscriberAllowsOnlyTerminationWithValue() { .expectSubscription() .expectErrorSatisfies( t -> - Assertions.assertThat(t) + assertThat(t) .hasMessage("Source completed empty") .isInstanceOf(IllegalStateException.class)) .verify(Duration.ofSeconds(timeout)); @@ -1056,8 +1032,8 @@ public void monoRetryNoBackoff() { StepVerifier.create(mono).verifyErrorMatches(Exceptions::isRetryExhausted); assertRetries(IOException.class, IOException.class); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); } @Test @@ -1075,8 +1051,8 @@ public void monoRetryFixedBackoff() { assertRetries(IOException.class); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); } @Test @@ -1100,8 +1076,8 @@ public void monoRetryExponentialBackoff() { assertRetries(IOException.class, IOException.class, IOException.class, IOException.class); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); } Consumer onRetry() { @@ -1118,12 +1094,12 @@ Consumer onExpire() { @SafeVarargs private final void assertRetries(Class... exceptions) { - assertEquals(exceptions.length, retries.size()); + assertThat(retries.size()).isEqualTo(exceptions.length); 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()); + assertThat(retryContext.totalRetries()).isEqualTo(index); + assertThat(retryContext.failure().getClass()).isEqualTo(exceptions[index]); index++; } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/StreamIdSupplierTest.java b/rsocket-core/src/test/java/io/rsocket/core/StreamIdSupplierTest.java index 98fde97f7..16bd9f16e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/StreamIdSupplierTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/StreamIdSupplierTest.java @@ -16,22 +16,23 @@ package io.rsocket.core; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import io.netty.util.collection.IntObjectHashMap; import io.netty.util.collection.IntObjectMap; -import org.junit.Test; +import org.junit.jupiter.api.Test; public class StreamIdSupplierTest { @Test public void testClientSequence() { IntObjectMap map = new IntObjectHashMap<>(); StreamIdSupplier s = StreamIdSupplier.clientSupplier(); - assertEquals(1, s.nextStreamId(map)); - assertEquals(3, s.nextStreamId(map)); - assertEquals(5, s.nextStreamId(map)); + assertThat(s.nextStreamId(map)).isEqualTo(1); + assertThat(s.nextStreamId(map)).isEqualTo(3); + assertThat(s.nextStreamId(map)).isEqualTo(5); } @Test diff --git a/rsocket-core/src/test/java/io/rsocket/core/TestingStuff.java b/rsocket-core/src/test/java/io/rsocket/core/TestingStuff.java deleted file mode 100644 index e0ebf5064..000000000 --- a/rsocket-core/src/test/java/io/rsocket/core/TestingStuff.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.rsocket.core; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import org.junit.Test; - -public class TestingStuff { - private String f = "00000001110000000068656c6c6f"; - private String f1 = - "00000001286004232e667127bb590fb097cf657776761dcdfe84863f67da47e9de9ac72197424116aae3aadf9e4d8347d8f7923107f7eacb56f741c82327e9c4cbe23e92b5afc306aa24a2153e27082ba0bb1e707bed43100ea84a87539a23442e10431584a42f6eb78fdf140fb14ee71cf4a23ad644dcd3ebceb86e4d0617aa0d2cfee56ce1afea5062842580275b5fdc96cae1bbe9f3a129148dbe1dfc44c8e11aa6a7ec8dafacbbdbd0b68731c16bd16c21eb857c9eb1bb6c359415674b6d41d14e99b1dd56a40fc836d723dd534e83c44d6283745332c627e13bcfc2cd483bccec67232fff0b2ccb7388f0d37da27562d7c3635fef061767400e45729bdef57ca8c041e33074ea1a42004c1b8cb02eb3afeaf5f6d82162d4174c549f840bdb88632faf2578393194f67538bf581a22f31850f88831af632bdaf32c80aa6d96a7afc20b8067c4f9d17859776e4c40beafff18a848df45927ca1c9b024ef278a9fb60bdf965b5822b64bebc74a8b7d95a4bd9d1c1fc82b4fbacc29e36458a878079ddd402788a462528d6c79df797218563cc70811c09b154588a3edd2e948bb61db7b3a36774e0bd5ab67fec4bf1e70811733213f292a728389473b9f68d288ac481529e10cfd428b14ad3f4592d1cc6dd08b1a7842bb492b51057c4d88ac5d538174560f94b49dce6d20ef71671d2e80c2b92ead6d4a26ed8f4187a563cb53eb0c558fe9f77b2133e835e2d2e671978e82a6f60ed61a6a945e39fe0dedcf73d7cb80253a5eb9f311c78ef2587649436f4ab42bcc882faba7bfd57d451407a07ce1d5ac7b5f0cf1ef84047c92e3fbdb64128925ef6e87def450ae8a1643e9906b7dc1f672bd98e012df3039f2ee412909f4b03db39f45b83955f31986b6fd3b5e4f26b6ec2284dcf55ff5fbbfbfb31cd6b22753c6435dbd3ec5558132c6ede9babd7945ac6e697d28b9697f9b2450db2b643a1abc4c9ad5bfa4529d0e1f261df1da5ee035738a5d8c536466fa741e9190c58cf1cacc819838a6b20d85f901f026c66dbaf23cde3a12ce4b443ef15cc247ba48cc0812c6f2c834c5773f3d4042219727404f0f2640cab486e298ae9f1c2f7a7e6f0619f130895d9f41d343fbdb05d68d6e0308d8d046314811066a13300b1346b8762919d833de7f55fea919ad55500ba4ec7e100b32bbabbf9d378eab61532fd91d4d1977db72b828e8d700062b045459d7729f140d889a67472a035d564384844ff16697743e4017e2bf21511ebb4c939bbab202bf6ef59e2be557027272f1bb21c325cf3e0432120bccba17bea52a7621031466e7973415437cd50cc950e63e6e2d17aad36f7a943892901e763e19082260b88f8971b35b4d9cc8725d6e4137b4648427ae68255e076dfb511871de0f7100d2ece6c8be88a0326ba8d73b5c9883f83c0dccd362e61cb16c7a0cc5ff00f7"; - private String f2 = "00000003110000000068656c6c6f"; - - @Test - public void testStuff() { - ByteBuf byteBuf = Unpooled.wrappedBuffer(ByteBufUtil.decodeHexDump(f1)); - System.out.println(ByteBufUtil.prettyHexDump(byteBuf)); - - new DefaultConnectionSetupPayload(byteBuf); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameCodecTest.java index fe05335d2..4815bfb8e 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameCodecTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameCodecTest.java @@ -16,11 +16,12 @@ package io.rsocket.frame; +import static org.assertj.core.api.Assertions.assertThat; + 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; public class ResumeFrameCodecTest { @@ -31,10 +32,10 @@ void testEncoding() { Arrays.fill(tokenBytes, (byte) 1); ByteBuf token = Unpooled.wrappedBuffer(tokenBytes); 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)); + assertThat(ResumeFrameCodec.version(byteBuf)).isEqualTo(ResumeFrameCodec.CURRENT_VERSION); + assertThat(ResumeFrameCodec.token(byteBuf)).isEqualTo(token); + assertThat(ResumeFrameCodec.lastReceivedServerPos(byteBuf)).isEqualTo(21); + assertThat(ResumeFrameCodec.firstAvailableClientPos(byteBuf)).isEqualTo(12); byteBuf.release(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameCodecTest.java index 33dd8eb70..b818d579d 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameCodecTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameCodecTest.java @@ -1,16 +1,17 @@ package io.rsocket.frame; +import static org.assertj.core.api.Assertions.assertThat; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Test; public class ResumeOkFrameCodecTest { @Test public void testEncoding() { ByteBuf byteBuf = ResumeOkFrameCodec.encode(ByteBufAllocator.DEFAULT, 42); - Assert.assertEquals(42, ResumeOkFrameCodec.lastReceivedClientPos(byteBuf)); + assertThat(ResumeOkFrameCodec.lastReceivedClientPos(byteBuf)).isEqualTo(42); byteBuf.release(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java b/rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java index ec8725b1e..9ebca34f7 100644 --- a/rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java +++ b/rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java @@ -16,8 +16,6 @@ package io.rsocket.lease; -import static org.junit.Assert.*; - public class LeaseImplTest { // // @Test diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java index a39caed2b..9227bcaca 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java @@ -21,7 +21,7 @@ import io.netty.buffer.ByteBufAllocator; import java.util.List; import org.assertj.core.util.Lists; -import org.junit.Test; +import org.junit.jupiter.api.Test; /** Unit tests for {@link MimeTypeMetadataCodec}. */ public class MimeTypeMetadataCodecTest { 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 3f97ab9dc..f04de78b6 100644 --- a/rsocket-core/src/test/java/io/rsocket/util/DefaultPayloadTest.java +++ b/rsocket-core/src/test/java/io/rsocket/util/DefaultPayloadTest.java @@ -16,8 +16,7 @@ package io.rsocket.util; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; +import static org.assertj.core.api.Assertions.assertThat; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -26,8 +25,7 @@ import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import java.nio.ByteBuffer; import java.util.concurrent.ThreadLocalRandom; -import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.Test; public class DefaultPayloadTest { public static final String DATA_VAL = "data"; @@ -41,12 +39,12 @@ public void testReuse() { } public void assertDataAndMetadata(Payload p, String dataVal, String metadataVal) { - assertThat("Unexpected data.", p.getDataUtf8(), equalTo(dataVal)); + assertThat(p.getDataUtf8()).describedAs("Unexpected data.").isEqualTo(dataVal); if (metadataVal == null) { - assertThat("Non-null metadata", p.hasMetadata(), equalTo(false)); + assertThat(p.hasMetadata()).describedAs("Non-null metadata").isEqualTo(false); } else { - assertThat("Null metadata", p.hasMetadata(), equalTo(true)); - assertThat("Unexpected metadata.", p.getMetadataUtf8(), equalTo(metadataVal)); + assertThat(p.hasMetadata()).describedAs("Null metadata").isEqualTo(true); + assertThat(p.getMetadataUtf8()).describedAs("Unexpected metadata.").isEqualTo(metadataVal); } } @@ -60,7 +58,7 @@ public void staticMethods() { public void shouldIndicateThatItHasNotMetadata() { Payload payload = DefaultPayload.create("data"); - Assertions.assertThat(payload.hasMetadata()).isFalse(); + assertThat(payload.hasMetadata()).isFalse(); } @Test @@ -68,7 +66,7 @@ public void shouldIndicateThatItHasMetadata1() { Payload payload = DefaultPayload.create(Unpooled.wrappedBuffer("data".getBytes()), Unpooled.EMPTY_BUFFER); - Assertions.assertThat(payload.hasMetadata()).isTrue(); + assertThat(payload.hasMetadata()).isTrue(); } @Test @@ -76,7 +74,7 @@ public void shouldIndicateThatItHasMetadata2() { Payload payload = DefaultPayload.create(ByteBuffer.wrap("data".getBytes()), ByteBuffer.allocate(0)); - Assertions.assertThat(payload.hasMetadata()).isTrue(); + assertThat(payload.hasMetadata()).isTrue(); } @Test @@ -96,9 +94,9 @@ public void shouldReleaseGivenByteBufDataAndMetadataUpOnPayloadCreation() { Payload payload = DefaultPayload.create(data, metadata); - Assertions.assertThat(payload.getData()).isEqualTo(ByteBuffer.wrap(new byte[] {i})); + assertThat(payload.getData()).isEqualTo(ByteBuffer.wrap(new byte[] {i})); - Assertions.assertThat(payload.getMetadata()) + assertThat(payload.getMetadata()) .isEqualTo( metadataPresent ? ByteBuffer.wrap(new byte[] {(byte) (i + 1)}) diff --git a/rsocket-examples/build.gradle b/rsocket-examples/build.gradle index e2a7ad31a..e5d74494f 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -34,10 +34,8 @@ dependencies { testImplementation 'org.assertj:assertj-core' testImplementation 'io.projectreactor:reactor-test' - // TODO: Remove after JUnit5 migration - testCompileOnly 'junit:junit' testImplementation 'org.hamcrest:hamcrest-library' - testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' } description = 'Example usage of the RSocket library' 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 e2471f2fc..ac311a231 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java @@ -16,9 +16,8 @@ package io.rsocket.integration; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -38,9 +37,10 @@ import io.rsocket.util.RSocketProxy; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import reactor.core.publisher.Flux; @@ -108,7 +108,7 @@ public Mono requestResponse(Payload payload) { private CountDownLatch disconnectionCounter; private AtomicInteger errorCount; - @Before + @BeforeEach public void startup() { errorCount = new AtomicInteger(); requestCount = new AtomicInteger(); @@ -163,23 +163,26 @@ public Flux requestChannel(Publisher payloads) { .block(); } - @After + @AfterEach public void teardown() { server.dispose(); } - @Test(timeout = 5_000L) + @Test + @Timeout(5_000L) public void testRequest() { client.requestResponse(DefaultPayload.create("REQUEST", "META")).block(); - assertThat("Server did not see the request.", requestCount.get(), is(1)); - assertTrue(calledRequester); - assertTrue(calledResponder); - assertTrue(calledClientAcceptor); - assertTrue(calledServerAcceptor); - assertTrue(calledFrame); + assertThat(requestCount).as("Server did not see the request.").hasValue(1); + + assertThat(calledRequester).isTrue(); + assertThat(calledResponder).isTrue(); + assertThat(calledClientAcceptor).isTrue(); + assertThat(calledServerAcceptor).isTrue(); + assertThat(calledFrame).isTrue(); } - @Test(timeout = 5_000L) + @Test + @Timeout(5_000L) public void testStream() { Subscriber subscriber = TestSubscriber.createCancelling(); client.requestStream(DefaultPayload.create("start")).subscribe(subscriber); @@ -188,7 +191,8 @@ public void testStream() { verifyNoMoreInteractions(subscriber); } - @Test(timeout = 5_000L) + @Test + @Timeout(5_000L) public void testClose() throws InterruptedException { client.dispose(); disconnectionCounter.await(); @@ -196,10 +200,8 @@ public void testClose() throws InterruptedException { @Test // (timeout = 5_000L) public void testCallRequestWithErrorAndThenRequest() { - try { - client.requestChannel(Mono.error(new Throwable())).blockLast(); - } catch (Throwable t) { - } + assertThatThrownBy(client.requestChannel(Mono.error(new Throwable("test")))::blockLast) + .hasMessage("java.lang.Throwable: test"); testRequest(); } 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 bad28f4dc..1924668fb 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java @@ -16,8 +16,7 @@ package io.rsocket.integration; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +import static org.assertj.core.api.Assertions.assertThat; import io.rsocket.Payload; import io.rsocket.RSocket; @@ -31,9 +30,10 @@ import io.rsocket.util.RSocketProxy; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -44,7 +44,7 @@ public class TcpIntegrationTest { private CloseableChannel server; - @Before + @BeforeEach public void startup() { server = RSocketServer.create((setup, sendingSocket) -> Mono.just(new RSocketProxy(handler))) @@ -56,12 +56,13 @@ private RSocket buildClient() { return RSocketConnector.connectWith(TcpClientTransport.create(server.address())).block(); } - @After + @AfterEach public void cleanup() { server.dispose(); } - @Test(timeout = 15_000L) + @Test + @Timeout(15_000L) public void testCompleteWithoutNext() { handler = new RSocket() { @@ -74,10 +75,11 @@ public Flux requestStream(Payload payload) { Boolean hasElements = client.requestStream(DefaultPayload.create("REQUEST", "META")).log().hasElements().block(); - assertFalse(hasElements); + assertThat(hasElements).isFalse(); } - @Test(timeout = 15_000L) + @Test + @Timeout(15_000L) public void testSingleStream() { handler = new RSocket() { @@ -91,10 +93,11 @@ public Flux requestStream(Payload payload) { Payload result = client.requestStream(DefaultPayload.create("REQUEST", "META")).blockLast(); - assertEquals("RESPONSE", result.getDataUtf8()); + assertThat(result.getDataUtf8()).isEqualTo("RESPONSE"); } - @Test(timeout = 15_000L) + @Test + @Timeout(15_000L) public void testZeroPayload() { handler = new RSocket() { @@ -108,10 +111,11 @@ public Flux requestStream(Payload payload) { Payload result = client.requestStream(DefaultPayload.create("REQUEST", "META")).blockFirst(); - assertEquals("", result.getDataUtf8()); + assertThat(result.getDataUtf8()).isEmpty(); } - @Test(timeout = 15_000L) + @Test + @Timeout(15_000L) public void testRequestResponseErrors() { handler = new RSocket() { @@ -141,11 +145,12 @@ public Mono requestResponse(Payload payload) { .onErrorReturn(DefaultPayload.create("ERROR")) .block(); - assertEquals("ERROR", response1.getDataUtf8()); - assertEquals("SUCCESS", response2.getDataUtf8()); + assertThat(response1.getDataUtf8()).isEqualTo("ERROR"); + assertThat(response2.getDataUtf8()).isEqualTo("SUCCESS"); } - @Test(timeout = 15_000L) + @Test + @Timeout(15_000L) public void testTwoConcurrentStreams() throws InterruptedException { ConcurrentHashMap> map = new ConcurrentHashMap<>(); Sinks.Many processor1 = Sinks.many().unicast().onBackpressureBuffer(); 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 7d34ba478..625f8fcb1 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java @@ -27,13 +27,14 @@ import io.rsocket.util.DefaultPayload; import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Test; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; public class TestingStreaming { LocalServerTransport serverTransport = LocalServerTransport.create("test"); - @Test(expected = ApplicationErrorException.class) + @Test public void testRangeButThrowException() { Closeable server = null; try { @@ -53,8 +54,9 @@ public void testRangeButThrowException() { .bind(serverTransport) .block(); - Flux.range(1, 6).flatMap(i -> consumer("connection number -> " + i)).blockLast(); - System.out.println("here"); + Assertions.assertThatThrownBy( + Flux.range(1, 6).flatMap(i -> consumer("connection number -> " + i))::blockLast) + .isInstanceOf(ApplicationErrorException.class); } finally { server.dispose(); diff --git a/rsocket-load-balancer/build.gradle b/rsocket-load-balancer/build.gradle index 29003feaf..5ab0d5422 100644 --- a/rsocket-load-balancer/build.gradle +++ b/rsocket-load-balancer/build.gradle @@ -33,10 +33,8 @@ dependencies { testImplementation 'org.assertj:assertj-core' testImplementation 'io.projectreactor:reactor-test' - // TODO: Remove after JUnit5 migration - testCompileOnly 'junit:junit' testImplementation 'org.hamcrest:hamcrest-library' - testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' testRuntimeOnly 'ch.qos.logback:logback-classic' } 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 0589cc346..52bf89558 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 @@ -16,6 +16,9 @@ package io.rsocket.client; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.client.filter.RSocketSupplier; @@ -24,9 +27,9 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Function; -import org.junit.Assert; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import org.mockito.Mockito; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -34,7 +37,8 @@ public class LoadBalancedRSocketMonoTest { - @Test(timeout = 10_000L) + @Test + @Timeout(10_000L) public void testNeverSelectFailingFactories() throws InterruptedException { TestingRSocket socket = new TestingRSocket(Function.identity()); RSocketSupplier failing = failingClient(); @@ -44,7 +48,8 @@ public void testNeverSelectFailingFactories() throws InterruptedException { testBalancer(factories); } - @Test(timeout = 10_000L) + @Test + @Timeout(10_000L) public void testNeverSelectFailingSocket() throws InterruptedException { TestingRSocket socket = new TestingRSocket(Function.identity()); TestingRSocket failingSocket = @@ -67,8 +72,9 @@ public double availability() { testBalancer(clients); } - @Test(timeout = 10_000L) - @Ignore + @Test + @Timeout(10_000L) + @Disabled public void testRefreshesSocketsOnSelectBeforeReturningFailedAfterNewFactoriesDelivered() { TestingRSocket socket = new TestingRSocket(Function.identity()); @@ -87,12 +93,12 @@ public void testRefreshesSocketsOnSelectBeforeReturningFailedAfterNewFactoriesDe LoadBalancedRSocketMono balancer = LoadBalancedRSocketMono.create(factories); - Assert.assertEquals(0.0, balancer.availability(), 0); + assertThat(balancer.availability()).isZero(); laterSupplier.complete(succeedingFactory(socket)); balancer.rSocketMono.block(); - Assert.assertEquals(1.0, balancer.availability(), 0); + assertThat(balancer.availability()).isEqualTo(1.0); } private void testBalancer(List factories) throws InterruptedException { @@ -128,7 +134,7 @@ private static RSocketSupplier failingClient() { Mockito.when(mock.get()) .thenAnswer( a -> { - Assert.fail(); + fail(); return null; }); diff --git a/rsocket-load-balancer/src/test/java/io/rsocket/client/RSocketSupplierTest.java b/rsocket-load-balancer/src/test/java/io/rsocket/client/RSocketSupplierTest.java index 887132f99..9e1982465 100644 --- a/rsocket-load-balancer/src/test/java/io/rsocket/client/RSocketSupplierTest.java +++ b/rsocket-load-balancer/src/test/java/io/rsocket/client/RSocketSupplierTest.java @@ -16,9 +16,8 @@ package io.rsocket.client; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @@ -31,7 +30,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; @@ -44,7 +43,7 @@ public class RSocketSupplierTest { public void testError() throws InterruptedException { testRSocket( (latch, socket) -> { - assertEquals(1.0, socket.availability(), 0.0); + assertThat(socket.availability()).isEqualTo(1.0); Publisher payloadPublisher = socket.requestResponse(EmptyPayload.INSTANCE); Subscriber subscriber = TestSubscriber.create(); @@ -64,7 +63,7 @@ public void testError() throws InterruptedException { payloadPublisher.subscribe(subscriber); verify(subscriber).onError(any(RuntimeException.class)); double bad = socket.availability(); - assertTrue(good > bad); + assertThat(good > bad).isTrue(); latch.countDown(); }); } @@ -73,7 +72,7 @@ public void testError() throws InterruptedException { public void testWidowReset() throws InterruptedException { testRSocket( (latch, socket) -> { - assertEquals(1.0, socket.availability(), 0.0); + assertThat(socket.availability()).isEqualTo(1.0); Publisher payloadPublisher = socket.requestResponse(EmptyPayload.INSTANCE); Subscriber subscriber = TestSubscriber.create(); @@ -87,7 +86,7 @@ public void testWidowReset() throws InterruptedException { verify(subscriber).onError(any(RuntimeException.class)); double bad = socket.availability(); - assertTrue(good > bad); + assertThat(good > bad).isTrue(); try { Thread.sleep(200); @@ -96,7 +95,7 @@ public void testWidowReset() throws InterruptedException { } double reset = socket.availability(); - assertTrue(reset > bad); + assertThat(reset > bad).isTrue(); latch.countDown(); }); } diff --git a/rsocket-load-balancer/src/test/java/io/rsocket/client/TimeoutClientTest.java b/rsocket-load-balancer/src/test/java/io/rsocket/client/TimeoutClientTest.java index 9a5ac644b..e6c8aa313 100644 --- a/rsocket-load-balancer/src/test/java/io/rsocket/client/TimeoutClientTest.java +++ b/rsocket-load-balancer/src/test/java/io/rsocket/client/TimeoutClientTest.java @@ -24,7 +24,7 @@ import io.rsocket.util.EmptyPayload; import java.time.Duration; import org.hamcrest.MatcherAssert; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; diff --git a/rsocket-load-balancer/src/test/java/io/rsocket/stat/MedianTest.java b/rsocket-load-balancer/src/test/java/io/rsocket/stat/MedianTest.java index 0aab4afd7..b214a725e 100644 --- a/rsocket-load-balancer/src/test/java/io/rsocket/stat/MedianTest.java +++ b/rsocket-load-balancer/src/test/java/io/rsocket/stat/MedianTest.java @@ -18,8 +18,8 @@ import java.util.Arrays; import java.util.Random; -import org.junit.Assert; -import org.junit.Test; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; public class MedianTest { private double errorSum = 0; @@ -59,7 +59,8 @@ private void testMedian(Random rng) { maxError = Math.max(maxError, error); minError = Math.min(minError, error); - Assert.assertTrue( - "p50=" + estimation + ", real=" + expected + ", error=" + error, error < 0.02); + Assertions.assertThat(error < 0.02) + .describedAs("p50=" + estimation + ", real=" + expected + ", error=" + error) + .isTrue(); } } diff --git a/rsocket-test/build.gradle b/rsocket-test/build.gradle index bdbecda41..f8e11d56c 100644 --- a/rsocket-test/build.gradle +++ b/rsocket-test/build.gradle @@ -29,9 +29,6 @@ dependencies { implementation 'io.projectreactor:reactor-test' implementation 'org.assertj:assertj-core' implementation 'org.mockito:mockito-core' - - // TODO: Remove after JUnit5 migration - implementation 'junit:junit' } jar { diff --git a/rsocket-test/src/main/java/io/rsocket/test/BaseClientServerTest.java b/rsocket-test/src/main/java/io/rsocket/test/BaseClientServerTest.java index d74f59fd8..e773b4a0d 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/BaseClientServerTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/BaseClientServerTest.java @@ -16,22 +16,35 @@ package io.rsocket.test; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; import io.rsocket.Payload; import io.rsocket.util.DefaultPayload; import java.util.concurrent.atomic.AtomicInteger; -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 reactor.core.publisher.Flux; public abstract class BaseClientServerTest> { - @Rule public final T setup = createClientServer(); + public final T setup = createClientServer(); protected abstract T createClientServer(); - @Test(timeout = 10000) + @BeforeEach + public void init() { + setup.init(); + } + + @AfterEach + public void teardown() { + setup.tearDown(); + } + + @Test + @Timeout(10000) public void testFireNForget10() { long outputCount = Flux.range(1, 10) @@ -40,10 +53,11 @@ public void testFireNForget10() { .count() .block(); - assertEquals(0, outputCount); + assertThat(outputCount).isZero(); } - @Test(timeout = 10000) + @Test + @Timeout(10000) public void testPushMetadata10() { long outputCount = Flux.range(1, 10) @@ -52,7 +66,7 @@ public void testPushMetadata10() { .count() .block(); - assertEquals(0, outputCount); + assertThat(outputCount).isZero(); } @Test // (timeout = 10000) @@ -65,10 +79,11 @@ public void testRequestResponse1() { .count() .block(); - assertEquals(1, outputCount); + assertThat(outputCount).isZero(); } - @Test(timeout = 10000) + @Test + @Timeout(10000) public void testRequestResponse10() { long outputCount = Flux.range(1, 10) @@ -78,7 +93,7 @@ public void testRequestResponse10() { .count() .block(); - assertEquals(10, outputCount); + assertThat(outputCount).isEqualTo(10); } private Payload testPayload(int metadataPresent) { @@ -97,7 +112,8 @@ private Payload testPayload(int metadataPresent) { return DefaultPayload.create("hello", metadata); } - @Test(timeout = 10000) + @Test + @Timeout(10000) public void testRequestResponse100() { long outputCount = Flux.range(1, 100) @@ -107,10 +123,11 @@ public void testRequestResponse100() { .count() .block(); - assertEquals(100, outputCount); + assertThat(outputCount).isEqualTo(100); } - @Test(timeout = 20000) + @Test + @Timeout(20000) public void testRequestResponse10_000() { long outputCount = Flux.range(1, 10_000) @@ -120,28 +137,31 @@ public void testRequestResponse10_000() { .count() .block(); - assertEquals(10_000, outputCount); + assertThat(outputCount).isEqualTo(10_000); } - @Test(timeout = 10000) + @Test + @Timeout(10000) public void testRequestStream() { Flux publisher = setup.getRSocket().requestStream(testPayload(3)); long count = publisher.take(5).count().block(); - assertEquals(5, count); + assertThat(count).isEqualTo(5); } - @Test(timeout = 10000) + @Test + @Timeout(10000) public void testRequestStreamAll() { Flux publisher = setup.getRSocket().requestStream(testPayload(3)); long count = publisher.count().block(); - assertEquals(10000, count); + assertThat(count).isEqualTo(10000); } - @Test(timeout = 10000) + @Test + @Timeout(10000) public void testRequestStreamWithRequestN() { CountdownBaseSubscriber ts = new CountdownBaseSubscriber(); ts.expect(5); @@ -149,16 +169,17 @@ public void testRequestStreamWithRequestN() { setup.getRSocket().requestStream(testPayload(3)).subscribe(ts); ts.await(); - assertEquals(5, ts.count()); + assertThat(ts.count()).isEqualTo(5); ts.expect(5); ts.await(); ts.cancel(); - assertEquals(10, ts.count()); + assertThat(ts.count()).isEqualTo(10); } - @Test(timeout = 10000) + @Test + @Timeout(10000) public void testRequestStreamWithDelayedRequestN() { CountdownBaseSubscriber ts = new CountdownBaseSubscriber(); @@ -167,34 +188,37 @@ public void testRequestStreamWithDelayedRequestN() { ts.expect(5); ts.await(); - assertEquals(5, ts.count()); + assertThat(ts.count()).isEqualTo(5); ts.expect(5); ts.await(); ts.cancel(); - assertEquals(10, ts.count()); + assertThat(ts.count()).isEqualTo(10); } - @Test(timeout = 10000) + @Test + @Timeout(10000) public void testChannel0() { Flux publisher = setup.getRSocket().requestChannel(Flux.empty()); long count = publisher.count().block(); - assertEquals(0, count); + assertThat(count).isZero(); } - @Test(timeout = 10000) + @Test + @Timeout(10000) public void testChannel1() { Flux publisher = setup.getRSocket().requestChannel(Flux.just(testPayload(0))); long count = publisher.count().block(); - assertEquals(1, count); + assertThat(count).isOne(); } - @Test(timeout = 10000) + @Test + @Timeout(10000) public void testChannel3() { Flux publisher = setup @@ -203,44 +227,48 @@ public void testChannel3() { long count = publisher.count().block(); - assertEquals(3, count); + assertThat(count).isEqualTo(3); } - @Test(timeout = 10000) + @Test + @Timeout(10000) public void testChannel512() { Flux payloads = Flux.range(1, 512).map(i -> DefaultPayload.create("hello " + i)); long count = setup.getRSocket().requestChannel(payloads).count().block(); - assertEquals(512, count); + assertThat(count).isEqualTo(512); } - @Test(timeout = 30000) + @Test + @Timeout(30000) public void testChannel20_000() { Flux payloads = Flux.range(1, 20_000).map(i -> DefaultPayload.create("hello " + i)); long count = setup.getRSocket().requestChannel(payloads).count().block(); - assertEquals(20_000, count); + assertThat(count).isEqualTo(20_000); } - @Test(timeout = 60_000) + @Test + @Timeout(60_000) public void testChannel200_000() { Flux payloads = Flux.range(1, 200_000).map(i -> DefaultPayload.create("hello " + i)); long count = setup.getRSocket().requestChannel(payloads).count().block(); - assertEquals(200_000, count); + assertThat(count).isEqualTo(200_000); } - @Test(timeout = 60_000) - @Ignore + @Test + @Timeout(60_000) + @Disabled public void testChannel2_000_000() { AtomicInteger counter = new AtomicInteger(0); Flux payloads = Flux.range(1, 2_000_000).map(i -> DefaultPayload.create("hello " + i)); long count = setup.getRSocket().requestChannel(payloads).count().block(); - assertEquals(2_000_000, count); + assertThat(count).isEqualTo(2_000_000); } } 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 6f562875f..1d6b7f69e 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/ClientSetupRule.java +++ b/rsocket-test/src/main/java/io/rsocket/test/ClientSetupRule.java @@ -25,12 +25,9 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; -import org.junit.rules.ExternalResource; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; import reactor.core.publisher.Mono; -public class ClientSetupRule extends ExternalResource { +public class ClientSetupRule { private static final String data = "hello world"; private static final String metadata = "metadata"; @@ -39,6 +36,7 @@ public class ClientSetupRule extends ExternalResource { private Function serverInit; private RSocket client; + private S server; public ClientSetupRule( Supplier addressSupplier, @@ -59,18 +57,14 @@ public ClientSetupRule( .block(); } - @Override - public Statement apply(Statement base, Description description) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - T address = addressSupplier.get(); - S server = serverInit.apply(address); - client = clientConnector.apply(address, server); - base.evaluate(); - server.dispose(); - } - }; + public void init() { + T address = addressSupplier.get(); + S server = serverInit.apply(address); + client = clientConnector.apply(address, server); + } + + public void tearDown() { + server.dispose(); } public RSocket getRSocket() { From 02833732d0dbd97dad30b4ff32831b0c9cc3fe8b Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 24 May 2021 23:36:56 +0300 Subject: [PATCH 043/116] reworks and improves Resumability impl this includes: * rework of InMemoryResumableFramesStore and improvement in its tests coverage * improvements in Client/Server resume Session and ensuring that if connection is rejected for any reasons - it is fully closed on both outbound and inbound ends (This fix is needed for LocalDuplexConnection scenario which may be in unterminated state if it will not be subscribed on the inbound) * enabling resumability tests for LocalTransport * improvements in logging * general cleanups and polishing Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .../io/rsocket/core/RSocketConnector.java | 1 + .../src/main/java/io/rsocket/core/Resume.java | 2 +- .../java/io/rsocket/core/ServerSetup.java | 3 +- .../rsocket/resume/ClientRSocketSession.java | 73 +- .../resume/InMemoryResumableFramesStore.java | 824 ++++++++++++++---- .../resume/ResumableDuplexConnection.java | 64 +- .../rsocket/resume/ServerRSocketSession.java | 55 +- .../resume/InMemoryResumeStoreTest.java | 492 ++++++++++- .../rsocket/resume/ResumeIntegrationTest.java | 6 +- .../java/io/rsocket/test/TransportTest.java | 20 +- .../local/LocalResumableTransportTest.java | 2 - ...sumableWithFragmentationTransportTest.java | 42 + ...sumableWithFragmentationTransportTest.java | 55 ++ .../WebsocketResumableTransportTest.java | 58 ++ ...sumableWithFragmentationTransportTest.java | 58 ++ 15 files changed, 1500 insertions(+), 255 deletions(-) create mode 100644 rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableWithFragmentationTransportTest.java create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableWithFragmentationTransportTest.java create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableTransportTest.java create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableWithFragmentationTransportTest.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 fe91cdb6f..edd13b48c 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -612,6 +612,7 @@ public Mono connect(Supplier transportSupplier) { final ResumableDuplexConnection resumableDuplexConnection = new ResumableDuplexConnection( CLIENT_TAG, + resumeToken, clientServerConnection, resumableFramesStore); final ResumableClientSetup resumableClientSetup = 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 48133af98..fa0eedbfa 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/Resume.java +++ b/rsocket-core/src/main/java/io/rsocket/core/Resume.java @@ -160,7 +160,7 @@ boolean isCleanupStoreOnKeepAlive() { Function getStoreFactory(String tag) { return storeFactory != null ? storeFactory - : token -> new InMemoryResumableFramesStore(tag, 100_000); + : token -> new InMemoryResumableFramesStore(tag, token, 100_000); } Duration 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 318c54816..0b23bcde5 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java @@ -109,7 +109,8 @@ public Mono acceptRSocketSetup( final ResumableFramesStore resumableFramesStore = resumeStoreFactory.apply(resumeToken); final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection("server", duplexConnection, resumableFramesStore); + new ResumableDuplexConnection( + "server", resumeToken, duplexConnection, resumableFramesStore); final ServerRSocketSession serverRSocketSession = new ServerRSocketSession( resumeToken, 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 9fd95ad17..c58cc4954 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -18,6 +18,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.CharsetUtil; import io.rsocket.DuplexConnection; import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.exceptions.Exceptions; @@ -54,6 +55,7 @@ public class ClientRSocketSession final Retry retry; final boolean cleanupStoreOnKeepAlive; final ByteBuf resumeToken; + final String session; volatile Subscription s; static final AtomicReferenceFieldUpdater S = @@ -71,20 +73,30 @@ public ClientRSocketSession( Retry retry, boolean cleanupStoreOnKeepAlive) { this.resumeToken = resumeToken; + this.session = resumeToken.toString(CharsetUtil.UTF_8); this.connectionFactory = connectionFactory.flatMap( dc -> { + final long impliedPosition = resumableFramesStore.frameImpliedPosition(); + final long position = resumableFramesStore.framePosition(); dc.sendFrame( 0, ResumeFrameCodec.encode( dc.alloc(), resumeToken.retain(), // server uses this to release its cache - resumableFramesStore.frameImpliedPosition(), // observed on the client side + impliedPosition, // observed on the client side // server uses this to check whether there is no mismatch - resumableFramesStore.framePosition() // sent from the client sent + position // sent from the client sent )); - logger.debug("Resume Frame has been sent"); + + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. ResumeFrame[impliedPosition[{}], position[{}]] has been sent.", + session, + impliedPosition, + position); + } return connectionTransformer.apply(dc); }); @@ -105,7 +117,12 @@ void reconnect(int index) { if (this.s == Operators.cancelledSubscription() && S.compareAndSet(this, Operators.cancelledSubscription(), null)) { keepAliveSupport.stop(); - logger.debug("Connection[" + index + "] is lost. Reconnecting to resume..."); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. Connection[{}] is lost. Reconnecting to resume...", + session, + index); + } connectionFactory.retryWhen(retry).timeout(resumeSessionDuration).subscribe(this); } } @@ -155,21 +172,30 @@ public void onNext(Tuple2 tuple2) { DuplexConnection nextDuplexConnection = tuple2.getT2(); if (!Operators.terminate(S, this)) { - logger.debug("Session has already been expired. Terminating received connection"); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. Session has already been expired. Terminating received connection", + session); + } final ConnectionErrorException connectionErrorException = new ConnectionErrorException("resumption_server=[Session Expired]"); nextDuplexConnection.sendErrorAndClose(connectionErrorException); + nextDuplexConnection.receive().subscribe().dispose(); return; } final int streamId = FrameHeaderCodec.streamId(shouldBeResumeOKFrame); if (streamId != 0) { - logger.debug( - "Illegal first frame received. RESUME_OK frame must be received before any others. Terminating received connection"); - resumableConnection.dispose(); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. Illegal first frame received. RESUME_OK frame must be received before any others. Terminating received connection", + session); + } final ConnectionErrorException connectionErrorException = new ConnectionErrorException("RESUME_OK frame must be received before any others"); + resumableConnection.dispose(connectionErrorException); nextDuplexConnection.sendErrorAndClose(connectionErrorException); + nextDuplexConnection.receive().subscribe().dispose(); return; } @@ -183,7 +209,8 @@ public void onNext(Tuple2 tuple2) { final long position = resumableFramesStore.framePosition(); final long impliedPosition = resumableFramesStore.frameImpliedPosition(); logger.debug( - "ResumeOK FRAME received. ServerResumeState{observedFramesPosition[{}]}. ClientResumeState{observedFramesPosition[{}], sentFramesPosition[{}]}", + "Side[client]|Session[{}]. ResumeOK FRAME received. ServerResumeState[remoteImpliedPosition[{}]]. ClientResumeState[impliedPosition[{}], position[{}]]", + session, remoteImpliedPos, impliedPosition, position); @@ -194,42 +221,54 @@ public void onNext(Tuple2 tuple2) { } } catch (IllegalStateException e) { logger.debug("Exception occurred while releasing frames in the frameStore", e); - resumableConnection.dispose(); + resumableConnection.dispose(e); final ConnectionErrorException t = new ConnectionErrorException(e.getMessage(), e); nextDuplexConnection.sendErrorAndClose(t); + nextDuplexConnection.receive().subscribe().dispose(); return; } if (resumableConnection.connect(nextDuplexConnection)) { keepAliveSupport.start(); - logger.debug("Session has been resumed successfully"); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. Session has been resumed successfully", session); + } } else { - logger.debug("Session has already been expired. Terminating received connection"); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. Session has already been expired. Terminating received connection", + session); + } final ConnectionErrorException connectionErrorException = new ConnectionErrorException("resumption_server_pos=[Session Expired]"); nextDuplexConnection.sendErrorAndClose(connectionErrorException); + nextDuplexConnection.receive().subscribe().dispose(); } } else { logger.debug( - "Mismatching remote and local state. Expected RemoteImpliedPosition[{}] to be greater or equal to the LocalPosition[{}]. Terminating received connection", + "Side[client]|Session[{}]. Mismatching remote and local state. Expected RemoteImpliedPosition[{}] to be greater or equal to the LocalPosition[{}]. Terminating received connection", + session, remoteImpliedPos, position); - resumableConnection.dispose(); final ConnectionErrorException connectionErrorException = new ConnectionErrorException("resumption_server_pos=[" + remoteImpliedPos + "]"); + resumableConnection.dispose(connectionErrorException); nextDuplexConnection.sendErrorAndClose(connectionErrorException); + nextDuplexConnection.receive().subscribe().dispose(); } } else if (frameType == FrameType.ERROR) { final RuntimeException exception = Exceptions.from(0, shouldBeResumeOKFrame); logger.debug("Received error frame. Terminating received connection", exception); - resumableConnection.dispose(); + resumableConnection.dispose(exception); } else { logger.debug( "Illegal first frame received. RESUME_OK frame must be received before any others. Terminating received connection"); - resumableConnection.dispose(); final ConnectionErrorException connectionErrorException = new ConnectionErrorException("RESUME_OK frame must be received before any others"); + resumableConnection.dispose(connectionErrorException); nextDuplexConnection.sendErrorAndClose(connectionErrorException); + nextDuplexConnection.receive().subscribe().dispose(); } } @@ -239,7 +278,7 @@ public void onError(Throwable t) { Operators.onErrorDropped(t, currentContext()); } - resumableConnection.dispose(); + resumableConnection.dispose(t); } @Override 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 03516af92..87d82048d 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java @@ -19,118 +19,286 @@ import static io.rsocket.resume.ResumableDuplexConnection.isResumableFrame; import io.netty.buffer.ByteBuf; -import java.util.ArrayList; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import io.netty.util.CharsetUtil; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; import reactor.core.publisher.Sinks; +import reactor.util.annotation.Nullable; /** * writes - n (where n is frequent, primary operation) reads - m (where m == KeepAliveFrequency) * skip - k -> 0 (where k is the rare operation which happens after disconnection */ public class InMemoryResumableFramesStore extends Flux - implements CoreSubscriber, ResumableFramesStore, Subscription { + implements ResumableFramesStore, Subscription { + private FramesSubscriber framesSubscriber; private static final Logger logger = LoggerFactory.getLogger(InMemoryResumableFramesStore.class); final Sinks.Empty disposed = Sinks.empty(); - final ArrayList cachedFrames; - final String tag; + final Queue cachedFrames; + final String side; + final String session; final int cacheLimit; volatile long impliedPosition; static final AtomicLongFieldUpdater IMPLIED_POSITION = AtomicLongFieldUpdater.newUpdater(InMemoryResumableFramesStore.class, "impliedPosition"); - volatile long position; - static final AtomicLongFieldUpdater POSITION = - AtomicLongFieldUpdater.newUpdater(InMemoryResumableFramesStore.class, "position"); + volatile long firstAvailableFramePosition; + static final AtomicLongFieldUpdater FIRST_AVAILABLE_FRAME_POSITION = + AtomicLongFieldUpdater.newUpdater( + InMemoryResumableFramesStore.class, "firstAvailableFramePosition"); - volatile int cacheSize; - static final AtomicIntegerFieldUpdater CACHE_SIZE = - AtomicIntegerFieldUpdater.newUpdater(InMemoryResumableFramesStore.class, "cacheSize"); + long remoteImpliedPosition; - CoreSubscriber saveFramesSubscriber; + int cacheSize; + + Throwable terminal; CoreSubscriber actual; + CoreSubscriber pendingActual; + + volatile long state; + static final AtomicLongFieldUpdater STATE = + AtomicLongFieldUpdater.newUpdater(InMemoryResumableFramesStore.class, "state"); /** - * Indicates whether there is an active connection or not. - * - *
      - *
    • 0 - no active connection - *
    • 1 - active connection - *
    • 2 - disposed - *
    - * - *
    -   * 0 <-----> 1
    -   * |         |
    -   * +--> 2 <--+
    -   * 
    + * Flag which indicates that {@link InMemoryResumableFramesStore} is finalized and all related + * stores are cleaned */ - volatile int state; - - static final AtomicIntegerFieldUpdater STATE = - AtomicIntegerFieldUpdater.newUpdater(InMemoryResumableFramesStore.class, "state"); + static final long FINALIZED_FLAG = + 0b1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + /** + * Flag which indicates that {@link InMemoryResumableFramesStore} is terminated via the {@link + * InMemoryResumableFramesStore#dispose()} method + */ + static final long DISPOSED_FLAG = + 0b0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + /** + * Flag which indicates that {@link InMemoryResumableFramesStore} is terminated via the {@link + * FramesSubscriber#onComplete()} or {@link FramesSubscriber#onError(Throwable)} ()} methods + */ + static final long TERMINATED_FLAG = + 0b0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + /** Flag which indicates that {@link InMemoryResumableFramesStore} has active frames consumer */ + static final long CONNECTED_FLAG = + 0b0001_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + /** + * Flag which indicates that {@link InMemoryResumableFramesStore} has no active frames consumer + * but there is a one pending + */ + static final long PENDING_CONNECTION_FLAG = + 0b0000_1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + /** + * Flag which indicates that there are some received implied position changes from the remote + * party + */ + static final long REMOTE_IMPLIED_POSITION_CHANGED_FLAG = + 0b0000_0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + /** + * Flag which indicates that there are some frames stored in the {@link + * io.rsocket.internal.UnboundedProcessor} which has to be cached and sent to the remote party + */ + static final long HAS_FRAME_FLAG = + 0b0000_0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + /** + * Flag which indicates that {@link InMemoryResumableFramesStore#drain(long)} has an actor which + * is currently progressing on the work. This flag should work as a guard to enter|exist into|from + * the {@link InMemoryResumableFramesStore#drain(long)} method. + */ + static final long MAX_WORK_IN_PROGRESS = + 0b0000_0000_0000_0000_0000_0000_0000_0000_1111_1111_1111_1111_1111_1111_1111_1111L; - public InMemoryResumableFramesStore(String tag, int cacheSizeBytes) { - this.tag = tag; + public InMemoryResumableFramesStore(String side, ByteBuf session, int cacheSizeBytes) { + this.side = side; + this.session = session.toString(CharsetUtil.UTF_8); this.cacheLimit = cacheSizeBytes; - this.cachedFrames = new ArrayList<>(); + this.cachedFrames = new ArrayDeque<>(); } public Mono saveFrames(Flux frames) { return frames .transform( - Operators.lift( - (__, actual) -> { - this.saveFramesSubscriber = actual; - return this; - })) + Operators.lift( + (__, actual) -> this.framesSubscriber = new FramesSubscriber(actual, this))) .then(); } @Override public void releaseFrames(long remoteImpliedPos) { - long pos = position; - logger.debug( - "{} Removing frames for local: {}, remote implied: {}", tag, pos, remoteImpliedPos); - long toRemoveBytes = Math.max(0, remoteImpliedPos - pos); - int removedBytes = 0; - final ArrayList frames = cachedFrames; - synchronized (this) { - while (toRemoveBytes > removedBytes && frames.size() > 0) { - ByteBuf cachedFrame = frames.remove(0); - int frameSize = cachedFrame.readableBytes(); - cachedFrame.release(); - removedBytes += frameSize; + long lastReceivedRemoteImpliedPosition = this.remoteImpliedPosition; + if (lastReceivedRemoteImpliedPosition > remoteImpliedPos) { + throw new IllegalStateException( + "Given Remote Implied Position is behind the last received Remote Implied Position"); + } + + this.remoteImpliedPosition = remoteImpliedPos; + + final long previousState = markRemoteImpliedPositionChanged(this); + if (isFinalized(previousState) || isWorkInProgress(previousState)) { + return; + } + + drain((previousState + 1) | REMOTE_IMPLIED_POSITION_CHANGED_FLAG); + } + + void drain(long expectedState) { + final Fuseable.QueueSubscription qs = this.framesSubscriber.qs; + final Queue cachedFrames = this.cachedFrames; + + for (; ; ) { + if (hasRemoteImpliedPositionChanged(expectedState)) { + expectedState = handlePendingRemoteImpliedPositionChanges(expectedState, cachedFrames); + } + + if (hasPendingConnection(expectedState)) { + expectedState = handlePendingConnection(expectedState, cachedFrames); + } + + if (isConnected(expectedState)) { + if (isTerminated(expectedState)) { + handleTerminal(this.terminal); + } else if (isDisposed()) { + handleTerminal(new CancellationException("Disposed")); + } else if (hasFrames(expectedState)) { + handlePendingFrames(qs); + } + } + + if (isDisposed(expectedState) || isTerminated(expectedState)) { + clearAndFinalize(this); + return; + } + + expectedState = markWorkDone(this, expectedState); + if (isFinalized(expectedState)) { + return; + } + + if (!isWorkInProgress(expectedState)) { + return; } } + } - if (toRemoveBytes > removedBytes) { - throw new IllegalStateException( - String.format( - "Local and remote state disagreement: " - + "need to remove additional %d bytes, but cache is empty", - toRemoveBytes)); - } else if (toRemoveBytes < removedBytes) { - throw new IllegalStateException( - "Local and remote state disagreement: local and remote frame sizes are not equal"); - } else { - POSITION.addAndGet(this, removedBytes); - if (cacheLimit != Integer.MAX_VALUE) { - CACHE_SIZE.addAndGet(this, -removedBytes); - logger.debug("{} Removed frames. Current cache size: {}", tag, cacheSize); + long handlePendingRemoteImpliedPositionChanges(long expectedState, Queue cachedFrames) { + final long remoteImpliedPosition = this.remoteImpliedPosition; + final long firstAvailableFramePosition = this.firstAvailableFramePosition; + final long toDropFromCache = Math.max(0, remoteImpliedPosition - firstAvailableFramePosition); + + if (toDropFromCache > 0) { + final int droppedFromCache = dropFramesFromCache(toDropFromCache, cachedFrames); + + if (toDropFromCache > droppedFromCache) { + this.terminal = + new IllegalStateException( + String.format( + "Local and remote state disagreement: " + + "need to remove additional %d bytes, but cache is empty", + toDropFromCache)); + expectedState = markTerminated(this) | TERMINATED_FLAG; + } + + if (toDropFromCache < droppedFromCache) { + this.terminal = + new IllegalStateException( + "Local and remote state disagreement: local and remote frame sizes are not equal"); + expectedState = markTerminated(this) | TERMINATED_FLAG; + } + + FIRST_AVAILABLE_FRAME_POSITION.lazySet(this, firstAvailableFramePosition + droppedFromCache); + if (this.cacheLimit != Integer.MAX_VALUE) { + this.cacheSize -= droppedFromCache; + + if (logger.isDebugEnabled()) { + logger.debug( + "Side[{}]|Session[{}]. Removed frames from cache to position[{}]. CacheSize[{}]", + this.side, + this.session, + this.remoteImpliedPosition, + this.cacheSize); + } } } + + return expectedState; + } + + void handlePendingFrames(Fuseable.QueueSubscription qs) { + for (; ; ) { + final ByteBuf frame = qs.poll(); + final boolean empty = frame == null; + + if (empty) { + break; + } + + handleFrame(frame); + + if (!isConnected(this.state)) { + break; + } + } + } + + long handlePendingConnection(long expectedState, Queue cachedFrames) { + CoreSubscriber lastActual = null; + for (; ; ) { + final CoreSubscriber nextActual = this.pendingActual; + + if (nextActual != lastActual) { + for (final ByteBuf frame : cachedFrames) { + nextActual.onNext(frame.retainedSlice()); + } + } + + expectedState = markConnected(this, expectedState); + if (isConnected(expectedState)) { + if (logger.isDebugEnabled()) { + logger.debug( + "Side[{}]|Session[{}]. Connected at Position[{}] and ImpliedPosition[{}]", + side, + session, + firstAvailableFramePosition, + impliedPosition); + } + + this.actual = nextActual; + break; + } + + if (!hasPendingConnection(expectedState)) { + break; + } + + lastActual = nextActual; + } + return expectedState; + } + + static int dropFramesFromCache(long toRemoveBytes, Queue cache) { + int removedBytes = 0; + while (toRemoveBytes > removedBytes && cache.size() > 0) { + final ByteBuf cachedFrame = cache.poll(); + final int frameSize = cachedFrame.readableBytes(); + + cachedFrame.release(); + + removedBytes += frameSize; + } + + return removedBytes; } @Override @@ -140,12 +308,12 @@ public Flux resumeStream() { @Override public long framePosition() { - return position; + return this.firstAvailableFramePosition; } @Override public long frameImpliedPosition() { - return impliedPosition & Long.MAX_VALUE; + return this.impliedPosition & Long.MAX_VALUE; } @Override @@ -169,7 +337,8 @@ void pauseImplied() { final long impliedPosition = this.impliedPosition; if (IMPLIED_POSITION.compareAndSet(this, impliedPosition, impliedPosition | Long.MIN_VALUE)) { - logger.debug("Tag {}. Paused at position[{}]", tag, impliedPosition); + logger.debug( + "Side[{}]|Session[{}]. Paused at position[{}]", side, session, impliedPosition); return; } } @@ -181,7 +350,11 @@ void resumeImplied() { final long restoredImpliedPosition = impliedPosition & Long.MAX_VALUE; if (IMPLIED_POSITION.compareAndSet(this, impliedPosition, restoredImpliedPosition)) { - logger.debug("Tag {}. Resumed at position[{}]", tag, restoredImpliedPosition); + logger.debug( + "Side[{}]|Session[{}]. Resumed at position[{}]", + side, + session, + restoredImpliedPosition); return; } } @@ -194,102 +367,94 @@ public Mono onClose() { @Override public void dispose() { - if (STATE.getAndSet(this, 2) != 2) { - cacheSize = 0; - synchronized (this) { - logger.debug("Tag {}.Disposing InMemoryFrameStore", tag); - for (ByteBuf frame : cachedFrames) { - if (frame != null) { - frame.release(); - } - } - cachedFrames.clear(); - } - disposed.tryEmitEmpty(); + final long previousState = markDisposed(this); + if (isFinalized(previousState) + || isDisposed(previousState) + || isWorkInProgress(previousState)) { + return; + } + + drain(previousState | DISPOSED_FLAG); + } + + void clearCache() { + final Queue frames = this.cachedFrames; + this.cacheSize = 0; + + ByteBuf frame; + while ((frame = frames.poll()) != null) { + frame.release(); } } @Override public boolean isDisposed() { - return state == 2; + return isDisposed(this.state); } - @Override - public void onSubscribe(Subscription s) { - saveFramesSubscriber.onSubscribe(Operators.emptySubscription()); - s.request(Long.MAX_VALUE); + void handleFrame(ByteBuf frame) { + final boolean isResumable = isResumableFrame(frame); + if (isResumable) { + handleResumableFrame(frame); + return; + } + + handleConnectionFrame(frame); } - @Override - public void onError(Throwable t) { - saveFramesSubscriber.onError(t); + void handleTerminal(@Nullable Throwable t) { + if (t != null) { + this.actual.onError(t); + } else { + this.actual.onComplete(); + } } - @Override - public void onComplete() { - saveFramesSubscriber.onComplete(); + void handleConnectionFrame(ByteBuf frame) { + this.actual.onNext(frame); } - @Override - public void onNext(ByteBuf frame) { - final int state; - final boolean isResumable = isResumableFrame(frame); - boolean canBeStore = isResumable; - if (isResumable) { - final ArrayList frames = cachedFrames; - final int incomingFrameSize = frame.readableBytes(); - final int cacheLimit = this.cacheLimit; + void handleResumableFrame(ByteBuf frame) { + final Queue frames = this.cachedFrames; + final int incomingFrameSize = frame.readableBytes(); + final int cacheLimit = this.cacheLimit; - if (cacheLimit != Integer.MAX_VALUE) { - long availableSize = cacheLimit - cacheSize; - if (availableSize < incomingFrameSize) { - int removedBytes = 0; - synchronized (this) { - while (availableSize < incomingFrameSize) { - if (frames.size() == 0) { - break; - } - ByteBuf cachedFrame; - cachedFrame = frames.remove(0); - final int frameSize = cachedFrame.readableBytes(); - availableSize += frameSize; - removedBytes += frameSize; - cachedFrame.release(); - } - } - CACHE_SIZE.addAndGet(this, -removedBytes); - - canBeStore = availableSize >= incomingFrameSize; - POSITION.addAndGet(this, removedBytes + (canBeStore ? 0 : incomingFrameSize)); + final boolean canBeStore; + int cacheSize = this.cacheSize; + if (cacheLimit != Integer.MAX_VALUE) { + final long availableSize = cacheLimit - cacheSize; + + if (availableSize < incomingFrameSize) { + final long firstAvailableFramePosition = this.firstAvailableFramePosition; + final long toRemoveBytes = incomingFrameSize - availableSize; + final int removedBytes = dropFramesFromCache(toRemoveBytes, frames); + + cacheSize = cacheSize - removedBytes; + canBeStore = removedBytes >= toRemoveBytes; + + if (canBeStore) { + FIRST_AVAILABLE_FRAME_POSITION.lazySet(this, firstAvailableFramePosition + removedBytes); } else { - canBeStore = true; + this.cacheSize = cacheSize; + FIRST_AVAILABLE_FRAME_POSITION.lazySet( + this, firstAvailableFramePosition + removedBytes + incomingFrameSize); } } else { canBeStore = true; } + } else { + canBeStore = true; + } - state = this.state; - if (canBeStore) { - synchronized (this) { - if (state != 2) { - frames.add(frame); - } - } + if (canBeStore) { + frames.offer(frame); - if (cacheLimit != Integer.MAX_VALUE) { - CACHE_SIZE.addAndGet(this, incomingFrameSize); - } + if (cacheLimit != Integer.MAX_VALUE) { + this.cacheSize = cacheSize + incomingFrameSize; } - } else { - state = this.state; } - final CoreSubscriber actual = this.actual; - if (state == 1) { - actual.onNext(isResumable && canBeStore ? frame.retainedSlice() : frame); - } else if (!isResumable || !canBeStore || state == 2) { - frame.release(); - } + this.actual.onNext(canBeStore ? frame.retainedSlice() : frame); } @Override @@ -298,30 +463,377 @@ public void request(long n) {} @Override public void cancel() { pauseImplied(); - state = 0; + markDisconnected(this); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[{}]|Session[{}]. Disconnected at Position[{}] and ImpliedPosition[{}]", + side, + session, + firstAvailableFramePosition, + frameImpliedPosition()); + } } @Override public void subscribe(CoreSubscriber actual) { - final int state = this.state; - if (state != 2) { - resumeImplied(); - logger.debug( - "Tag: {}. Subscribed at Position[{}] and ImpliedPosition[{}]", - tag, - position, - impliedPosition); - actual.onSubscribe(this); - synchronized (this) { - for (final ByteBuf frame : cachedFrames) { - actual.onNext(frame.retainedSlice()); + resumeImplied(); + actual.onSubscribe(this); + this.pendingActual = actual; + + final long previousState = markPendingConnection(this); + if (isDisposed(previousState)) { + actual.onError(new CancellationException("Disposed")); + return; + } + + if (isTerminated(previousState)) { + actual.onError(new CancellationException("Disposed")); + return; + } + + if (isWorkInProgress(previousState)) { + return; + } + + drain((previousState + 1) | PENDING_CONNECTION_FLAG); + } + + static class FramesSubscriber + implements CoreSubscriber, Fuseable.QueueSubscription { + + final CoreSubscriber actual; + final InMemoryResumableFramesStore parent; + + Fuseable.QueueSubscription qs; + + boolean done; + + FramesSubscriber(CoreSubscriber actual, InMemoryResumableFramesStore parent) { + this.actual = actual; + this.parent = parent; + } + + @Override + @SuppressWarnings("unchecked") + public void onSubscribe(Subscription s) { + if (Operators.validate(this.qs, s)) { + final Fuseable.QueueSubscription qs = (Fuseable.QueueSubscription) s; + this.qs = qs; + + final int m = qs.requestFusion(Fuseable.ANY); + + if (m != Fuseable.ASYNC) { + s.cancel(); + this.actual.onSubscribe(this); + this.actual.onError(new IllegalStateException("Source has to be ASYNC fuseable")); + return; } + + this.actual.onSubscribe(this); } + } - this.actual = actual; - STATE.compareAndSet(this, 0, 1); - } else { - Operators.complete(actual); + @Override + public void onNext(ByteBuf byteBuf) { + final InMemoryResumableFramesStore parent = this.parent; + long previousState = InMemoryResumableFramesStore.markFrameAdded(parent); + + if (isFinalized(previousState)) { + this.qs.clear(); + return; + } + + if (isWorkInProgress(previousState) + || (!isConnected(previousState) && !hasPendingConnection(previousState))) { + return; + } + + parent.drain(previousState + 1); } + + @Override + public void onError(Throwable t) { + if (this.done) { + Operators.onErrorDropped(t, this.actual.currentContext()); + return; + } + + final InMemoryResumableFramesStore parent = this.parent; + + parent.terminal = t; + this.done = true; + + final long previousState = InMemoryResumableFramesStore.markTerminated(parent); + if (isFinalized(previousState)) { + Operators.onErrorDropped(t, this.actual.currentContext()); + return; + } + + if (isWorkInProgress(previousState)) { + return; + } + + parent.drain(previousState | TERMINATED_FLAG); + } + + @Override + public void onComplete() { + if (this.done) { + return; + } + + final InMemoryResumableFramesStore parent = this.parent; + + this.done = true; + + final long previousState = InMemoryResumableFramesStore.markTerminated(parent); + if (isFinalized(previousState)) { + return; + } + + if (isWorkInProgress(previousState)) { + return; + } + + parent.drain(previousState | TERMINATED_FLAG); + } + + @Override + public void cancel() { + if (this.done) { + return; + } + + this.done = true; + + final long previousState = InMemoryResumableFramesStore.markTerminated(parent); + if (isFinalized(previousState)) { + return; + } + + if (isWorkInProgress(previousState)) { + return; + } + + parent.drain(previousState | TERMINATED_FLAG); + } + + @Override + public void request(long n) {} + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public Void poll() { + return null; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public void clear() {} + } + + static long markFrameAdded(InMemoryResumableFramesStore store) { + for (; ; ) { + final long state = store.state; + + if (isFinalized(state)) { + return state; + } + + long nextState = state; + if (isConnected(state) || hasPendingConnection(state) || isWorkInProgress(state)) { + nextState = + (state & MAX_WORK_IN_PROGRESS) == MAX_WORK_IN_PROGRESS ? nextState : nextState + 1; + } + + if (STATE.compareAndSet(store, state, nextState | HAS_FRAME_FLAG)) { + return state; + } + } + } + + static long markPendingConnection(InMemoryResumableFramesStore store) { + for (; ; ) { + final long state = store.state; + + if (isFinalized(state) || isDisposed(state) || isTerminated(state)) { + return state; + } + + if (isConnected(state)) { + return state; + } + + final long nextState = + (state & MAX_WORK_IN_PROGRESS) == MAX_WORK_IN_PROGRESS ? state : state + 1; + if (STATE.compareAndSet(store, state, nextState | PENDING_CONNECTION_FLAG)) { + return state; + } + } + } + + static long markRemoteImpliedPositionChanged(InMemoryResumableFramesStore store) { + for (; ; ) { + final long state = store.state; + + if (isFinalized(state)) { + return state; + } + + final long nextState = + (state & MAX_WORK_IN_PROGRESS) == MAX_WORK_IN_PROGRESS ? state : (state + 1); + if (STATE.compareAndSet(store, state, nextState | REMOTE_IMPLIED_POSITION_CHANGED_FLAG)) { + return state; + } + } + } + + static long markDisconnected(InMemoryResumableFramesStore store) { + for (; ; ) { + final long state = store.state; + + if (isFinalized(state)) { + return state; + } + + if (STATE.compareAndSet(store, state, state & ~CONNECTED_FLAG & ~PENDING_CONNECTION_FLAG)) { + return state; + } + } + } + + static long markWorkDone(InMemoryResumableFramesStore store, long expectedState) { + for (; ; ) { + final long state = store.state; + + if (expectedState != state) { + return state; + } + + if (isFinalized(state)) { + return state; + } + + final long nextState = state & ~MAX_WORK_IN_PROGRESS & ~REMOTE_IMPLIED_POSITION_CHANGED_FLAG; + if (STATE.compareAndSet(store, state, nextState)) { + return nextState; + } + } + } + + static long markConnected(InMemoryResumableFramesStore store, long expectedState) { + for (; ; ) { + final long state = store.state; + + if (state != expectedState) { + return state; + } + + if (isFinalized(state)) { + return state; + } + + final long nextState = state ^ PENDING_CONNECTION_FLAG | CONNECTED_FLAG; + if (STATE.compareAndSet(store, state, nextState)) { + return nextState; + } + } + } + + static long markTerminated(InMemoryResumableFramesStore store) { + for (; ; ) { + final long state = store.state; + + if (isFinalized(state)) { + return state; + } + + final long nextState = + (state & MAX_WORK_IN_PROGRESS) == MAX_WORK_IN_PROGRESS ? state : (state + 1); + if (STATE.compareAndSet(store, state, nextState | TERMINATED_FLAG)) { + return state; + } + } + } + + static long markDisposed(InMemoryResumableFramesStore store) { + for (; ; ) { + final long state = store.state; + + if (isFinalized(state)) { + return state; + } + + final long nextState = + (state & MAX_WORK_IN_PROGRESS) == MAX_WORK_IN_PROGRESS ? state : (state + 1); + if (STATE.compareAndSet(store, state, nextState | DISPOSED_FLAG)) { + return state; + } + } + } + + static void clearAndFinalize(InMemoryResumableFramesStore store) { + final Fuseable.QueueSubscription qs = store.framesSubscriber.qs; + for (; ; ) { + final long state = store.state; + + qs.clear(); + store.clearCache(); + + if (isFinalized(state)) { + return; + } + + if (STATE.compareAndSet(store, state, state | FINALIZED_FLAG & ~MAX_WORK_IN_PROGRESS)) { + store.disposed.tryEmitEmpty(); + store.framesSubscriber.onComplete(); + return; + } + } + } + + static boolean isConnected(long state) { + return (state & CONNECTED_FLAG) == CONNECTED_FLAG; + } + + static boolean hasRemoteImpliedPositionChanged(long state) { + return (state & REMOTE_IMPLIED_POSITION_CHANGED_FLAG) == REMOTE_IMPLIED_POSITION_CHANGED_FLAG; + } + + static boolean hasPendingConnection(long state) { + return (state & PENDING_CONNECTION_FLAG) == PENDING_CONNECTION_FLAG; + } + + static boolean hasFrames(long state) { + return (state & HAS_FRAME_FLAG) == HAS_FRAME_FLAG; + } + + static boolean isTerminated(long state) { + return (state & TERMINATED_FLAG) == TERMINATED_FLAG; + } + + static boolean isDisposed(long state) { + return (state & DISPOSED_FLAG) == DISPOSED_FLAG; + } + + static boolean isFinalized(long state) { + return (state & FINALIZED_FLAG) == FINALIZED_FLAG; + } + + static boolean isWorkInProgress(long state) { + return (state & MAX_WORK_IN_PROGRESS) > 0; } } 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 6e90e6d63..18cd7167a 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -18,8 +18,11 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.CharsetUtil; import io.rsocket.DuplexConnection; import io.rsocket.RSocketErrorException; +import io.rsocket.exceptions.ConnectionCloseException; +import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.internal.UnboundedProcessor; import java.net.SocketAddress; @@ -35,13 +38,15 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; import reactor.core.publisher.Sinks; +import reactor.util.annotation.Nullable; public class ResumableDuplexConnection extends Flux implements DuplexConnection, Subscription { static final Logger logger = LoggerFactory.getLogger(ResumableDuplexConnection.class); - final String tag; + final String side; + final String session; final ResumableFramesStore resumableFramesStore; final UnboundedProcessor savableFramesSender; @@ -66,8 +71,12 @@ public class ResumableDuplexConnection extends Flux int connectionIndex = 0; public ResumableDuplexConnection( - String tag, DuplexConnection initialConnection, ResumableFramesStore resumableFramesStore) { - this.tag = tag; + String side, + ByteBuf session, + DuplexConnection initialConnection, + ResumableFramesStore resumableFramesStore) { + this.side = side; + this.session = session.toString(CharsetUtil.UTF_8); this.onConnectionClosedSink = Sinks.unsafe().many().unicast().onBackpressureBuffer(); this.resumableFramesStore = resumableFramesStore; this.savableFramesSender = new UnboundedProcessor(); @@ -94,29 +103,51 @@ public boolean connect(DuplexConnection nextConnection) { } void initConnection(DuplexConnection nextConnection) { - logger.debug("Tag {}. Initializing connection {}", tag, nextConnection); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[{}]|Session[{}]. Connecting to DuplexConnection[{}]", + side, + session, + nextConnection); + } final int currentConnectionIndex = connectionIndex; final FrameReceivingSubscriber frameReceivingSubscriber = - new FrameReceivingSubscriber(tag, resumableFramesStore, receiveSubscriber); + new FrameReceivingSubscriber(side, resumableFramesStore, receiveSubscriber); this.connectionIndex = currentConnectionIndex + 1; this.activeReceivingSubscriber = frameReceivingSubscriber; - final Disposable disposable = + final Disposable resumeStreamSubscription = resumableFramesStore .resumeStream() - .subscribe(f -> nextConnection.sendFrame(FrameHeaderCodec.streamId(f), f)); + .subscribe( + f -> nextConnection.sendFrame(FrameHeaderCodec.streamId(f), f), + t -> sendErrorAndClose(new ConnectionErrorException(t.getMessage())), + () -> + sendErrorAndClose( + new ConnectionCloseException("Connection Closed Unexpectedly"))); nextConnection.receive().subscribe(frameReceivingSubscriber); nextConnection .onClose() .doFinally( __ -> { frameReceivingSubscriber.dispose(); - disposable.dispose(); + resumeStreamSubscription.dispose(); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[{}]|Session[{}]. Disconnected from DuplexConnection[{}]", + side, + session, + nextConnection); + } Sinks.EmitResult result = onConnectionClosedSink.tryEmitNext(currentConnectionIndex); if (!result.equals(Sinks.EmitResult.OK)) { - logger.error("Failed to notify session of closed connection: {}", result); + logger.error( + "Side[{}]|Session[{}]. Failed to notify session of closed connection: {}", + side, + session, + result); } }) .subscribe(); @@ -196,6 +227,10 @@ public Mono onClose() { @Override public void dispose() { + dispose(null); + } + + void dispose(@Nullable Throwable e) { final DuplexConnection activeConnection = ACTIVE_CONNECTION.getAndSet(this, DisposedConnection.INSTANCE); if (activeConnection == DisposedConnection.INSTANCE) { @@ -206,11 +241,20 @@ public void dispose() { activeConnection.dispose(); } + if (logger.isDebugEnabled()) { + logger.debug("Side[{}]|Session[{}]. Disposing...", side, session); + } + framesSaverDisposable.dispose(); activeReceivingSubscriber.dispose(); savableFramesSender.dispose(); onConnectionClosedSink.tryEmitComplete(); - onClose.tryEmitEmpty(); + + if (e != null) { + onClose.tryEmitError(e); + } else { + onClose.tryEmitEmpty(); + } } @Override 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 b62c615f3..a57899cac 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -123,12 +123,15 @@ void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplex long impliedPosition = resumableFramesStore.frameImpliedPosition(); long position = resumableFramesStore.framePosition(); - logger.debug( - "Resume FRAME received. ClientResumeState{observedFramesPosition[{}], sentFramesPosition[{}]}, ServerResumeState{observedFramesPosition[{}], sentFramesPosition[{}]}", - remoteImpliedPos, - remotePos, - impliedPosition, - position); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[server]|Session[{}]. Resume FRAME received. ClientResumeState{observedFramesPosition[{}], sentFramesPosition[{}]}, ServerResumeState{observedFramesPosition[{}], sentFramesPosition[{}]}", + resumeToken, + remoteImpliedPos, + remotePos, + impliedPosition, + position); + } for (; ; ) { final Subscription subscription = this.s; @@ -138,6 +141,7 @@ void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplex final RejectedResumeException rejectedResumeException = new RejectedResumeException("resume_internal_error: Session Expired"); nextDuplexConnection.sendErrorAndClose(rejectedResumeException); + nextDuplexConnection.receive().subscribe().dispose(); return; } @@ -152,31 +156,47 @@ void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplex if (position != remoteImpliedPos) { resumableFramesStore.releaseFrames(remoteImpliedPos); } - nextDuplexConnection.sendFrame( - 0, ResumeOkFrameCodec.encode(allocator, resumableFramesStore.frameImpliedPosition())); - logger.debug("ResumeOK Frame has been sent"); + nextDuplexConnection.sendFrame(0, ResumeOkFrameCodec.encode(allocator, impliedPosition)); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[server]|Session[{}]. ResumeOKFrame[impliedPosition[{}]] has been sent", + resumeToken, + impliedPosition); + } } catch (Throwable t) { logger.debug("Exception occurred while releasing frames in the frameStore", t); tryTimeoutSession(); nextDuplexConnection.sendErrorAndClose(new RejectedResumeException(t.getMessage(), t)); + nextDuplexConnection.receive().subscribe().dispose(); return; } if (resumableConnection.connect(nextDuplexConnection)) { keepAliveSupport.start(); - logger.debug("Session[{}] has been resumed successfully", resumeToken); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[server]|Session[{}]. Session has been resumed successfully", resumeToken); + } } else { - logger.debug("Session has already been expired. Terminating received connection"); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[server]|Session[{}]. Session has already been expired. Terminating received connection", + resumeToken); + } final ConnectionErrorException connectionErrorException = new ConnectionErrorException("resume_internal_error: Session Expired"); nextDuplexConnection.sendErrorAndClose(connectionErrorException); + nextDuplexConnection.receive().subscribe().dispose(); } } else { - logger.debug( - "Mismatching remote and local state. Expected RemoteImpliedPosition[{}] to be greater or equal to the LocalPosition[{}] and RemotePosition[{}] to be less or equal to LocalImpliedPosition[{}]. Terminating received connection", - remoteImpliedPos, - position, - remotePos, - impliedPosition); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[server]|Session[{}]. Mismatching remote and local state. Expected RemoteImpliedPosition[{}] to be greater or equal to the LocalPosition[{}] and RemotePosition[{}] to be less or equal to LocalImpliedPosition[{}]. Terminating received connection", + resumeToken, + remoteImpliedPos, + position, + remotePos, + impliedPosition); + } tryTimeoutSession(); final RejectedResumeException rejectedResumeException = new RejectedResumeException( @@ -184,6 +204,7 @@ void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplex "resumption_pos=[ remote: { pos: %d, impliedPos: %d }, local: { pos: %d, impliedPos: %d }]", remotePos, remoteImpliedPos, position, impliedPosition)); nextDuplexConnection.sendErrorAndClose(rejectedResumeException); + nextDuplexConnection.receive().subscribe().dispose(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java b/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java index a595faa86..bba40d674 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java @@ -4,59 +4,123 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCounted; +import io.rsocket.RaceTestConstants; +import io.rsocket.internal.UnboundedProcessor; +import io.rsocket.internal.subscriber.AssertSubscriber; import java.util.Arrays; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.Disposable; +import reactor.core.publisher.Hooks; +import reactor.test.util.RaceTestUtils; public class InMemoryResumeStoreTest { @Test void saveNonResumableFrame() { - InMemoryResumableFramesStore store = inMemoryStore(25); - ByteBuf frame1 = fakeConnectionFrame(10); - ByteBuf frame2 = fakeConnectionFrame(35); - store.saveFrames(Flux.just(frame1, frame2)).block(); + final InMemoryResumableFramesStore store = inMemoryStore(25); + final UnboundedProcessor sender = new UnboundedProcessor(); + + store.saveFrames(sender).subscribe(); + + final AssertSubscriber assertSubscriber = + store.resumeStream().subscribeWith(AssertSubscriber.create()); + + final ByteBuf frame1 = fakeConnectionFrame(10); + final ByteBuf frame2 = fakeConnectionFrame(35); + + sender.onNext(frame1); + sender.onNext(frame2); + assertThat(store.cachedFrames.size()).isZero(); assertThat(store.cacheSize).isZero(); - assertThat(store.position).isZero(); + assertThat(store.firstAvailableFramePosition).isZero(); + + assertSubscriber.assertValueCount(2).values().forEach(ByteBuf::release); + assertThat(frame1.refCnt()).isZero(); assertThat(frame2.refCnt()).isZero(); } @Test void saveWithoutTailRemoval() { - InMemoryResumableFramesStore store = inMemoryStore(25); - ByteBuf frame = fakeResumableFrame(10); - store.saveFrames(Flux.just(frame)).block(); + final InMemoryResumableFramesStore store = inMemoryStore(25); + final UnboundedProcessor sender = new UnboundedProcessor(); + + store.saveFrames(sender).subscribe(); + + final AssertSubscriber assertSubscriber = + store.resumeStream().subscribeWith(AssertSubscriber.create()); + + final ByteBuf frame = fakeResumableFrame(10); + + sender.onNext(frame); + assertThat(store.cachedFrames.size()).isEqualTo(1); assertThat(store.cacheSize).isEqualTo(frame.readableBytes()); - assertThat(store.position).isZero(); + assertThat(store.firstAvailableFramePosition).isZero(); + + assertSubscriber.assertValueCount(1).values().forEach(ByteBuf::release); + assertThat(frame.refCnt()).isOne(); } @Test void saveRemoveOneFromTail() { - InMemoryResumableFramesStore store = inMemoryStore(25); - ByteBuf frame1 = fakeResumableFrame(20); - ByteBuf frame2 = fakeResumableFrame(10); - store.saveFrames(Flux.just(frame1, frame2)).block(); + final InMemoryResumableFramesStore store = inMemoryStore(25); + final UnboundedProcessor sender = new UnboundedProcessor(); + + store.saveFrames(sender).subscribe(); + + final AssertSubscriber assertSubscriber = + store.resumeStream().subscribeWith(AssertSubscriber.create()); + final ByteBuf frame1 = fakeResumableFrame(20); + final ByteBuf frame2 = fakeResumableFrame(10); + + sender.onNext(frame1); + sender.onNext(frame2); + assertThat(store.cachedFrames.size()).isOne(); assertThat(store.cacheSize).isEqualTo(frame2.readableBytes()); - assertThat(store.position).isEqualTo(frame1.readableBytes()); + assertThat(store.firstAvailableFramePosition).isEqualTo(frame1.readableBytes()); + + assertSubscriber.assertValueCount(2).values().forEach(ByteBuf::release); + assertThat(frame1.refCnt()).isZero(); assertThat(frame2.refCnt()).isOne(); } @Test void saveRemoveTwoFromTail() { - InMemoryResumableFramesStore store = inMemoryStore(25); - ByteBuf frame1 = fakeResumableFrame(10); - ByteBuf frame2 = fakeResumableFrame(10); - ByteBuf frame3 = fakeResumableFrame(20); - store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); + final InMemoryResumableFramesStore store = inMemoryStore(25); + final UnboundedProcessor sender = new UnboundedProcessor(); + + store.saveFrames(sender).subscribe(); + + final AssertSubscriber assertSubscriber = + store.resumeStream().subscribeWith(AssertSubscriber.create()); + + final ByteBuf frame1 = fakeResumableFrame(10); + final ByteBuf frame2 = fakeResumableFrame(10); + final ByteBuf frame3 = fakeResumableFrame(20); + + sender.onNext(frame1); + sender.onNext(frame2); + sender.onNext(frame3); + assertThat(store.cachedFrames.size()).isOne(); assertThat(store.cacheSize).isEqualTo(frame3.readableBytes()); - assertThat(store.position).isEqualTo(size(frame1, frame2)); + assertThat(store.firstAvailableFramePosition).isEqualTo(size(frame1, frame2)); + + assertSubscriber.assertValueCount(3).values().forEach(ByteBuf::release); + assertThat(frame1.refCnt()).isZero(); assertThat(frame2.refCnt()).isZero(); assertThat(frame3.refCnt()).isOne(); @@ -64,14 +128,27 @@ void saveRemoveTwoFromTail() { @Test void saveBiggerThanStore() { - InMemoryResumableFramesStore store = inMemoryStore(25); - ByteBuf frame1 = fakeResumableFrame(10); - ByteBuf frame2 = fakeResumableFrame(10); - ByteBuf frame3 = fakeResumableFrame(30); - store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); + final InMemoryResumableFramesStore store = inMemoryStore(25); + final UnboundedProcessor sender = new UnboundedProcessor(); + + store.saveFrames(sender).subscribe(); + + final AssertSubscriber assertSubscriber = + store.resumeStream().subscribeWith(AssertSubscriber.create()); + final ByteBuf frame1 = fakeResumableFrame(10); + final ByteBuf frame2 = fakeResumableFrame(10); + final ByteBuf frame3 = fakeResumableFrame(30); + + sender.onNext(frame1); + sender.onNext(frame2); + sender.onNext(frame3); + assertThat(store.cachedFrames.size()).isZero(); assertThat(store.cacheSize).isZero(); - assertThat(store.position).isEqualTo(size(frame1, frame2, frame3)); + assertThat(store.firstAvailableFramePosition).isEqualTo(size(frame1, frame2, frame3)); + + assertSubscriber.assertValueCount(3).values().forEach(ByteBuf::release); + assertThat(frame1.refCnt()).isZero(); assertThat(frame2.refCnt()).isZero(); assertThat(frame3.refCnt()).isZero(); @@ -79,15 +156,30 @@ void saveBiggerThanStore() { @Test void releaseFrames() { - InMemoryResumableFramesStore store = inMemoryStore(100); - ByteBuf frame1 = fakeResumableFrame(10); - ByteBuf frame2 = fakeResumableFrame(10); - ByteBuf frame3 = fakeResumableFrame(30); - store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); + final InMemoryResumableFramesStore store = inMemoryStore(100); + + final UnboundedProcessor producer = new UnboundedProcessor(); + store.saveFrames(producer).subscribe(); + + final AssertSubscriber assertSubscriber = + store.resumeStream().subscribeWith(AssertSubscriber.create()); + + final ByteBuf frame1 = fakeResumableFrame(10); + final ByteBuf frame2 = fakeResumableFrame(10); + final ByteBuf frame3 = fakeResumableFrame(30); + + producer.onNext(frame1); + producer.onNext(frame2); + producer.onNext(frame3); + store.releaseFrames(20); + assertThat(store.cachedFrames.size()).isOne(); assertThat(store.cacheSize).isEqualTo(frame3.readableBytes()); - assertThat(store.position).isEqualTo(size(frame1, frame2)); + assertThat(store.firstAvailableFramePosition).isEqualTo(size(frame1, frame2)); + + assertSubscriber.assertValueCount(3).values().forEach(ByteBuf::release); + assertThat(frame1.refCnt()).isZero(); assertThat(frame2.refCnt()).isZero(); assertThat(frame3.refCnt()).isOne(); @@ -95,20 +187,350 @@ void releaseFrames() { @Test void receiveImpliedPosition() { - InMemoryResumableFramesStore store = inMemoryStore(100); + final InMemoryResumableFramesStore store = inMemoryStore(100); + ByteBuf frame1 = fakeResumableFrame(10); ByteBuf frame2 = fakeResumableFrame(30); + store.resumableFrameReceived(frame1); store.resumableFrameReceived(frame2); + assertThat(store.frameImpliedPosition()).isEqualTo(size(frame1, frame2)); } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void ensuresCleansOnTerminal(boolean hasSubscriber) { + final InMemoryResumableFramesStore store = inMemoryStore(100); + + final UnboundedProcessor producer = new UnboundedProcessor(); + store.saveFrames(producer).subscribe(); + + final AssertSubscriber assertSubscriber = + hasSubscriber ? store.resumeStream().subscribeWith(AssertSubscriber.create()) : null; + + final ByteBuf frame1 = fakeResumableFrame(10); + final ByteBuf frame2 = fakeResumableFrame(10); + final ByteBuf frame3 = fakeResumableFrame(30); + + producer.onNext(frame1); + producer.onNext(frame2); + producer.onNext(frame3); + producer.onComplete(); + + assertThat(store.cachedFrames.size()).isZero(); + assertThat(store.cacheSize).isZero(); + + assertThat(producer.isDisposed()).isTrue(); + + if (hasSubscriber) { + assertSubscriber.assertValueCount(3).assertTerminated().values().forEach(ByteBuf::release); + } + + assertThat(frame1.refCnt()).isZero(); + assertThat(frame2.refCnt()).isZero(); + assertThat(frame3.refCnt()).isZero(); + } + + @Test + void ensuresCleansOnTerminalLateSubscriber() { + final InMemoryResumableFramesStore store = inMemoryStore(100); + + final UnboundedProcessor producer = new UnboundedProcessor(); + store.saveFrames(producer).subscribe(); + + final ByteBuf frame1 = fakeResumableFrame(10); + final ByteBuf frame2 = fakeResumableFrame(10); + final ByteBuf frame3 = fakeResumableFrame(30); + + producer.onNext(frame1); + producer.onNext(frame2); + producer.onNext(frame3); + producer.onComplete(); + + assertThat(store.cachedFrames.size()).isZero(); + assertThat(store.cacheSize).isZero(); + + assertThat(producer.isDisposed()).isTrue(); + + final AssertSubscriber assertSubscriber = + store.resumeStream().subscribeWith(AssertSubscriber.create()); + assertSubscriber.assertTerminated(); + + assertThat(frame1.refCnt()).isZero(); + assertThat(frame2.refCnt()).isZero(); + assertThat(frame3.refCnt()).isZero(); + } + + @ParameterizedTest(name = "Sending vs Reconnect Race Test. WithLateSubscriber[{0}]") + @ValueSource(booleans = {true, false}) + void sendingVsReconnectRaceTest(boolean withLateSubscriber) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final InMemoryResumableFramesStore store = inMemoryStore(Integer.MAX_VALUE); + final UnboundedProcessor frames = new UnboundedProcessor(); + final BlockingQueue receivedFrames = new ArrayBlockingQueue<>(10); + final AtomicInteger receivedPosition = new AtomicInteger(); + + store.saveFrames(frames).subscribe(); + + final Consumer consumer = + f -> { + if (ResumableDuplexConnection.isResumableFrame(f)) { + receivedPosition.addAndGet(f.readableBytes()); + receivedFrames.offer(f); + return; + } + f.release(); + }; + final AtomicReference disposableReference = + new AtomicReference<>( + withLateSubscriber ? null : store.resumeStream().subscribe(consumer)); + + final ByteBuf byteBuf1 = fakeResumableFrame(5); + final ByteBuf byteBuf11 = fakeConnectionFrame(5); + final ByteBuf byteBuf2 = fakeResumableFrame(6); + final ByteBuf byteBuf21 = fakeConnectionFrame(5); + final ByteBuf byteBuf3 = fakeResumableFrame(7); + final ByteBuf byteBuf31 = fakeConnectionFrame(5); + final ByteBuf byteBuf4 = fakeResumableFrame(8); + final ByteBuf byteBuf41 = fakeConnectionFrame(5); + final ByteBuf byteBuf5 = fakeResumableFrame(25); + final ByteBuf byteBuf51 = fakeConnectionFrame(35); + + RaceTestUtils.race( + () -> { + if (withLateSubscriber) { + disposableReference.set(store.resumeStream().subscribe(consumer)); + } + + // disconnect + disposableReference.get().dispose(); + + while (InMemoryResumableFramesStore.isWorkInProgress(store.state)) { + // ignore + } + + // mimic RESUME_OK frame received + store.releaseFrames(receivedPosition.get()); + disposableReference.set(store.resumeStream().subscribe(consumer)); + + // disconnect + disposableReference.get().dispose(); + + while (InMemoryResumableFramesStore.isWorkInProgress(store.state)) { + // ignore + } + + // mimic RESUME_OK frame received + store.releaseFrames(receivedPosition.get()); + disposableReference.set(store.resumeStream().subscribe(consumer)); + }, + () -> { + frames.onNext(byteBuf1); + frames.onNextPrioritized(byteBuf11); + frames.onNext(byteBuf2); + frames.onNext(byteBuf3); + frames.onNextPrioritized(byteBuf31); + frames.onNext(byteBuf4); + frames.onNext(byteBuf5); + }, + () -> { + frames.onNextPrioritized(byteBuf21); + frames.onNextPrioritized(byteBuf41); + frames.onNextPrioritized(byteBuf51); + }); + + store.releaseFrames(receivedFrames.stream().mapToInt(ByteBuf::readableBytes).sum()); + + assertThat(store.cacheSize).isZero(); + assertThat(store.cachedFrames).isEmpty(); + + assertThat(receivedFrames) + .hasSize(5) + .containsSequence(byteBuf1, byteBuf2, byteBuf3, byteBuf4, byteBuf5); + receivedFrames.forEach(ReferenceCounted::release); + + assertThat(byteBuf1.refCnt()).isZero(); + assertThat(byteBuf11.refCnt()).isZero(); + assertThat(byteBuf2.refCnt()).isZero(); + assertThat(byteBuf21.refCnt()).isZero(); + assertThat(byteBuf3.refCnt()).isZero(); + assertThat(byteBuf31.refCnt()).isZero(); + assertThat(byteBuf4.refCnt()).isZero(); + assertThat(byteBuf41.refCnt()).isZero(); + assertThat(byteBuf5.refCnt()).isZero(); + assertThat(byteBuf51.refCnt()).isZero(); + } + } + + @ParameterizedTest( + name = "Sending vs Reconnect with incorrect position Race Test. WithLateSubscriber[{0}]") + @ValueSource(booleans = {true, false}) + void incorrectReleaseFramesWithOnNextRaceTest(boolean withLateSubscriber) { + Hooks.onErrorDropped(t -> {}); + try { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final InMemoryResumableFramesStore store = inMemoryStore(Integer.MAX_VALUE); + final UnboundedProcessor frames = new UnboundedProcessor(); + + store.saveFrames(frames).subscribe(); + + final AtomicInteger terminationCnt = new AtomicInteger(); + final Consumer consumer = ReferenceCounted::release; + final Consumer errorConsumer = __ -> terminationCnt.incrementAndGet(); + final AtomicReference disposableReference = + new AtomicReference<>( + withLateSubscriber + ? null + : store.resumeStream().subscribe(consumer, errorConsumer)); + + final ByteBuf byteBuf1 = fakeResumableFrame(5); + final ByteBuf byteBuf11 = fakeConnectionFrame(5); + final ByteBuf byteBuf2 = fakeResumableFrame(6); + final ByteBuf byteBuf21 = fakeConnectionFrame(5); + final ByteBuf byteBuf3 = fakeResumableFrame(7); + final ByteBuf byteBuf31 = fakeConnectionFrame(5); + final ByteBuf byteBuf4 = fakeResumableFrame(8); + final ByteBuf byteBuf41 = fakeConnectionFrame(5); + final ByteBuf byteBuf5 = fakeResumableFrame(25); + final ByteBuf byteBuf51 = fakeConnectionFrame(35); + + RaceTestUtils.race( + () -> { + if (withLateSubscriber) { + disposableReference.set(store.resumeStream().subscribe(consumer, errorConsumer)); + } + // disconnect + disposableReference.get().dispose(); + + // mimic RESUME_OK frame received but with incorrect position + store.releaseFrames(25); + disposableReference.set(store.resumeStream().subscribe(consumer, errorConsumer)); + }, + () -> { + frames.onNext(byteBuf1); + frames.onNextPrioritized(byteBuf11); + frames.onNext(byteBuf2); + frames.onNext(byteBuf3); + frames.onNextPrioritized(byteBuf31); + frames.onNext(byteBuf4); + frames.onNext(byteBuf5); + }, + () -> { + frames.onNextPrioritized(byteBuf21); + frames.onNextPrioritized(byteBuf41); + frames.onNextPrioritized(byteBuf51); + }); + + assertThat(store.cacheSize).isZero(); + assertThat(store.cachedFrames).isEmpty(); + assertThat(disposableReference.get().isDisposed()).isTrue(); + assertThat(terminationCnt).hasValue(1); + + assertThat(byteBuf1.refCnt()).isZero(); + assertThat(byteBuf11.refCnt()).isZero(); + assertThat(byteBuf2.refCnt()).isZero(); + assertThat(byteBuf21.refCnt()).isZero(); + assertThat(byteBuf3.refCnt()).isZero(); + assertThat(byteBuf31.refCnt()).isZero(); + assertThat(byteBuf4.refCnt()).isZero(); + assertThat(byteBuf41.refCnt()).isZero(); + assertThat(byteBuf5.refCnt()).isZero(); + assertThat(byteBuf51.refCnt()).isZero(); + } + } finally { + Hooks.resetOnErrorDropped(); + } + } + + @ParameterizedTest( + name = + "Dispose vs Sending vs Reconnect with incorrect position Race Test. WithLateSubscriber[{0}]") + @ValueSource(booleans = {true, false}) + void incorrectReleaseFramesWithOnNextWithDisposeRaceTest(boolean withLateSubscriber) { + Hooks.onErrorDropped(t -> {}); + try { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final InMemoryResumableFramesStore store = inMemoryStore(Integer.MAX_VALUE); + final UnboundedProcessor frames = new UnboundedProcessor(); + + store.saveFrames(frames).subscribe(); + + final AtomicInteger terminationCnt = new AtomicInteger(); + final Consumer consumer = ReferenceCounted::release; + final Consumer errorConsumer = __ -> terminationCnt.incrementAndGet(); + final AtomicReference disposableReference = + new AtomicReference<>( + withLateSubscriber + ? null + : store.resumeStream().subscribe(consumer, errorConsumer)); + + final ByteBuf byteBuf1 = fakeResumableFrame(5); + final ByteBuf byteBuf11 = fakeConnectionFrame(5); + final ByteBuf byteBuf2 = fakeResumableFrame(6); + final ByteBuf byteBuf21 = fakeConnectionFrame(5); + final ByteBuf byteBuf3 = fakeResumableFrame(7); + final ByteBuf byteBuf31 = fakeConnectionFrame(5); + final ByteBuf byteBuf4 = fakeResumableFrame(8); + final ByteBuf byteBuf41 = fakeConnectionFrame(5); + final ByteBuf byteBuf5 = fakeResumableFrame(25); + final ByteBuf byteBuf51 = fakeConnectionFrame(35); + + RaceTestUtils.race( + () -> { + if (withLateSubscriber) { + disposableReference.set(store.resumeStream().subscribe(consumer, errorConsumer)); + } + // disconnect + disposableReference.get().dispose(); + + // mimic RESUME_OK frame received but with incorrect position + store.releaseFrames(25); + disposableReference.set(store.resumeStream().subscribe(consumer, errorConsumer)); + }, + () -> { + frames.onNext(byteBuf1); + frames.onNextPrioritized(byteBuf11); + frames.onNext(byteBuf2); + frames.onNext(byteBuf3); + frames.onNextPrioritized(byteBuf31); + frames.onNext(byteBuf4); + frames.onNext(byteBuf5); + }, + () -> { + frames.onNextPrioritized(byteBuf21); + frames.onNextPrioritized(byteBuf41); + frames.onNextPrioritized(byteBuf51); + }, + store::dispose); + + assertThat(store.cacheSize).isZero(); + assertThat(store.cachedFrames).isEmpty(); + assertThat(disposableReference.get().isDisposed()).isTrue(); + assertThat(terminationCnt).hasValueGreaterThanOrEqualTo(1).hasValueLessThanOrEqualTo(2); + + assertThat(byteBuf1.refCnt()).isZero(); + assertThat(byteBuf11.refCnt()).isZero(); + assertThat(byteBuf2.refCnt()).isZero(); + assertThat(byteBuf21.refCnt()).isZero(); + assertThat(byteBuf3.refCnt()).isZero(); + assertThat(byteBuf31.refCnt()).isZero(); + assertThat(byteBuf4.refCnt()).isZero(); + assertThat(byteBuf41.refCnt()).isZero(); + assertThat(byteBuf5.refCnt()).isZero(); + assertThat(byteBuf51.refCnt()).isZero(); + } + } finally { + Hooks.resetOnErrorDropped(); + } + } + private int size(ByteBuf... byteBufs) { return Arrays.stream(byteBufs).mapToInt(ByteBuf::readableBytes).sum(); } private static InMemoryResumableFramesStore inMemoryStore(int size) { - return new InMemoryResumableFramesStore("test", size); + return new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, size); } private static ByteBuf fakeResumableFrame(int size) { 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 b2dad0022..5eb78fabe 100644 --- a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java @@ -182,7 +182,7 @@ private static Mono newClientRSocket( .resume( new Resume() .sessionDuration(Duration.ofSeconds(sessionDurationSeconds)) - .storeFactory(t -> new InMemoryResumableFramesStore("client", 500_000)) + .storeFactory(t -> new InMemoryResumableFramesStore("client", t, 500_000)) .cleanupStoreOnKeepAlive() .retry(Retry.fixedDelay(Long.MAX_VALUE, Duration.ofSeconds(1)))) .keepAlive(Duration.ofSeconds(5), Duration.ofMinutes(5)) @@ -199,7 +199,7 @@ private static Mono newServerRSocket(int sessionDurationSecond new Resume() .sessionDuration(Duration.ofSeconds(sessionDurationSeconds)) .cleanupStoreOnKeepAlive() - .storeFactory(t -> new InMemoryResumableFramesStore("server", 500_000))) + .storeFactory(t -> new InMemoryResumableFramesStore("server", t, 500_000))) .bind(serverTransport(SERVER_HOST, SERVER_PORT)); } @@ -212,7 +212,7 @@ public Flux requestChannel(Publisher payloads) { return duplicate( Flux.interval(Duration.ofMillis(1)) .onBackpressureLatest() - .publishOn(Schedulers.elastic()), + .publishOn(Schedulers.boundedElastic()), 20) .map(v -> DefaultPayload.create(String.valueOf(counter.getAndIncrement()))) .takeUntilOther(Flux.from(payloads).then()); 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 0bae8cd69..5384c7e8d 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -51,7 +51,6 @@ import org.assertj.core.api.Assertions; import org.assertj.core.api.Assumptions; 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 org.reactivestreams.Subscription; @@ -60,7 +59,6 @@ import reactor.core.Exceptions; import reactor.core.Fuseable; 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; @@ -93,14 +91,8 @@ static String read(String resourceName) { } } - @BeforeEach - default void setUp() { - Hooks.onOperatorDebug(); - } - @AfterEach default void close() { - Hooks.resetOnOperatorDebug(); getTransportPair().responder.awaitAllInteractionTermination(getTimeout()); getTransportPair().dispose(); getTransportPair().awaitClosed(); @@ -547,7 +539,7 @@ public TransportPair( "Server", duplexConnection, Duration.ofMillis( - ThreadLocalRandom.current().nextInt(10, 1500))) + ThreadLocalRandom.current().nextInt(100, 1000))) : duplexConnection); } }); @@ -555,7 +547,8 @@ public TransportPair( if (withResumability) { rSocketServer.resume( new Resume() - .storeFactory(__ -> new InMemoryResumableFramesStore("server", Integer.MAX_VALUE))); + .storeFactory( + token -> new InMemoryResumableFramesStore("server", token, Integer.MAX_VALUE))); } if (withRandomFragmentation) { @@ -568,7 +561,7 @@ public TransportPair( final RSocketConnector rSocketConnector = RSocketConnector.create() .payloadDecoder(PayloadDecoder.ZERO_COPY) - .keepAlive(Duration.ofMillis(Integer.MAX_VALUE), Duration.ofMillis(Integer.MAX_VALUE)) + .keepAlive(Duration.ofMillis(10), Duration.ofHours(1)) .interceptors( registry -> { if (runClientWithAsyncInterceptors && !withResumability) { @@ -594,7 +587,7 @@ public TransportPair( "Client", duplexConnection, Duration.ofMillis( - ThreadLocalRandom.current().nextInt(1, 2000))) + ThreadLocalRandom.current().nextInt(10, 1500))) : duplexConnection); } }); @@ -602,7 +595,8 @@ public TransportPair( if (withResumability) { rSocketConnector.resume( new Resume() - .storeFactory(__ -> new InMemoryResumableFramesStore("client", Integer.MAX_VALUE))); + .storeFactory( + token -> new InMemoryResumableFramesStore("client", token, Integer.MAX_VALUE))); } if (withRandomFragmentation) { diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java index 8bea7c682..51c812cc3 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java @@ -19,9 +19,7 @@ import io.rsocket.test.TransportTest; import java.time.Duration; import java.util.UUID; -import org.junit.jupiter.api.Disabled; -@Disabled("leaking somewhere for no clear reason") final class LocalResumableTransportTest implements TransportTest { private final TransportPair transportPair = diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableWithFragmentationTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableWithFragmentationTransportTest.java new file mode 100644 index 000000000..124cecec9 --- /dev/null +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableWithFragmentationTransportTest.java @@ -0,0 +1,42 @@ +/* + * 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.TransportTest; +import java.time.Duration; +import java.util.UUID; + +final class LocalResumableWithFragmentationTransportTest implements TransportTest { + + private final TransportPair transportPair = + new TransportPair<>( + () -> "test-" + UUID.randomUUID(), + (address, server, allocator) -> LocalClientTransport.create(address, allocator), + (address, allocator) -> LocalServerTransport.create(address), + true, + true); + + @Override + public Duration getTimeout() { + return Duration.ofSeconds(10); + } + + @Override + public TransportPair getTransportPair() { + return transportPair; + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableWithFragmentationTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableWithFragmentationTransportTest.java new file mode 100644 index 000000000..7d9d80542 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableWithFragmentationTransportTest.java @@ -0,0 +1,55 @@ +/* + * 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.netty.channel.ChannelOption; +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.time.Duration; +import reactor.netty.tcp.TcpClient; +import reactor.netty.tcp.TcpServer; + +final class TcpResumableWithFragmentationTransportTest implements TransportTest { + + private final TransportPair transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + TcpClientTransport.create( + TcpClient.create() + .remoteAddress(server::address) + .option(ChannelOption.ALLOCATOR, allocator)), + (address, allocator) -> + TcpServerTransport.create( + TcpServer.create() + .bindAddress(() -> address) + .option(ChannelOption.ALLOCATOR, allocator)), + true, + true); + + @Override + public Duration getTimeout() { + return Duration.ofMinutes(3); + } + + @Override + public TransportPair getTransportPair() { + return transportPair; + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableTransportTest.java new file mode 100644 index 000000000..34dc99ae0 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableTransportTest.java @@ -0,0 +1,58 @@ +/* + * 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.netty.channel.ChannelOption; +import io.rsocket.test.TransportTest; +import io.rsocket.transport.netty.client.WebsocketClientTransport; +import io.rsocket.transport.netty.server.WebsocketServerTransport; +import java.net.InetSocketAddress; +import java.time.Duration; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.server.HttpServer; + +final class WebsocketResumableTransportTest implements TransportTest { + + private final TransportPair transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + WebsocketClientTransport.create( + HttpClient.create() + .host(server.address().getHostName()) + .port(server.address().getPort()) + .option(ChannelOption.ALLOCATOR, allocator), + ""), + (address, allocator) -> + WebsocketServerTransport.create( + HttpServer.create() + .host(address.getHostName()) + .port(address.getPort()) + .option(ChannelOption.ALLOCATOR, allocator)), + false, + true); + + @Override + public Duration getTimeout() { + return Duration.ofMinutes(3); + } + + @Override + public TransportPair getTransportPair() { + return transportPair; + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableWithFragmentationTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableWithFragmentationTransportTest.java new file mode 100644 index 000000000..21c027e88 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableWithFragmentationTransportTest.java @@ -0,0 +1,58 @@ +/* + * 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.netty.channel.ChannelOption; +import io.rsocket.test.TransportTest; +import io.rsocket.transport.netty.client.WebsocketClientTransport; +import io.rsocket.transport.netty.server.WebsocketServerTransport; +import java.net.InetSocketAddress; +import java.time.Duration; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.server.HttpServer; + +final class WebsocketResumableWithFragmentationTransportTest implements TransportTest { + + private final TransportPair transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + WebsocketClientTransport.create( + HttpClient.create() + .host(server.address().getHostName()) + .port(server.address().getPort()) + .option(ChannelOption.ALLOCATOR, allocator), + ""), + (address, allocator) -> + WebsocketServerTransport.create( + HttpServer.create() + .host(address.getHostName()) + .port(address.getPort()) + .option(ChannelOption.ALLOCATOR, allocator)), + true, + true); + + @Override + public Duration getTimeout() { + return Duration.ofMinutes(3); + } + + @Override + public TransportPair getTransportPair() { + return transportPair; + } +} From a6f954785b13e91186878ca407613902b3c231b7 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 2 Jun 2021 23:08:22 +0300 Subject: [PATCH 044/116] improves LocalDuplexConnection (#onClose notification + ByteBufs releases) At the moment, the onClose hook has no "wait until cleaned" logic, which leads to unpredicted behaviors when used with resumability or others scenarios where we need to wait until all the queues are cleaned and there are no other resources in use (e.g. ByteBufs). For that porpuse, this commit adds onFinalizeHook to the UnboundedProcessor so we can now listen when the UnboundedProcessor is finalized and only after that send the onClose signal Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .../rsocket/internal/UnboundedProcessor.java | 10 +++++++ .../transport/local/LocalClientTransport.java | 15 ++++++---- .../local/LocalDuplexConnection.java | 30 +++++++++++-------- 3 files changed, 37 insertions(+), 18 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 9e7500465..c3278a09c 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -45,6 +45,7 @@ public final class UnboundedProcessor extends FluxProcessor final Queue queue; final Queue priorityQueue; + final Runnable onFinalizedHook; boolean cancelled; boolean done; @@ -88,6 +89,11 @@ public final class UnboundedProcessor extends FluxProcessor boolean outputFused; public UnboundedProcessor() { + this(() -> {}); + } + + public UnboundedProcessor(Runnable onFinalizedHook) { + this.onFinalizedHook = onFinalizedHook; this.queue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); this.priorityQueue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); } @@ -793,6 +799,9 @@ static long markTerminatedOrFinalized(UnboundedProcessor instance) { } if (STATE.compareAndSet(instance, state, nextState | FLAG_TERMINATED)) { + if (isFinalized(nextState)) { + instance.onFinalizedHook.run(); + } return state; } } @@ -906,6 +915,7 @@ static void clearAndFinalize(UnboundedProcessor instance) { if (STATE.compareAndSet( instance, state, (state & ~MAX_WIP_VALUE & ~FLAG_HAS_VALUE) | FLAG_FINALIZED)) { + instance.onFinalizedHook.run(); break; } } 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 588f772d3..113b7a2f8 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 @@ -77,14 +77,17 @@ public Mono connect() { return Mono.error(new IllegalArgumentException("Could not find server: " + name)); } - UnboundedProcessor in = new UnboundedProcessor(); - UnboundedProcessor out = new UnboundedProcessor(); - Sinks.Empty closeSink = Sinks.empty(); + Sinks.One inSink = Sinks.one(); + Sinks.One outSink = Sinks.one(); + UnboundedProcessor in = new UnboundedProcessor(() -> inSink.tryEmitValue(inSink)); + UnboundedProcessor out = new UnboundedProcessor(() -> outSink.tryEmitValue(outSink)); - server.apply(new LocalDuplexConnection(name, allocator, out, in, closeSink)).subscribe(); + Mono onClose = inSink.asMono().zipWith(outSink.asMono()).then(); - return Mono.just( - (DuplexConnection) new LocalDuplexConnection(name, allocator, in, out, closeSink)); + server.apply(new LocalDuplexConnection(name, allocator, out, in, onClose)).subscribe(); + + return Mono.just( + new LocalDuplexConnection(name, allocator, in, out, onClose)); }); } } 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 5e18aa4cc..5c395156c 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 @@ -27,11 +27,9 @@ 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.Mono; import reactor.core.publisher.Operators; -import reactor.core.publisher.Sinks; /** An implementation of {@link DuplexConnection} that connects inside the same JVM. */ final class LocalDuplexConnection implements DuplexConnection { @@ -40,7 +38,7 @@ final class LocalDuplexConnection implements DuplexConnection { private final ByteBufAllocator allocator; private final Flux in; - private final Sinks.Empty onClose; + private final Mono onClose; private final UnboundedProcessor out; @@ -58,7 +56,7 @@ final class LocalDuplexConnection implements DuplexConnection { ByteBufAllocator allocator, Flux in, UnboundedProcessor out, - Sinks.Empty onClose) { + Mono onClose) { this.address = new LocalSocketAddress(name); this.allocator = Objects.requireNonNull(allocator, "allocator must not be null"); this.in = Objects.requireNonNull(in, "in must not be null"); @@ -69,24 +67,23 @@ final class LocalDuplexConnection implements DuplexConnection { @Override public void dispose() { out.onComplete(); - onClose.tryEmitEmpty(); } @Override - @SuppressWarnings("ConstantConditions") public boolean isDisposed() { - return onClose.scan(Scannable.Attr.TERMINATED) || onClose.scan(Scannable.Attr.CANCELLED); + return out.isDisposed(); } @Override public Mono onClose() { - return onClose.asMono(); + return onClose; } @Override public Flux receive() { return in.transform( - Operators.lift((__, actual) -> new ByteBufReleaserOperator(actual))); + Operators.lift( + (__, actual) -> new ByteBufReleaserOperator(actual, this))); } @Override @@ -119,11 +116,14 @@ static class ByteBufReleaserOperator implements CoreSubscriber, Subscription, Fuseable.QueueSubscription { final CoreSubscriber actual; + final LocalDuplexConnection parent; Subscription s; - public ByteBufReleaserOperator(CoreSubscriber actual) { + public ByteBufReleaserOperator( + CoreSubscriber actual, LocalDuplexConnection parent) { this.actual = actual; + this.parent = parent; } @Override @@ -136,17 +136,22 @@ public void onSubscribe(Subscription s) { @Override public void onNext(ByteBuf buf) { - actual.onNext(buf); - buf.release(); + try { + actual.onNext(buf); + } finally { + buf.release(); + } } @Override public void onError(Throwable t) { + parent.out.onError(t); actual.onError(t); } @Override public void onComplete() { + parent.out.onComplete(); actual.onComplete(); } @@ -158,6 +163,7 @@ public void request(long n) { @Override public void cancel() { s.cancel(); + parent.out.onComplete(); } @Override From 7e8b7859c7818f0eecd74ea360cf567c4f953672 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 1 Jun 2021 19:12:31 +0300 Subject: [PATCH 045/116] increase tests logging verbosity and fork every testclass on a new JVM Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dcc133833..7cfc3a5bd 100644 --- a/build.gradle +++ b/build.gradle @@ -153,8 +153,9 @@ subprojects { test { useJUnitPlatform() testLogging { - events "FAILED" + events "PASSED", "FAILED" showExceptions true + showCauses true exceptionFormat "FULL" stackTraceFilters "ENTRY_POINT" maxGranularity 3 @@ -169,6 +170,8 @@ subprojects { } } + forkEvery = 1 + if (isCiServer) { def stdout = new LinkedList() beforeTest { TestDescriptor td -> From b8c7c2e36c2c16c8be112b67e70073cd7b70e8df Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 2 Jun 2021 23:04:02 +0300 Subject: [PATCH 046/116] ensures no modifications during iteration since Processor/Subscription termination with `cancel` or `onError` leads to the following self-removal logic, it can happen that collection concurrent modification exception may appear. To avoid so we can copy all the entries and by doing so avoid any subsequent problems Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/core/RSocketRequester.java | 18 +++++++++++++----- .../java/io/rsocket/core/RSocketResponder.java | 9 +++++++-- 2 files changed, 20 insertions(+), 7 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 ae9bf6e97..d1a37e7e8 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -50,6 +50,8 @@ import io.rsocket.keepalive.KeepAliveSupport; import io.rsocket.lease.RequesterLeaseHandler; import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.Collection; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; @@ -772,20 +774,26 @@ private void terminate(Throwable e) { leaseHandler.dispose(); // Iterate explicitly to handle collisions with concurrent removals - for (IntObjectMap.PrimitiveEntry> entry : receivers.entries()) { + final IntObjectMap> receivers = this.receivers; + // copy to avoid collection modification from the foreach loop + final Collection> receiversCopy = + new ArrayList<>(receivers.values()); + for (Processor handler : receiversCopy) { try { - entry.value().onError(e); + handler.onError(e); } catch (Throwable ex) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Dropped exception", ex); } } } - // Iterate explicitly to handle collisions with concurrent removals - for (IntObjectMap.PrimitiveEntry entry : senders.entries()) { + final IntObjectMap senders = this.senders; + // copy to avoid collection modification from the foreach loop + final Collection sendersCopy = new ArrayList<>(senders.values()); + for (Subscription subscription : sendersCopy) { try { - entry.value().cancel(); + subscription.cancel(); } catch (Throwable ex) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Dropped exception", ex); 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 54f339c12..edb01ba16 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -40,6 +40,8 @@ import io.rsocket.internal.UnboundedProcessor; import io.rsocket.lease.ResponderLeaseHandler; import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.Collection; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; @@ -264,9 +266,12 @@ private void cleanup(Throwable e) { private synchronized void cleanUpSendingSubscriptions() { // Iterate explicitly to handle collisions with concurrent removals - for (IntObjectMap.PrimitiveEntry entry : sendingSubscriptions.entries()) { + final IntObjectMap sendingSubscriptions = this.sendingSubscriptions; + final Collection sendingSubscriptionsCopy = + new ArrayList<>(sendingSubscriptions.values()); + for (Subscription subscription : sendingSubscriptionsCopy) { try { - entry.value().cancel(); + subscription.cancel(); } catch (Throwable ex) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Dropped exception", ex); From 0e27df07c410e5e1381f26422ce62774cf5585cd Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sun, 28 Mar 2021 19:04:22 +0300 Subject: [PATCH 047/116] adds *FireAndForgetMono's stress tests Signed-off-by: Oleh Dokuka --- rsocket-core/build.gradle | 1 + .../FireAndForgetRequesterMonoStressTest.java | 115 +++++++ ...wFireAndForgetRequesterMonoStressTest.java | 288 ++++++++++++++++++ .../io/rsocket/core/StressSubscription.java | 2 +- .../core/TestRequesterResponderSupport.java | 39 +++ .../rsocket/core/UnpooledByteBufPayload.java | 155 ++++++++++ .../rsocket/core/RequesterLeaseTracker.java | 5 +- .../test/LeaksTrackingByteBufAllocator.java | 2 +- .../io/rsocket/test/TestDuplexConnection.java | 166 ++++++++++ 9 files changed, 769 insertions(+), 4 deletions(-) create mode 100644 rsocket-core/src/jcstress/java/io/rsocket/core/FireAndForgetRequesterMonoStressTest.java create mode 100644 rsocket-core/src/jcstress/java/io/rsocket/core/SlowFireAndForgetRequesterMonoStressTest.java create mode 100644 rsocket-core/src/jcstress/java/io/rsocket/core/TestRequesterResponderSupport.java create mode 100644 rsocket-core/src/jcstress/java/io/rsocket/core/UnpooledByteBufPayload.java create mode 100644 rsocket-test/src/main/java/io/rsocket/test/TestDuplexConnection.java diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index ece4b8e4c..25f6bffdc 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -42,6 +42,7 @@ dependencies { testImplementation 'org.hamcrest:hamcrest-library' + jcstressImplementation(project(":rsocket-test")) jcstressImplementation "ch.qos.logback:logback-classic" } diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/FireAndForgetRequesterMonoStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/core/FireAndForgetRequesterMonoStressTest.java new file mode 100644 index 000000000..e91be2451 --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/FireAndForgetRequesterMonoStressTest.java @@ -0,0 +1,115 @@ +package io.rsocket.core; + +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; + +import io.netty.buffer.ByteBuf; +import io.rsocket.test.TestDuplexConnection; +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.LLLL_Result; + +public abstract class FireAndForgetRequesterMonoStressTest { + + abstract static class BaseStressTest { + + final StressSubscriber outboundSubscriber = new StressSubscriber<>(); + + final StressSubscriber stressSubscriber = new StressSubscriber<>(); + + final TestDuplexConnection testDuplexConnection = + new TestDuplexConnection(this.outboundSubscriber, false); + + final TestRequesterResponderSupport requesterResponderSupport = + new TestRequesterResponderSupport(testDuplexConnection, StreamIdSupplier.clientSupplier()); + + final FireAndForgetRequesterMono source = source(); + + abstract FireAndForgetRequesterMono source(); + } + + @JCStressTest + @Outcome( + id = {"-9223372036854775808, 3, 1, 0"}, + expect = ACCEPTABLE) + @State + public static class TwoSubscribesRaceStressTest extends BaseStressTest { + + final StressSubscriber stressSubscriber1 = new StressSubscriber<>(); + + @Override + FireAndForgetRequesterMono source() { + return new FireAndForgetRequesterMono( + UnpooledByteBufPayload.create( + "test-data", "test-metadata", this.requesterResponderSupport.getAllocator()), + this.requesterResponderSupport); + } + + @Actor + public void subscribe1() { + this.source.subscribe(this.stressSubscriber); + } + + @Actor + public void subscribe2() { + this.source.subscribe(this.stressSubscriber1); + } + + @Arbiter + public void arbiter(LLLL_Result r) { + r.r1 = this.source.state; + r.r2 = + this.stressSubscriber.onCompleteCalls + + this.stressSubscriber.onErrorCalls * 2 + + this.stressSubscriber1.onCompleteCalls + + this.stressSubscriber1.onErrorCalls * 2; + r.r3 = this.outboundSubscriber.onNextCalls; + r.r4 = this.source.payload.refCnt(); + + this.outboundSubscriber.values.forEach(ByteBuf::release); + } + } + + @JCStressTest + @Outcome( + id = {"-9223372036854775808, 1, 1, 0"}, + expect = ACCEPTABLE, + desc = "frame delivered before cancellation") + @Outcome( + id = {"-9223372036854775808, 0, 0, 0"}, + expect = ACCEPTABLE, + desc = "cancellation happened first") + @State + public static class SubscribeAndCancelRaceStressTest extends BaseStressTest { + + @Override + FireAndForgetRequesterMono source() { + return new FireAndForgetRequesterMono( + UnpooledByteBufPayload.create( + "test-data", "test-metadata", this.requesterResponderSupport.getAllocator()), + this.requesterResponderSupport); + } + + @Actor + public void subscribe() { + this.source.subscribe(this.stressSubscriber); + } + + @Actor + public void cancel() { + this.stressSubscriber.cancel(); + } + + @Arbiter + public void arbiter(LLLL_Result r) { + r.r1 = this.source.state; + r.r2 = this.stressSubscriber.onCompleteCalls + this.stressSubscriber.onErrorCalls * 2; + r.r3 = this.outboundSubscriber.onNextCalls; + r.r4 = this.source.payload.refCnt(); + + this.outboundSubscriber.values.forEach(ByteBuf::release); + } + } +} diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/SlowFireAndForgetRequesterMonoStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/core/SlowFireAndForgetRequesterMonoStressTest.java new file mode 100644 index 000000000..5de7eb4b9 --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/SlowFireAndForgetRequesterMonoStressTest.java @@ -0,0 +1,288 @@ +package io.rsocket.core; + +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; + +import io.netty.buffer.ByteBuf; +import io.rsocket.frame.LeaseFrameCodec; +import io.rsocket.test.TestDuplexConnection; +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.LLLLL_Result; + +public abstract class SlowFireAndForgetRequesterMonoStressTest { + + abstract static class BaseStressTest { + + final StressSubscriber outboundSubscriber = new StressSubscriber<>(); + + final StressSubscriber stressSubscriber = new StressSubscriber<>(); + + final TestDuplexConnection testDuplexConnection = + new TestDuplexConnection(this.outboundSubscriber, false); + + final RequesterLeaseTracker requesterLeaseTracker = + new RequesterLeaseTracker("test", maximumAllowedAwaitingPermitHandlersNumber()); + + final TestRequesterResponderSupport requesterResponderSupport = + new TestRequesterResponderSupport( + testDuplexConnection, StreamIdSupplier.clientSupplier(), requesterLeaseTracker); + + final SlowFireAndForgetRequesterMono source = source(); + + abstract SlowFireAndForgetRequesterMono source(); + + abstract int maximumAllowedAwaitingPermitHandlersNumber(); + } + + @JCStressTest + @Outcome( + id = {"-9223372036854775808, 3, 1, 0, 0"}, + expect = ACCEPTABLE) + @State + public static class TwoSubscribesRaceStressTest extends BaseStressTest { + + final StressSubscriber stressSubscriber1 = new StressSubscriber<>(); + + @Override + SlowFireAndForgetRequesterMono source() { + return new SlowFireAndForgetRequesterMono( + UnpooledByteBufPayload.create( + "test-data", "test-metadata", this.requesterResponderSupport.getAllocator()), + this.requesterResponderSupport); + } + + @Override + int maximumAllowedAwaitingPermitHandlersNumber() { + return 0; + } + + // init + { + final ByteBuf leaseFrame = + LeaseFrameCodec.encode(this.testDuplexConnection.alloc(), 1000, 1, null); + this.requesterLeaseTracker.handleLeaseFrame(leaseFrame); + leaseFrame.release(); + } + + @Actor + public void subscribe1() { + this.source.subscribe(this.stressSubscriber); + } + + @Actor + public void subscribe2() { + this.source.subscribe(this.stressSubscriber1); + } + + @Arbiter + public void arbiter(LLLLL_Result r) { + r.r1 = this.source.state; + r.r2 = + this.stressSubscriber.onCompleteCalls + + this.stressSubscriber.onErrorCalls * 2 + + this.stressSubscriber1.onCompleteCalls + + this.stressSubscriber1.onErrorCalls * 2; + r.r3 = this.outboundSubscriber.onNextCalls; + r.r4 = this.requesterLeaseTracker.availableRequests; + r.r5 = this.source.payload.refCnt(); + + this.outboundSubscriber.values.forEach(ByteBuf::release); + } + } + + @JCStressTest + @Outcome( + id = {"-9223372036854775808, 1, 1, 0, 0"}, + expect = ACCEPTABLE, + desc = "frame delivered before cancellation") + @Outcome( + id = {"-9223372036854775808, 0, 0, 1, 0"}, + expect = ACCEPTABLE, + desc = "cancellation happened first") + @Outcome( + id = {"-9223372036854775808, 0, 0, 0, 0"}, + expect = ACCEPTABLE, + desc = "cancellation happened in between") + @State + public static class SubscribeAndCancelRaceStressTest extends BaseStressTest { + + @Override + SlowFireAndForgetRequesterMono source() { + return new SlowFireAndForgetRequesterMono( + UnpooledByteBufPayload.create( + "test-data", "test-metadata", this.requesterResponderSupport.getAllocator()), + this.requesterResponderSupport); + } + + @Override + int maximumAllowedAwaitingPermitHandlersNumber() { + return 0; + } + + // init + { + final ByteBuf leaseFrame = + LeaseFrameCodec.encode(this.testDuplexConnection.alloc(), 1000, 1, null); + this.requesterLeaseTracker.handleLeaseFrame(leaseFrame); + leaseFrame.release(); + } + + @Actor + public void subscribe() { + this.source.subscribe(this.stressSubscriber); + } + + @Actor + public void cancel() { + this.stressSubscriber.cancel(); + } + + @Arbiter + public void arbiter(LLLLL_Result r) { + r.r1 = this.source.state; + r.r2 = this.stressSubscriber.onCompleteCalls + this.stressSubscriber.onErrorCalls * 2; + r.r3 = this.outboundSubscriber.onNextCalls; + r.r4 = this.requesterLeaseTracker.availableRequests; + r.r5 = this.source.payload.refCnt(); + + this.outboundSubscriber.values.forEach(ByteBuf::release); + } + } + + @JCStressTest + @Outcome( + id = {"-9223372036854775808, 1, 1, 0, 0"}, + expect = ACCEPTABLE, + desc = "frame delivered before cancellation") + @Outcome( + id = {"-9223372036854775808, 0, 0, 0, 0"}, + expect = ACCEPTABLE, + desc = "cancellation happened in between") + @Outcome( + id = {"-9223372036854775808, 0, 0, 1, 0"}, + expect = ACCEPTABLE, + desc = "cancellation happened first") + @State + public static class SubscribeAndCancelWithDeferredLeaseRaceStressTest extends BaseStressTest { + + final ByteBuf leaseFrame = + LeaseFrameCodec.encode(this.testDuplexConnection.alloc(), 1000, 1, null); + + @Override + SlowFireAndForgetRequesterMono source() { + return new SlowFireAndForgetRequesterMono( + UnpooledByteBufPayload.create( + "test-data", "test-metadata", this.requesterResponderSupport.getAllocator()), + this.requesterResponderSupport); + } + + @Override + int maximumAllowedAwaitingPermitHandlersNumber() { + return 1; + } + + @Actor + public void issueLease() { + final ByteBuf leaseFrame = this.leaseFrame; + this.requesterLeaseTracker.handleLeaseFrame(leaseFrame); + leaseFrame.release(); + } + + @Actor + public void subscribe() { + this.source.subscribe(this.stressSubscriber); + } + + @Actor + public void cancel() { + this.stressSubscriber.cancel(); + } + + @Arbiter + public void arbiter(LLLLL_Result r) { + r.r1 = this.source.state; + r.r2 = this.stressSubscriber.onCompleteCalls + this.stressSubscriber.onErrorCalls * 2; + r.r3 = this.outboundSubscriber.onNextCalls; + r.r4 = this.requesterLeaseTracker.availableRequests; + r.r5 = this.source.payload.refCnt(); + + this.outboundSubscriber.values.forEach(ByteBuf::release); + } + } + + @JCStressTest + @Outcome( + id = {"-9223372036854775808, 1, 1, 0, 0"}, + expect = ACCEPTABLE, + desc = "frame delivered before cancellation") + @Outcome( + id = {"-9223372036854775808, 2, 0, 1, 0"}, + expect = ACCEPTABLE, + desc = "no lease error delivered before cancellation") + @Outcome( + id = {"-9223372036854775808, 0, 0, 1, 0"}, + expect = ACCEPTABLE, + desc = "cancellation happened first") + @Outcome( + id = {"-9223372036854775808, 0, 0, 0, 0"}, + expect = ACCEPTABLE, + desc = "cancellation happened in between") + @Outcome( + id = {"-9223372036854775808, 3, 0, 1, 0"}, + expect = ACCEPTABLE, + desc = + "cancellation happened after lease permit requested but before it was actually decided and in the case when no lease are available. Error is dropped") + @State + public static class SubscribeAndCancelWithDeferredLease2RaceStressTest extends BaseStressTest { + + final ByteBuf leaseFrame = + LeaseFrameCodec.encode(this.testDuplexConnection.alloc(), 1000, 1, null); + + @Override + SlowFireAndForgetRequesterMono source() { + return new SlowFireAndForgetRequesterMono( + UnpooledByteBufPayload.create( + "test-data", "test-metadata", this.requesterResponderSupport.getAllocator()), + this.requesterResponderSupport); + } + + @Override + int maximumAllowedAwaitingPermitHandlersNumber() { + return 0; + } + + @Actor + public void issueLease() { + final ByteBuf leaseFrame = this.leaseFrame; + this.requesterLeaseTracker.handleLeaseFrame(leaseFrame); + leaseFrame.release(); + } + + @Actor + public void subscribe() { + this.source.subscribe(this.stressSubscriber); + } + + @Actor + public void cancel() { + this.stressSubscriber.cancel(); + } + + @Arbiter + public void arbiter(LLLLL_Result r) { + r.r1 = this.source.state; + r.r2 = + this.stressSubscriber.onCompleteCalls + + this.stressSubscriber.onErrorCalls * 2 + + this.stressSubscriber.droppedErrors.size() * 3; + r.r3 = this.outboundSubscriber.onNextCalls; + r.r4 = this.requesterLeaseTracker.availableRequests; + r.r5 = this.source.payload.refCnt(); + + this.outboundSubscriber.values.forEach(ByteBuf::release); + } + } +} diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscription.java b/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscription.java index 583ba7ad2..3b51b8ef6 100644 --- a/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscription.java +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscription.java @@ -39,7 +39,7 @@ public class StressSubscription implements Subscription { public volatile int requestsCount; - @SuppressWarnings("rawtypes") + @SuppressWarnings("rawtype s") static final AtomicIntegerFieldUpdater REQUESTS_COUNT = AtomicIntegerFieldUpdater.newUpdater(StressSubscription.class, "requestsCount"); diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/TestRequesterResponderSupport.java b/rsocket-core/src/jcstress/java/io/rsocket/core/TestRequesterResponderSupport.java new file mode 100644 index 000000000..420da66ba --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/TestRequesterResponderSupport.java @@ -0,0 +1,39 @@ +package io.rsocket.core; + +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + +import io.rsocket.DuplexConnection; +import io.rsocket.RSocket; +import io.rsocket.frame.decoder.PayloadDecoder; +import reactor.util.annotation.Nullable; + +public class TestRequesterResponderSupport extends RequesterResponderSupport implements RSocket { + + @Nullable private final RequesterLeaseTracker requesterLeaseTracker; + + public TestRequesterResponderSupport( + DuplexConnection connection, StreamIdSupplier streamIdSupplier) { + this(connection, streamIdSupplier, null); + } + + public TestRequesterResponderSupport( + DuplexConnection connection, + StreamIdSupplier streamIdSupplier, + @Nullable RequesterLeaseTracker requesterLeaseTracker) { + super( + 0, + FRAME_LENGTH_MASK, + Integer.MAX_VALUE, + PayloadDecoder.ZERO_COPY, + connection, + streamIdSupplier, + __ -> null); + this.requesterLeaseTracker = requesterLeaseTracker; + } + + @Override + @Nullable + public RequesterLeaseTracker getRequesterLeaseTracker() { + return this.requesterLeaseTracker; + } +} diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/UnpooledByteBufPayload.java b/rsocket-core/src/jcstress/java/io/rsocket/core/UnpooledByteBufPayload.java new file mode 100644 index 000000000..22c478979 --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/UnpooledByteBufPayload.java @@ -0,0 +1,155 @@ +package io.rsocket.core; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.AbstractReferenceCounted; +import io.netty.util.IllegalReferenceCountException; +import io.rsocket.Payload; +import reactor.util.annotation.Nullable; + +public class UnpooledByteBufPayload extends AbstractReferenceCounted implements Payload { + + private final ByteBuf data; + private final ByteBuf metadata; + + /** + * Static factory method for a text payload. Mainly looks better than "new ByteBufPayload(data)" + * + * @param data the data of the payload. + * @return a payload. + */ + public static Payload create(String data) { + return create(data, ByteBufAllocator.DEFAULT); + } + + /** + * Static factory method for a text payload. Mainly looks better than "new ByteBufPayload(data)" + * + * @param data the data of the payload. + * @return a payload. + */ + public static Payload create(String data, ByteBufAllocator allocator) { + return new UnpooledByteBufPayload(ByteBufUtil.writeUtf8(allocator, data), null); + } + + /** + * Static factory method for a text payload. Mainly looks better than "new ByteBufPayload(data, + * metadata)" + * + * @param data the data of the payload. + * @param metadata the metadata for the payload. + * @return a payload. + */ + public static Payload create(String data, @Nullable String metadata) { + return create(data, metadata, ByteBufAllocator.DEFAULT); + } + + /** + * Static factory method for a text payload. Mainly looks better than "new ByteBufPayload(data, + * metadata)" + * + * @param data the data of the payload. + * @param metadata the metadata for the payload. + * @return a payload. + */ + public static Payload create(String data, @Nullable String metadata, ByteBufAllocator allocator) { + return new UnpooledByteBufPayload( + ByteBufUtil.writeUtf8(allocator, data), + metadata == null ? null : ByteBufUtil.writeUtf8(allocator, metadata)); + } + + public UnpooledByteBufPayload(ByteBuf data, @Nullable ByteBuf metadata) { + this.data = data; + this.metadata = metadata; + } + + @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(); + } + + @Override + public UnpooledByteBufPayload retain() { + super.retain(); + return this; + } + + @Override + public UnpooledByteBufPayload retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public UnpooledByteBufPayload touch() { + ensureAccessible(); + data.touch(); + if (metadata != null) { + metadata.touch(); + } + return this; + } + + @Override + public UnpooledByteBufPayload touch(Object hint) { + ensureAccessible(); + data.touch(hint); + if (metadata != null) { + metadata.touch(hint); + } + return this; + } + + @Override + protected void deallocate() { + data.release(); + if (metadata != null) { + metadata.release(); + } + } + + /** + * 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 UnpooledByteBufPayload#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/main/java/io/rsocket/core/RequesterLeaseTracker.java b/rsocket-core/src/main/java/io/rsocket/core/RequesterLeaseTracker.java index 6e7a822f1..50da83b8f 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequesterLeaseTracker.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequesterLeaseTracker.java @@ -55,8 +55,9 @@ synchronized void issue(LeasePermitHandler leasePermitHandler) { final boolean isExpired = leaseReceived && isExpired(l); if (leaseReceived && availableRequests > 0 && !isExpired) { - leasePermitHandler.handlePermit(); - this.availableRequests = availableRequests - 1; + if (leasePermitHandler.handlePermit()) { + this.availableRequests = availableRequests - 1; + } } else { final Queue queue = this.awaitingPermitHandlersQueue; if (this.maximumAllowedAwaitingPermitHandlersNumber > queue.size()) { diff --git a/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java b/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java index f98a5570e..139ae146b 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java +++ b/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java @@ -18,7 +18,7 @@ * Additional Utils which allows to decorate a ByteBufAllocator and track/assertOnLeaks all created * ByteBuffs */ -class LeaksTrackingByteBufAllocator implements ByteBufAllocator { +public class LeaksTrackingByteBufAllocator implements ByteBufAllocator { /** * Allows to instrument any given the instance of ByteBufAllocator diff --git a/rsocket-test/src/main/java/io/rsocket/test/TestDuplexConnection.java b/rsocket-test/src/main/java/io/rsocket/test/TestDuplexConnection.java new file mode 100644 index 000000000..57a00e229 --- /dev/null +++ b/rsocket-test/src/main/java/io/rsocket/test/TestDuplexConnection.java @@ -0,0 +1,166 @@ +package io.rsocket.test; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.RSocketErrorException; +import io.rsocket.frame.PayloadFrameCodec; +import java.net.SocketAddress; +import java.util.function.BiFunction; +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.Mono; +import reactor.core.publisher.Operators; +import reactor.core.publisher.Sinks; +import reactor.util.annotation.Nullable; + +public class TestDuplexConnection implements DuplexConnection { + + final ByteBufAllocator allocator; + final Sinks.Many inbound = Sinks.unsafe().many().unicast().onBackpressureError(); + final Sinks.Many outbound = Sinks.unsafe().many().unicast().onBackpressureError(); + final Sinks.One close = Sinks.one(); + + public TestDuplexConnection( + CoreSubscriber outboundSubscriber, boolean trackLeaks) { + this.outbound.asFlux().subscribe(outboundSubscriber); + this.allocator = + trackLeaks + ? LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT) + : ByteBufAllocator.DEFAULT; + } + + @Override + public void dispose() { + this.inbound.tryEmitComplete(); + this.outbound.tryEmitComplete(); + this.close.tryEmitEmpty(); + } + + @Override + public Mono onClose() { + return this.close.asMono(); + } + + @Override + public void sendErrorAndClose(RSocketErrorException errorException) {} + + @Override + public Flux receive() { + return this.inbound + .asFlux() + .transform( + Operators.lift( + (BiFunction< + Scannable, + CoreSubscriber, + CoreSubscriber>) + ByteBufReleaserOperator::create)); + } + + @Override + public ByteBufAllocator alloc() { + return this.allocator; + } + + @Override + public SocketAddress remoteAddress() { + return new SocketAddress() { + @Override + public String toString() { + return "Test"; + } + }; + } + + @Override + public void sendFrame(int streamId, ByteBuf frame) { + this.outbound.tryEmitNext(frame); + } + + public void sendPayloadFrame( + int streamId, ByteBuf data, @Nullable ByteBuf metadata, boolean complete) { + sendFrame( + streamId, + PayloadFrameCodec.encode(this.allocator, streamId, false, complete, true, metadata, data)); + } + + static class ByteBufReleaserOperator + implements CoreSubscriber, Subscription, Fuseable.QueueSubscription { + + static CoreSubscriber create( + Scannable scannable, CoreSubscriber actual) { + return new ByteBufReleaserOperator(actual); + } + + final CoreSubscriber actual; + + Subscription s; + + public ByteBufReleaserOperator(CoreSubscriber actual) { + this.actual = actual; + } + + @Override + public void onSubscribe(Subscription s) { + if (Operators.validate(this.s, s)) { + this.s = s; + this.actual.onSubscribe(this); + } + } + + @Override + public void onNext(ByteBuf buf) { + this.actual.onNext(buf); + buf.release(); + } + + @Override + public void onError(Throwable t) { + actual.onError(t); + } + + @Override + public void onComplete() { + actual.onComplete(); + } + + @Override + public void request(long n) { + s.request(n); + } + + @Override + public void cancel() { + s.cancel(); + } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public ByteBuf poll() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public int size() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + } +} From de60762f68fee6fa277614c8f89485c03035310b Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 17 May 2021 01:45:57 +0300 Subject: [PATCH 048/116] adds RequestResponseMono's stress tests Signed-off-by: Oleh Dokuka --- ...equestResponseRequesterMonoStressTest.java | 650 ++++++++++++++++++ 1 file changed, 650 insertions(+) create mode 100644 rsocket-core/src/jcstress/java/io/rsocket/core/RequestResponseRequesterMonoStressTest.java diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/RequestResponseRequesterMonoStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/core/RequestResponseRequesterMonoStressTest.java new file mode 100644 index 000000000..1dde77b34 --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/RequestResponseRequesterMonoStressTest.java @@ -0,0 +1,650 @@ +package io.rsocket.core; + +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.rsocket.Payload; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.LeaseFrameCodec; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.test.TestDuplexConnection; +import java.util.stream.IntStream; +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.LLLLLL_Result; +import org.openjdk.jcstress.infra.results.LLLLL_Result; +import org.openjdk.jcstress.infra.results.LLLL_Result; + +public abstract class RequestResponseRequesterMonoStressTest { + + abstract static class BaseStressTest { + + final StressSubscriber outboundSubscriber = new StressSubscriber<>(); + + final StressSubscriber stressSubscriber = new StressSubscriber<>(initialRequest()); + + final TestDuplexConnection testDuplexConnection = + new TestDuplexConnection(this.outboundSubscriber, false); + + final RequesterLeaseTracker requesterLeaseTracker; + + final TestRequesterResponderSupport requesterResponderSupport; + + final RequestResponseRequesterMono source; + + BaseStressTest(RequesterLeaseTracker requesterLeaseTracker) { + this.requesterLeaseTracker = requesterLeaseTracker; + this.requesterResponderSupport = + new TestRequesterResponderSupport( + testDuplexConnection, StreamIdSupplier.clientSupplier(), requesterLeaseTracker); + this.source = source(); + } + + abstract RequestResponseRequesterMono source(); + + abstract long initialRequest(); + } + + abstract static class BaseStressTestWithLease extends BaseStressTest { + + BaseStressTestWithLease(int maximumAllowedAwaitingPermitHandlersNumber) { + super(new RequesterLeaseTracker("test", maximumAllowedAwaitingPermitHandlersNumber)); + } + } + + @JCStressTest + @Outcome( + id = {"-9223372036854775808, 3, 1, 0, 0"}, + expect = ACCEPTABLE) + @State + public static class TwoSubscribesRaceStressTest extends BaseStressTestWithLease { + + final StressSubscriber stressSubscriber1 = new StressSubscriber<>(); + + public TwoSubscribesRaceStressTest() { + super(0); + } + + @Override + RequestResponseRequesterMono source() { + return new RequestResponseRequesterMono( + UnpooledByteBufPayload.create( + "test-data", "test-metadata", this.requesterResponderSupport.getAllocator()), + this.requesterResponderSupport); + } + + @Override + long initialRequest() { + return Long.MAX_VALUE; + } + + // init + { + final ByteBuf leaseFrame = + LeaseFrameCodec.encode(this.testDuplexConnection.alloc(), 1000, 1, null); + this.requesterLeaseTracker.handleLeaseFrame(leaseFrame); + leaseFrame.release(); + } + + @Actor + public void subscribe1() { + this.source.subscribe(this.stressSubscriber); + } + + @Actor + public void subscribe2() { + this.source.subscribe(this.stressSubscriber1); + } + + @Arbiter + public void arbiter(LLLLL_Result r) { + final ByteBuf nextFrame = + PayloadFrameCodec.encode( + this.testDuplexConnection.alloc(), + 1, + false, + true, + true, + null, + ByteBufUtil.writeUtf8(this.testDuplexConnection.alloc(), "response-data")); + this.source.handleNext(nextFrame, false, true); + nextFrame.release(); + + r.r1 = this.source.state; + r.r2 = + this.stressSubscriber.onCompleteCalls + + this.stressSubscriber.onErrorCalls * 2 + + this.stressSubscriber1.onCompleteCalls + + this.stressSubscriber1.onErrorCalls * 2; + r.r3 = this.outboundSubscriber.onNextCalls; + r.r4 = this.requesterLeaseTracker.availableRequests; + + this.outboundSubscriber.values.forEach(ByteBuf::release); + this.stressSubscriber.values.forEach(Payload::release); + this.stressSubscriber1.values.forEach(Payload::release); + + r.r5 = this.source.payload.refCnt() + nextFrame.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = {"-9223372036854775808, 0, 2, 0, 0, " + (0x04 + 2 * 0x09)}, + expect = ACCEPTABLE, + desc = "frame delivered before cancellation") + @Outcome( + id = {"-9223372036854775808, 0, 0, 1, 0, 0"}, + expect = ACCEPTABLE, + desc = "cancellation happened first") + @State + public static class SubscribeAndRequestAndCancelRaceStressTest extends BaseStressTestWithLease { + + public SubscribeAndRequestAndCancelRaceStressTest() { + super(0); + } + + @Override + RequestResponseRequesterMono source() { + return new RequestResponseRequesterMono( + UnpooledByteBufPayload.create( + "test-data", "test-metadata", this.requesterResponderSupport.getAllocator()), + this.requesterResponderSupport); + } + + @Override + long initialRequest() { + return 0; + } + + // init + { + final ByteBuf leaseFrame = + LeaseFrameCodec.encode(this.testDuplexConnection.alloc(), 1000, 1, null); + this.requesterLeaseTracker.handleLeaseFrame(leaseFrame); + leaseFrame.release(); + } + + @Actor + public void subscribe() { + this.source.subscribe(this.stressSubscriber); + } + + @Actor + public void cancel() { + this.stressSubscriber.cancel(); + } + + @Actor + public void request() { + this.stressSubscriber.request(1); + this.stressSubscriber.request(Long.MAX_VALUE); + this.stressSubscriber.request(1); + } + + @Arbiter + public void arbiter(LLLLLL_Result r) { + r.r1 = this.source.state; + r.r2 = this.stressSubscriber.onCompleteCalls + this.stressSubscriber.onErrorCalls * 2; + r.r3 = this.outboundSubscriber.onNextCalls; + r.r4 = this.requesterLeaseTracker.availableRequests; + r.r5 = this.source.payload.refCnt(); + + r.r6 = + IntStream.range(0, this.outboundSubscriber.values.size()) + .map( + i -> + FrameHeaderCodec.frameType(this.outboundSubscriber.values.get(i)) + .getEncodedType() + * (i + 1)) + .sum(); + + this.outboundSubscriber.values.forEach(ByteBuf::release); + } + } + + @JCStressTest + @Outcome( + id = {"-9223372036854775808, 0, 2, 0, 0, " + (0x04 + 2 * 0x09)}, + expect = ACCEPTABLE, + desc = "frame delivered before cancellation") + @Outcome( + id = {"-9223372036854775808, 0, 0, 1, 0, 0"}, + expect = ACCEPTABLE, + desc = "cancellation happened first") + @State + public static class SubscribeAndRequestAndCancelWithDeferredLeaseRaceStressTest + extends BaseStressTestWithLease { + + final ByteBuf leaseFrame = + LeaseFrameCodec.encode(this.testDuplexConnection.alloc(), 1000, 1, null); + + public SubscribeAndRequestAndCancelWithDeferredLeaseRaceStressTest() { + super(1); + } + + @Override + RequestResponseRequesterMono source() { + return new RequestResponseRequesterMono( + UnpooledByteBufPayload.create( + "test-data", "test-metadata", this.requesterResponderSupport.getAllocator()), + this.requesterResponderSupport); + } + + @Override + long initialRequest() { + return 0; + } + + @Actor + public void issueLease() { + final ByteBuf leaseFrame = this.leaseFrame; + this.requesterLeaseTracker.handleLeaseFrame(leaseFrame); + leaseFrame.release(); + } + + @Actor + public void subscribe() { + this.source.subscribe(this.stressSubscriber); + } + + @Actor + public void cancel() { + this.stressSubscriber.cancel(); + } + + @Actor + public void request() { + this.stressSubscriber.request(1); + this.stressSubscriber.request(Long.MAX_VALUE); + this.stressSubscriber.request(1); + } + + @Arbiter + public void arbiter(LLLLLL_Result r) { + r.r1 = this.source.state; + r.r2 = this.stressSubscriber.onCompleteCalls + this.stressSubscriber.onErrorCalls * 2; + r.r3 = this.outboundSubscriber.onNextCalls; + r.r4 = this.requesterLeaseTracker.availableRequests; + r.r5 = this.source.payload.refCnt(); + r.r6 = + IntStream.range(0, this.outboundSubscriber.values.size()) + .map( + i -> + FrameHeaderCodec.frameType(this.outboundSubscriber.values.get(i)) + .getEncodedType() + * (i + 1)) + .sum(); + + this.outboundSubscriber.values.forEach(ByteBuf::release); + } + } + + @JCStressTest + @Outcome( + id = {"-9223372036854775808, 0, 2, 0, 0, " + (0x04 + 2 * 0x09)}, + expect = ACCEPTABLE, + desc = "frame delivered before cancellation") + @Outcome( + id = {"-9223372036854775808, 2, 0, 1, 0, 0"}, + expect = ACCEPTABLE, + desc = "NoLeaseError delivered before cancellation") + @Outcome( + id = {"-9223372036854775808, 0, 0, 1, 0, 0"}, + expect = ACCEPTABLE, + desc = "cancellation happened first or in between") + @Outcome( + id = {"-9223372036854775808, 3, 0, 1, 0, 0"}, + expect = ACCEPTABLE, + desc = + "cancellation happened after lease permit requested but before it was actually decided and in the case when no lease are available. Error is dropped") + @State + public static class SubscribeAndRequestAndCancelWithDeferredLease2RaceStressTest + extends BaseStressTestWithLease { + + final ByteBuf leaseFrame = + LeaseFrameCodec.encode(this.testDuplexConnection.alloc(), 1000, 1, null); + + SubscribeAndRequestAndCancelWithDeferredLease2RaceStressTest() { + super(0); + } + + @Override + RequestResponseRequesterMono source() { + return new RequestResponseRequesterMono( + UnpooledByteBufPayload.create( + "test-data", "test-metadata", this.requesterResponderSupport.getAllocator()), + this.requesterResponderSupport); + } + + @Override + long initialRequest() { + return 0; + } + + @Actor + public void issueLease() { + final ByteBuf leaseFrame = this.leaseFrame; + this.requesterLeaseTracker.handleLeaseFrame(leaseFrame); + leaseFrame.release(); + } + + @Actor + public void subscribe() { + this.source.subscribe(this.stressSubscriber); + } + + @Actor + public void cancel() { + this.stressSubscriber.cancel(); + } + + @Actor + public void request() { + this.stressSubscriber.request(1); + this.stressSubscriber.request(Long.MAX_VALUE); + this.stressSubscriber.request(1); + } + + @Arbiter + public void arbiter(LLLLLL_Result r) { + r.r1 = this.source.state; + r.r2 = + this.stressSubscriber.onCompleteCalls + + this.stressSubscriber.onErrorCalls * 2 + + this.stressSubscriber.droppedErrors.size() * 3; + r.r3 = this.outboundSubscriber.onNextCalls; + r.r4 = this.requesterLeaseTracker.availableRequests; + r.r5 = this.source.payload.refCnt(); + r.r6 = + IntStream.range(0, this.outboundSubscriber.values.size()) + .map( + i -> + FrameHeaderCodec.frameType(this.outboundSubscriber.values.get(i)) + .getEncodedType() + * (i + 1)) + .sum(); + + this.outboundSubscriber.values.forEach(ByteBuf::release); + } + } + + @JCStressTest + @Outcome( + id = {"-9223372036854775808, 0, 2, 0, " + (0x04 + 2 * 0x09)}, + expect = ACCEPTABLE, + desc = "frame delivered before cancellation") + @Outcome( + id = {"-9223372036854775808, 0, 0, 0, 0"}, + expect = ACCEPTABLE, + desc = "cancellation happened first") + @State + public static class SubscribeAndRequestAndCancel extends BaseStressTest { + + SubscribeAndRequestAndCancel() { + super(null); + } + + @Override + RequestResponseRequesterMono source() { + return new RequestResponseRequesterMono( + UnpooledByteBufPayload.create( + "test-data", "test-metadata", this.requesterResponderSupport.getAllocator()), + this.requesterResponderSupport); + } + + @Override + long initialRequest() { + return 0; + } + + @Actor + public void subscribe() { + this.source.subscribe(this.stressSubscriber); + } + + @Actor + public void cancel() { + this.stressSubscriber.cancel(); + } + + @Actor + public void request() { + this.stressSubscriber.request(1); + this.stressSubscriber.request(Long.MAX_VALUE); + this.stressSubscriber.request(1); + } + + @Arbiter + public void arbiter(LLLLL_Result r) { + r.r1 = this.source.state; + r.r2 = + this.stressSubscriber.onCompleteCalls + + this.stressSubscriber.onErrorCalls * 2 + + this.stressSubscriber.droppedErrors.size() * 3; + r.r3 = this.outboundSubscriber.onNextCalls; + r.r4 = this.source.payload.refCnt(); + r.r5 = + IntStream.range(0, this.outboundSubscriber.values.size()) + .map( + i -> + FrameHeaderCodec.frameType(this.outboundSubscriber.values.get(i)) + .getEncodedType() + * (i + 1)) + .sum(); + + this.outboundSubscriber.values.forEach(ByteBuf::release); + } + } + + @JCStressTest + @Outcome( + id = {"-9223372036854775808, 1, 1, 0"}, + expect = ACCEPTABLE, + desc = "frame delivered before cancellation") + @Outcome( + id = {"-9223372036854775808, 0, 0, 0"}, + expect = ACCEPTABLE, + desc = "cancellation happened first or in between") + @State + public static class CancelWithInboundNextRaceStressTest extends BaseStressTestWithLease { + + final ByteBuf nextFrame = + PayloadFrameCodec.encode( + this.testDuplexConnection.alloc(), + 1, + false, + true, + true, + null, + ByteBufUtil.writeUtf8(this.testDuplexConnection.alloc(), "response-data")); + + CancelWithInboundNextRaceStressTest() { + super(0); + } + + @Override + RequestResponseRequesterMono source() { + return new RequestResponseRequesterMono( + UnpooledByteBufPayload.create( + "test-data", "test-metadata", this.requesterResponderSupport.getAllocator()), + this.requesterResponderSupport); + } + + // init + { + final ByteBuf leaseFrame = + LeaseFrameCodec.encode(this.testDuplexConnection.alloc(), 1000, 1, null); + this.requesterLeaseTracker.handleLeaseFrame(leaseFrame); + leaseFrame.release(); + + this.source.subscribe(this.stressSubscriber); + } + + @Override + long initialRequest() { + return 1; + } + + @Actor + public void inboundNext() { + this.source.handleNext(this.nextFrame, false, true); + this.nextFrame.release(); + } + + @Actor + public void cancel() { + this.stressSubscriber.cancel(); + } + + @Arbiter + public void arbiter(LLLL_Result r) { + r.r1 = this.source.state; + r.r2 = + this.stressSubscriber.onCompleteCalls + + this.stressSubscriber.onErrorCalls * 2 + + this.stressSubscriber.droppedErrors.size() * 3; + r.r3 = this.stressSubscriber.onNextCalls; + + this.outboundSubscriber.values.forEach(ByteBuf::release); + this.stressSubscriber.values.forEach(Payload::release); + + r.r4 = this.source.payload.refCnt() + this.nextFrame.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = {"-9223372036854775808, 1, 0, 0"}, + expect = ACCEPTABLE, + desc = "frame delivered before cancellation") + @Outcome( + id = {"-9223372036854775808, 0, 0, 0"}, + expect = ACCEPTABLE, + desc = "cancellation happened first or in between") + @State + public static class CancelWithInboundCompleteRaceStressTest extends BaseStressTestWithLease { + + CancelWithInboundCompleteRaceStressTest() { + super(0); + } + + @Override + RequestResponseRequesterMono source() { + return new RequestResponseRequesterMono( + UnpooledByteBufPayload.create( + "test-data", "test-metadata", this.requesterResponderSupport.getAllocator()), + this.requesterResponderSupport); + } + + // init + { + final ByteBuf leaseFrame = + LeaseFrameCodec.encode(this.testDuplexConnection.alloc(), 1000, 1, null); + this.requesterLeaseTracker.handleLeaseFrame(leaseFrame); + leaseFrame.release(); + + this.source.subscribe(this.stressSubscriber); + } + + @Override + long initialRequest() { + return 1; + } + + @Actor + public void inboundComplete() { + this.source.handleComplete(); + } + + @Actor + public void cancel() { + this.stressSubscriber.cancel(); + } + + @Arbiter + public void arbiter(LLLL_Result r) { + r.r1 = this.source.state; + r.r2 = + this.stressSubscriber.onCompleteCalls + + this.stressSubscriber.onErrorCalls * 2 + + this.stressSubscriber.droppedErrors.size() * 3; + r.r3 = this.stressSubscriber.onNextCalls; + + this.outboundSubscriber.values.forEach(ByteBuf::release); + this.stressSubscriber.values.forEach(Payload::release); + + r.r4 = this.source.payload.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = {"-9223372036854775808, 2, 0, 0"}, + expect = ACCEPTABLE, + desc = "frame delivered before cancellation") + @Outcome( + id = {"-9223372036854775808, 3, 0, 0"}, + expect = ACCEPTABLE, + desc = "cancellation happened first. inbound error dropped") + @State + public static class CancelWithInboundErrorRaceStressTest extends BaseStressTestWithLease { + + static final RuntimeException ERROR = new RuntimeException("Test"); + + CancelWithInboundErrorRaceStressTest() { + super(0); + } + + @Override + RequestResponseRequesterMono source() { + return new RequestResponseRequesterMono( + UnpooledByteBufPayload.create( + "test-data", "test-metadata", this.requesterResponderSupport.getAllocator()), + this.requesterResponderSupport); + } + + // init + { + final ByteBuf leaseFrame = + LeaseFrameCodec.encode(this.testDuplexConnection.alloc(), 1000, 1, null); + this.requesterLeaseTracker.handleLeaseFrame(leaseFrame); + leaseFrame.release(); + + this.source.subscribe(this.stressSubscriber); + } + + @Override + long initialRequest() { + return 1; + } + + @Actor + public void inboundError() { + this.source.handleError(ERROR); + } + + @Actor + public void cancel() { + this.stressSubscriber.cancel(); + } + + @Arbiter + public void arbiter(LLLL_Result r) { + r.r1 = this.source.state; + r.r2 = + this.stressSubscriber.onCompleteCalls + + this.stressSubscriber.onErrorCalls * 2 + + this.stressSubscriber.droppedErrors.size() * 3; + r.r3 = this.stressSubscriber.onNextCalls; + + this.outboundSubscriber.values.forEach(ByteBuf::release); + this.stressSubscriber.values.forEach(Payload::release); + + r.r4 = this.source.payload.refCnt(); + } + } +} From f521a6ac9fd4310b017f8fd368ecf9d1f2ee68a7 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 7 Jun 2021 14:30:30 +0300 Subject: [PATCH 049/116] polishes logging and refactor Client/ServerRSocketSession. adds tests. Improves KeepAliveSupport Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/core/ServerSetup.java | 4 +- .../rsocket/keepalive/KeepAliveSupport.java | 69 ++- .../rsocket/resume/ClientRSocketSession.java | 173 ++++--- .../resume/ResumableDuplexConnection.java | 36 +- .../rsocket/resume/ServerRSocketSession.java | 133 ++++-- .../resume/ClientRSocketSessionTest.java | 446 ++++++++++++++++++ .../rsocket/resume/ResumeCalculatorTest.java | 57 --- .../resume/ServerRSocketSessionTest.java | 182 +++++++ 8 files changed, 900 insertions(+), 200 deletions(-) create mode 100644 rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java 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 0b23bcde5..e716b8fcb 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java @@ -114,10 +114,10 @@ public Mono acceptRSocketSetup( final ServerRSocketSession serverRSocketSession = new ServerRSocketSession( resumeToken, - duplexConnection, resumableDuplexConnection, - resumeSessionDuration, + duplexConnection, resumableFramesStore, + resumeSessionDuration, cleanupStoreOnKeepAlive); sessionManager.save(serverRSocketSession, resumeToken); 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 6a3ab40d3..4fd18d041 100644 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java @@ -23,7 +23,7 @@ import io.rsocket.resume.ResumeStateHolder; import java.time.Duration; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.function.Consumer; import reactor.core.Disposable; import reactor.core.publisher.Flux; @@ -38,11 +38,19 @@ public abstract class KeepAliveSupport implements KeepAliveFramesAcceptor { final Duration keepAliveTimeout; final long keepAliveTimeoutMillis; - final AtomicBoolean started = new AtomicBoolean(); + volatile int state; + static final AtomicIntegerFieldUpdater STATE = + AtomicIntegerFieldUpdater.newUpdater(KeepAliveSupport.class, "state"); + + static final int STOPPED_STATE = 0; + static final int STARTING_STATE = 1; + static final int STARTED_STATE = 2; + static final int DISPOSED_STATE = -1; volatile Consumer onTimeout; volatile Consumer onFrameSent; - volatile Disposable ticksDisposable; + + Disposable ticksDisposable; volatile ResumeStateHolder resumeStateHolder; volatile long lastReceivedMillis; @@ -57,25 +65,30 @@ private KeepAliveSupport( } public KeepAliveSupport start() { - this.lastReceivedMillis = scheduler.now(TimeUnit.MILLISECONDS); - if (started.compareAndSet(false, true)) { - ticksDisposable = + if (this.state == STOPPED_STATE && STATE.compareAndSet(this, STOPPED_STATE, STARTING_STATE)) { + this.lastReceivedMillis = scheduler.now(TimeUnit.MILLISECONDS); + + final Disposable disposable = Flux.interval(keepAliveInterval, scheduler).subscribe(v -> onIntervalTick()); + this.ticksDisposable = disposable; + + if (this.state != STARTING_STATE + || !STATE.compareAndSet(this, STARTING_STATE, STARTED_STATE)) { + disposable.dispose(); + } } return this; } public void stop() { - if (started.compareAndSet(true, false)) { - ticksDisposable.dispose(); - } + terminate(STOPPED_STATE); } @Override public void receive(ByteBuf keepAliveFrame) { this.lastReceivedMillis = scheduler.now(TimeUnit.MILLISECONDS); if (resumeStateHolder != null) { - long remoteLastReceivedPos = remoteLastReceivedPosition(keepAliveFrame); + final long remoteLastReceivedPos = KeepAliveFrameCodec.lastPosition(keepAliveFrame); resumeStateHolder.onImpliedPosition(remoteLastReceivedPos); } if (KeepAliveFrameCodec.respondFlag(keepAliveFrame)) { @@ -104,6 +117,16 @@ public KeepAliveSupport onTimeout(Consumer onTimeout) { return this; } + @Override + public void dispose() { + terminate(DISPOSED_STATE); + } + + @Override + public boolean isDisposed() { + return ticksDisposable.isDisposed(); + } + abstract void onIntervalTick(); void send(ByteBuf frame) { @@ -122,22 +145,24 @@ void tryTimeout() { } } - long localLastReceivedPosition() { - return resumeStateHolder != null ? resumeStateHolder.impliedPosition() : 0; - } + void terminate(int terminationState) { + for (; ; ) { + final int state = this.state; - long remoteLastReceivedPosition(ByteBuf keepAliveFrame) { - return KeepAliveFrameCodec.lastPosition(keepAliveFrame); - } + if (state == STOPPED_STATE || state == DISPOSED_STATE) { + return; + } - @Override - public void dispose() { - stop(); + final Disposable disposable = this.ticksDisposable; + if (STATE.compareAndSet(this, state, terminationState)) { + disposable.dispose(); + return; + } + } } - @Override - public boolean isDisposed() { - return ticksDisposable.isDisposed(); + long localLastReceivedPosition() { + return resumeStateHolder != null ? resumeStateHolder.impliedPosition() : 0; } public static final class ClientKeepAliveSupport extends KeepAliveSupport { 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 c58cc4954..8dcf67a0b 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -22,12 +22,14 @@ import io.rsocket.DuplexConnection; import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.exceptions.Exceptions; +import io.rsocket.exceptions.RejectedResumeException; import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; import io.rsocket.frame.ResumeFrameCodec; import io.rsocket.frame.ResumeOkFrameCodec; import io.rsocket.keepalive.KeepAliveSupport; import java.time.Duration; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Function; import org.reactivestreams.Subscription; @@ -109,22 +111,21 @@ public ClientRSocketSession( resumableDuplexConnection.onClose().doFinally(__ -> dispose()).subscribe(); resumableDuplexConnection.onActiveConnectionClosed().subscribe(this::reconnect); - - S.lazySet(this, Operators.cancelledSubscription()); } void reconnect(int index) { - if (this.s == Operators.cancelledSubscription() - && S.compareAndSet(this, Operators.cancelledSubscription(), null)) { - keepAliveSupport.stop(); - if (logger.isDebugEnabled()) { - logger.debug( - "Side[client]|Session[{}]. Connection[{}] is lost. Reconnecting to resume...", - session, - index); - } - connectionFactory.retryWhen(retry).timeout(resumeSessionDuration).subscribe(this); + keepAliveSupport.stop(); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. Connection[{}] is lost. Reconnecting to resume...", + session, + index); } + connectionFactory + .doOnNext(this::tryReestablishSession) + .retryWhen(retry) + .timeout(resumeSessionDuration) + .subscribe(this); } @Override @@ -159,31 +160,10 @@ public boolean isDisposed() { return resumableConnection.isDisposed(); } - @Override - public void onSubscribe(Subscription s) { - if (Operators.setOnce(S, this, s)) { - s.request(Long.MAX_VALUE); - } - } - - @Override - public void onNext(Tuple2 tuple2) { + void tryReestablishSession(Tuple2 tuple2) { ByteBuf shouldBeResumeOKFrame = tuple2.getT1(); DuplexConnection nextDuplexConnection = tuple2.getT2(); - if (!Operators.terminate(S, this)) { - if (logger.isDebugEnabled()) { - logger.debug( - "Side[client]|Session[{}]. Session has already been expired. Terminating received connection", - session); - } - final ConnectionErrorException connectionErrorException = - new ConnectionErrorException("resumption_server=[Session Expired]"); - nextDuplexConnection.sendErrorAndClose(connectionErrorException); - nextDuplexConnection.receive().subscribe().dispose(); - return; - } - final int streamId = FrameHeaderCodec.streamId(shouldBeResumeOKFrame); if (streamId != 0) { if (logger.isDebugEnabled()) { @@ -196,7 +176,8 @@ public void onNext(Tuple2 tuple2) { resumableConnection.dispose(connectionErrorException); nextDuplexConnection.sendErrorAndClose(connectionErrorException); nextDuplexConnection.receive().subscribe().dispose(); - return; + + throw connectionErrorException; // throw to retry connection again } final FrameType frameType = FrameHeaderCodec.nativeFrameType(shouldBeResumeOKFrame); @@ -208,33 +189,56 @@ public void onNext(Tuple2 tuple2) { // observed final long position = resumableFramesStore.framePosition(); final long impliedPosition = resumableFramesStore.frameImpliedPosition(); - logger.debug( - "Side[client]|Session[{}]. ResumeOK FRAME received. ServerResumeState[remoteImpliedPosition[{}]]. ClientResumeState[impliedPosition[{}], position[{}]]", - session, - remoteImpliedPos, - impliedPosition, - position); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. ResumeOK FRAME received. ServerResumeState[remoteImpliedPosition[{}]]. ClientResumeState[impliedPosition[{}], position[{}]]", + session, + remoteImpliedPos, + impliedPosition, + position); + } if (position <= remoteImpliedPos) { try { if (position != remoteImpliedPos) { resumableFramesStore.releaseFrames(remoteImpliedPos); } } catch (IllegalStateException e) { - logger.debug("Exception occurred while releasing frames in the frameStore", e); - resumableConnection.dispose(e); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. Exception occurred while releasing frames in the frameStore", + session, + e); + } final ConnectionErrorException t = new ConnectionErrorException(e.getMessage(), e); + + resumableConnection.dispose(t); + nextDuplexConnection.sendErrorAndClose(t); nextDuplexConnection.receive().subscribe().dispose(); + return; } - if (resumableConnection.connect(nextDuplexConnection)) { - keepAliveSupport.start(); + if (!tryCancelSessionTimeout()) { if (logger.isDebugEnabled()) { logger.debug( - "Side[client]|Session[{}]. Session has been resumed successfully", session); + "Side[client]|Session[{}]. Session has already been expired. Terminating received connection", + session); } - } else { + final ConnectionErrorException connectionErrorException = + new ConnectionErrorException("resumption_server=[Session Expired]"); + nextDuplexConnection.sendErrorAndClose(connectionErrorException); + nextDuplexConnection.receive().subscribe().dispose(); + return; + } + + keepAliveSupport.start(); + + if (logger.isDebugEnabled()) { + logger.debug("Side[client]|Session[{}]. Session has been resumed successfully", session); + } + + if (!resumableConnection.connect(nextDuplexConnection)) { if (logger.isDebugEnabled()) { logger.debug( "Side[client]|Session[{}]. Session has already been expired. Terminating received connection", @@ -244,41 +248,96 @@ public void onNext(Tuple2 tuple2) { new ConnectionErrorException("resumption_server_pos=[Session Expired]"); nextDuplexConnection.sendErrorAndClose(connectionErrorException); nextDuplexConnection.receive().subscribe().dispose(); + // no need to do anything since connection resumable connection is liklly to + // be disposed } } else { - logger.debug( - "Side[client]|Session[{}]. Mismatching remote and local state. Expected RemoteImpliedPosition[{}] to be greater or equal to the LocalPosition[{}]. Terminating received connection", - session, - remoteImpliedPos, - position); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. Mismatching remote and local state. Expected RemoteImpliedPosition[{}] to be greater or equal to the LocalPosition[{}]. Terminating received connection", + session, + remoteImpliedPos, + position); + } final ConnectionErrorException connectionErrorException = new ConnectionErrorException("resumption_server_pos=[" + remoteImpliedPos + "]"); + resumableConnection.dispose(connectionErrorException); + nextDuplexConnection.sendErrorAndClose(connectionErrorException); nextDuplexConnection.receive().subscribe().dispose(); } } else if (frameType == FrameType.ERROR) { final RuntimeException exception = Exceptions.from(0, shouldBeResumeOKFrame); - logger.debug("Received error frame. Terminating received connection", exception); - resumableConnection.dispose(exception); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. Received error frame. Terminating received connection", + session, + exception); + } + if (exception instanceof RejectedResumeException) { + resumableConnection.dispose(exception); + nextDuplexConnection.dispose(); + nextDuplexConnection.receive().subscribe().dispose(); + return; + } + + nextDuplexConnection.dispose(); + throw exception; // assume retryable exception } else { - logger.debug( - "Illegal first frame received. RESUME_OK frame must be received before any others. Terminating received connection"); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. Illegal first frame received. RESUME_OK frame must be received before any others. Terminating received connection", + session); + } final ConnectionErrorException connectionErrorException = new ConnectionErrorException("RESUME_OK frame must be received before any others"); + resumableConnection.dispose(connectionErrorException); + nextDuplexConnection.sendErrorAndClose(connectionErrorException); nextDuplexConnection.receive().subscribe().dispose(); + + // no need to do anything since remote server rejected our connection completely + } + } + + boolean tryCancelSessionTimeout() { + for (; ; ) { + final Subscription subscription = this.s; + + if (subscription == Operators.cancelledSubscription()) { + return false; + } + + if (S.compareAndSet(this, subscription, null)) { + subscription.cancel(); + return true; + } } } + @Override + public void onSubscribe(Subscription s) { + if (Operators.setOnce(S, this, s)) { + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(Tuple2 objects) {} + @Override public void onError(Throwable t) { if (!Operators.terminate(S, this)) { Operators.onErrorDropped(t, currentContext()); } - resumableConnection.dispose(t); + if (t instanceof TimeoutException) { + resumableConnection.dispose(); + } else { + resumableConnection.dispose(t); + } } @Override 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 18cd7167a..933ac09ca 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -103,21 +103,18 @@ public boolean connect(DuplexConnection nextConnection) { } void initConnection(DuplexConnection nextConnection) { - if (logger.isDebugEnabled()) { - logger.debug( - "Side[{}]|Session[{}]. Connecting to DuplexConnection[{}]", - side, - session, - nextConnection); - } - - final int currentConnectionIndex = connectionIndex; + final int nextConnectionIndex = this.connectionIndex + 1; final FrameReceivingSubscriber frameReceivingSubscriber = new FrameReceivingSubscriber(side, resumableFramesStore, receiveSubscriber); - this.connectionIndex = currentConnectionIndex + 1; + this.connectionIndex = nextConnectionIndex; this.activeReceivingSubscriber = frameReceivingSubscriber; + if (logger.isDebugEnabled()) { + logger.debug( + "Side[{}]|Session[{}]|DuplexConnection[{}]. Connecting", side, session, connectionIndex); + } + final Disposable resumeStreamSubscription = resumableFramesStore .resumeStream() @@ -136,17 +133,18 @@ void initConnection(DuplexConnection nextConnection) { resumeStreamSubscription.dispose(); if (logger.isDebugEnabled()) { logger.debug( - "Side[{}]|Session[{}]. Disconnected from DuplexConnection[{}]", + "Side[{}]|Session[{}]|DuplexConnection[{}]. Disconnected", side, session, - nextConnection); + connectionIndex); } - Sinks.EmitResult result = onConnectionClosedSink.tryEmitNext(currentConnectionIndex); + Sinks.EmitResult result = onConnectionClosedSink.tryEmitNext(nextConnectionIndex); if (!result.equals(Sinks.EmitResult.OK)) { logger.error( - "Side[{}]|Session[{}]. Failed to notify session of closed connection: {}", + "Side[{}]|Session[{}]|DuplexConnection[{}]. Failed to notify session of closed connection: {}", side, session, + connectionIndex, result); } }) @@ -193,14 +191,18 @@ public void sendErrorAndClose(RSocketErrorException rSocketErrorException) { null, t -> { framesSaverDisposable.dispose(); + activeReceivingSubscriber.dispose(); savableFramesSender.dispose(); onConnectionClosedSink.tryEmitComplete(); + onClose.tryEmitError(t); }, () -> { framesSaverDisposable.dispose(); + activeReceivingSubscriber.dispose(); savableFramesSender.dispose(); onConnectionClosedSink.tryEmitComplete(); + final Throwable cause = rSocketErrorException.getCause(); if (cause == null) { onClose.tryEmitEmpty(); @@ -242,7 +244,11 @@ void dispose(@Nullable Throwable e) { } if (logger.isDebugEnabled()) { - logger.debug("Side[{}]|Session[{}]. Disposing...", side, session); + logger.debug( + "Side[{}]|Session[{}]|DuplexConnection[{}]. Disposing...", + side, + session, + connectionIndex); } framesSaverDisposable.dispose(); 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 a57899cac..83c5bf8c1 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -44,7 +44,7 @@ public class ServerRSocketSession final ResumableDuplexConnection resumableConnection; final Duration resumeSessionDuration; final ResumableFramesStore resumableFramesStore; - final String resumeToken; + final String session; final ByteBufAllocator allocator; final boolean cleanupStoreOnKeepAlive; @@ -66,13 +66,13 @@ public class ServerRSocketSession KeepAliveSupport keepAliveSupport; public ServerRSocketSession( - ByteBuf resumeToken, - DuplexConnection initialDuplexConnection, + ByteBuf session, ResumableDuplexConnection resumableDuplexConnection, - Duration resumeSessionDuration, + DuplexConnection initialDuplexConnection, ResumableFramesStore resumableFramesStore, + Duration resumeSessionDuration, boolean cleanupStoreOnKeepAlive) { - this.resumeToken = resumeToken.toString(CharsetUtil.UTF_8); + this.session = session.toString(CharsetUtil.UTF_8); this.allocator = initialDuplexConnection.alloc(); this.resumeSessionDuration = resumeSessionDuration; this.resumableFramesStore = resumableFramesStore; @@ -88,8 +88,14 @@ public ServerRSocketSession( void tryTimeoutSession() { keepAliveSupport.stop(); + + if (logger.isDebugEnabled()) { + logger.debug( + "Side[server]|Session[{}]. Connection is lost. Trying to timeout the active session", + session); + } + Mono.delay(resumeSessionDuration).subscribe(this); - logger.debug("Connection is lost. Trying to timeout the active session[{}]", resumeToken); if (WIP.decrementAndGet(this) == 0) { return; @@ -103,6 +109,10 @@ void tryTimeoutSession() { public void resumeWith(ByteBuf resumeFrame, DuplexConnection nextDuplexConnection) { + if (logger.isDebugEnabled()) { + logger.debug("Side[server]|Session[{}]. New DuplexConnection received.", session); + } + long remotePos = ResumeFrameCodec.firstAvailableClientPos(resumeFrame); long remoteImpliedPos = ResumeFrameCodec.lastReceivedServerPos(resumeFrame); @@ -119,36 +129,30 @@ public void resumeWith(ByteBuf resumeFrame, DuplexConnection nextDuplexConnectio } void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplexConnection) { + if (!tryCancelSessionTimeout()) { + if (logger.isDebugEnabled()) { + logger.debug( + "Side[server]|Session[{}]. Session has already been expired. Terminating received connection", + session); + } + final RejectedResumeException rejectedResumeException = + new RejectedResumeException("resume_internal_error: Session Expired"); + nextDuplexConnection.sendErrorAndClose(rejectedResumeException); + nextDuplexConnection.receive().subscribe().dispose(); + return; + } long impliedPosition = resumableFramesStore.frameImpliedPosition(); long position = resumableFramesStore.framePosition(); if (logger.isDebugEnabled()) { logger.debug( - "Side[server]|Session[{}]. Resume FRAME received. ClientResumeState{observedFramesPosition[{}], sentFramesPosition[{}]}, ServerResumeState{observedFramesPosition[{}], sentFramesPosition[{}]}", - resumeToken, - remoteImpliedPos, - remotePos, + "Side[server]|Session[{}]. Resume FRAME received. ServerResumeState[impliedPosition[{}], position[{}]]. ClientResumeState[remoteImpliedPosition[{}], remotePosition[{}]]", + session, impliedPosition, - position); - } - - for (; ; ) { - final Subscription subscription = this.s; - - if (subscription == Operators.cancelledSubscription()) { - logger.debug("Session has already been expired. Terminating received connection"); - final RejectedResumeException rejectedResumeException = - new RejectedResumeException("resume_internal_error: Session Expired"); - nextDuplexConnection.sendErrorAndClose(rejectedResumeException); - nextDuplexConnection.receive().subscribe().dispose(); - return; - } - - if (S.compareAndSet(this, subscription, null)) { - subscription.cancel(); - break; - } + position, + remoteImpliedPos, + remotePos); } if (remotePos <= impliedPosition && position <= remoteImpliedPos) { @@ -160,44 +164,60 @@ void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplex if (logger.isDebugEnabled()) { logger.debug( "Side[server]|Session[{}]. ResumeOKFrame[impliedPosition[{}]] has been sent", - resumeToken, + session, impliedPosition); } } catch (Throwable t) { - logger.debug("Exception occurred while releasing frames in the frameStore", t); - tryTimeoutSession(); - nextDuplexConnection.sendErrorAndClose(new RejectedResumeException(t.getMessage(), t)); - nextDuplexConnection.receive().subscribe().dispose(); - return; - } - if (resumableConnection.connect(nextDuplexConnection)) { - keepAliveSupport.start(); if (logger.isDebugEnabled()) { logger.debug( - "Side[server]|Session[{}]. Session has been resumed successfully", resumeToken); + "Side[server]|Session[{}]. Exception occurred while releasing frames in the frameStore", + session, + t); } - } else { + + dispose(); + + final RejectedResumeException rejectedResumeException = + new RejectedResumeException(t.getMessage(), t); + nextDuplexConnection.sendErrorAndClose(rejectedResumeException); + nextDuplexConnection.receive().subscribe().dispose(); + + return; + } + + keepAliveSupport.start(); + + if (logger.isDebugEnabled()) { + logger.debug("Side[server]|Session[{}]. Session has been resumed successfully", session); + } + + if (!resumableConnection.connect(nextDuplexConnection)) { if (logger.isDebugEnabled()) { logger.debug( "Side[server]|Session[{}]. Session has already been expired. Terminating received connection", - resumeToken); + session); } - final ConnectionErrorException connectionErrorException = - new ConnectionErrorException("resume_internal_error: Session Expired"); - nextDuplexConnection.sendErrorAndClose(connectionErrorException); + final RejectedResumeException rejectedResumeException = + new RejectedResumeException("resume_internal_error: Session Expired"); + nextDuplexConnection.sendErrorAndClose(rejectedResumeException); nextDuplexConnection.receive().subscribe().dispose(); + + // resumableConnection is likely to be disposed at this stage. Thus we have + // nothing to do } } else { if (logger.isDebugEnabled()) { logger.debug( "Side[server]|Session[{}]. Mismatching remote and local state. Expected RemoteImpliedPosition[{}] to be greater or equal to the LocalPosition[{}] and RemotePosition[{}] to be less or equal to LocalImpliedPosition[{}]. Terminating received connection", - resumeToken, + session, remoteImpliedPos, position, remotePos, impliedPosition); } - tryTimeoutSession(); + + dispose(); + final RejectedResumeException rejectedResumeException = new RejectedResumeException( String.format( @@ -208,6 +228,21 @@ void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplex } } + boolean tryCancelSessionTimeout() { + for (; ; ) { + final Subscription subscription = this.s; + + if (subscription == Operators.cancelledSubscription()) { + return false; + } + + if (S.compareAndSet(this, subscription, null)) { + subscription.cancel(); + return true; + } + } + } + @Override public long impliedPosition() { return resumableFramesStore.frameImpliedPosition(); @@ -216,7 +251,11 @@ public long impliedPosition() { @Override public void onImpliedPosition(long remoteImpliedPos) { if (cleanupStoreOnKeepAlive) { - resumableFramesStore.releaseFrames(remoteImpliedPos); + try { + resumableFramesStore.releaseFrames(remoteImpliedPos); + } catch (Throwable e) { + resumableConnection.sendErrorAndClose(new ConnectionErrorException(e.getMessage(), e)); + } } } diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java new file mode 100644 index 000000000..34d8a7345 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java @@ -0,0 +1,446 @@ +package io.rsocket.resume; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCounted; +import io.rsocket.FrameAssert; +import io.rsocket.exceptions.ConnectionCloseException; +import io.rsocket.exceptions.RejectedResumeException; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.KeepAliveFrameCodec; +import io.rsocket.frame.ResumeOkFrameCodec; +import io.rsocket.keepalive.KeepAliveSupport; +import io.rsocket.test.util.TestClientTransport; +import io.rsocket.test.util.TestDuplexConnection; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Operators; +import reactor.test.StepVerifier; +import reactor.test.scheduler.VirtualTimeScheduler; +import reactor.util.function.Tuples; +import reactor.util.retry.Retry; + +public class ClientRSocketSessionTest { + + @Test + void sessionTimeoutSmokeTest() { + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ClientRSocketSession session = + new ClientRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.connect().delaySubscription(Duration.ofMillis(1)), + c -> { + AtomicBoolean firstHandled = new AtomicBoolean(); + return ((TestDuplexConnection) c) + .receive() + .next() + .doOnNext(__ -> firstHandled.set(true)) + .doOnCancel( + () -> { + if (firstHandled.compareAndSet(false, true)) { + c.dispose(); + } + }) + .map(b -> Tuples.of(b, c)); + }, + framesStore, + Duration.ofMinutes(1), + Retry.indefinitely(), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + // deactivate connection + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); + // timeout should not terminate current connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + + // send RESUME_OK frame + transport.testConnection().addToReceivedBuffer(ResumeOkFrameCodec.encode(transport.alloc(), 0)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be terminated + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); + + // disconnects for the second time + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + transport + .testConnection() + .addToReceivedBuffer( + ErrorFrameCodec.encode( + transport.alloc(), 0, new ConnectionCloseException("some message"))); + // connection should be closed because of the wrong first frame + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout is still in progress + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); + // should obtain new connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_OK frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); + + assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); + assertThat(transport.testConnection().isDisposed()).isTrue(); + + assertThat(session.isDisposed()).isTrue(); + + resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); + transport.alloc().assertHasNoLeaks(); + } + + @Test + void sessionTerminationOnWrongFrameTest() { + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ClientRSocketSession session = + new ClientRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.connect().delaySubscription(Duration.ofMillis(1)), + c -> { + AtomicBoolean firstHandled = new AtomicBoolean(); + return ((TestDuplexConnection) c) + .receive() + .next() + .doOnNext(__ -> firstHandled.set(true)) + .doOnCancel( + () -> { + if (firstHandled.compareAndSet(false, true)) { + c.dispose(); + } + }) + .map(b -> Tuples.of(b, c)); + }, + framesStore, + Duration.ofMinutes(1), + Retry.indefinitely(), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + // deactivate connection + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); + // timeout should not terminate current connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + + // send RESUME_OK frame + transport.testConnection().addToReceivedBuffer(ResumeOkFrameCodec.encode(transport.alloc(), 0)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be terminated + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); + + // disconnects for the second time + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + // Send KEEPALIVE frame as a first frame + transport + .testConnection() + .addToReceivedBuffer( + KeepAliveFrameCodec.encode(transport.alloc(), false, 0, Unpooled.EMPTY_BUFFER)); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); + + assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); + assertThat(transport.testConnection().isDisposed()).isTrue(); + assertThat(session.isDisposed()).isTrue(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.ERROR) + .matches(ReferenceCounted::release); + + resumableDuplexConnection + .onClose() + .as(StepVerifier::create) + .expectErrorMessage("RESUME_OK frame must be received before any others") + .verify(); + transport.alloc().assertHasNoLeaks(); + } + + @Test + void shouldErrorWithNoRetriesOnErrorFrameTest() { + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ClientRSocketSession session = + new ClientRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.connect().delaySubscription(Duration.ofMillis(1)), + c -> { + AtomicBoolean firstHandled = new AtomicBoolean(); + return ((TestDuplexConnection) c) + .receive() + .next() + .doOnNext(__ -> firstHandled.set(true)) + .doOnCancel( + () -> { + if (firstHandled.compareAndSet(false, true)) { + c.dispose(); + } + }) + .map(b -> Tuples.of(b, c)); + }, + framesStore, + Duration.ofMinutes(1), + Retry.indefinitely(), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + // deactivate connection + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); + // timeout should not terminate current connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + + // send REJECTED_RESUME_ERROR frame + transport + .testConnection() + .addToReceivedBuffer( + ErrorFrameCodec.encode( + transport.alloc(), 0, new RejectedResumeException("failed resumption"))); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // timeout should be terminated + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isTrue(); + + resumableDuplexConnection + .onClose() + .as(StepVerifier::create) + .expectError(RejectedResumeException.class) + .verify(); + transport.alloc().assertHasNoLeaks(); + } + + @Test + void shouldTerminateConnectionOnIllegalStateInKeepAliveFrame() { + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ClientRSocketSession session = + new ClientRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.connect().delaySubscription(Duration.ofMillis(1)), + c -> { + AtomicBoolean firstHandled = new AtomicBoolean(); + return ((TestDuplexConnection) c) + .receive() + .next() + .doOnNext(__ -> firstHandled.set(true)) + .doOnCancel( + () -> { + if (firstHandled.compareAndSet(false, true)) { + c.dispose(); + } + }) + .map(b -> Tuples.of(b, c)); + }, + framesStore, + Duration.ofMinutes(1), + Retry.indefinitely(), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + keepAliveSupport.resumeState(session); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + final ByteBuf keepAliveFrame = + KeepAliveFrameCodec.encode(transport.alloc(), false, 1529, Unpooled.EMPTY_BUFFER); + keepAliveSupport.receive(keepAliveFrame); + keepAliveFrame.release(); + + assertThat(transport.testConnection().isDisposed()).isTrue(); + // timeout should be terminated + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isTrue(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.ERROR) + .matches(ReferenceCounted::release); + + resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); + + transport.alloc().assertHasNoLeaks(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java deleted file mode 100644 index d15abd189..000000000 --- a/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java +++ /dev/null @@ -1,57 +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 org.junit.jupiter.api.Assertions; -// import org.junit.jupiter.api.BeforeEach; -// import org.junit.jupiter.api.Test; -// -// public class ResumeCalculatorTest { -// -// @BeforeEach -// void setUp() {} -// -// @Test -// void clientResumeSuccess() { -// long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 42, -1, 3); -// Assertions.assertEquals(3, position); -// } -// -// @Test -// void clientResumeError() { -// long position = ResumableDuplexConnection.calculateRemoteImpliedPos(4, 42, -1, 3); -// Assertions.assertEquals(-1, position); -// } -// -// @Test -// void serverResumeSuccess() { -// long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 42, 4, 23); -// Assertions.assertEquals(23, position); -// } -// -// @Test -// void serverResumeErrorClientState() { -// long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 3, 4, 23); -// Assertions.assertEquals(-1, position); -// } -// -// @Test -// void serverResumeErrorServerState() { -// long position = ResumableDuplexConnection.calculateRemoteImpliedPos(4, 42, 4, 1); -// Assertions.assertEquals(-1, position); -// } -// } diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java new file mode 100644 index 000000000..a3a682d94 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java @@ -0,0 +1,182 @@ +package io.rsocket.resume; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCounted; +import io.rsocket.FrameAssert; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.KeepAliveFrameCodec; +import io.rsocket.frame.ResumeFrameCodec; +import io.rsocket.keepalive.KeepAliveSupport; +import io.rsocket.test.util.TestClientTransport; +import java.time.Duration; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Operators; +import reactor.test.StepVerifier; +import reactor.test.scheduler.VirtualTimeScheduler; + +public class ServerRSocketSessionTest { + + @Test + void sessionTimeoutSmokeTest() { + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ServerRSocketSession session = + new ServerRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.testConnection(), + framesStore, + Duration.ofMinutes(1), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + // deactivate connection + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // resubscribe so a new connection is generated + transport.connect().subscribe(); + + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); + // timeout should not terminate current connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + + // send RESUME frame + final ByteBuf resumeFrame = + ResumeFrameCodec.encode(transport.alloc(), Unpooled.EMPTY_BUFFER, 0, 0); + session.resumeWith(resumeFrame, transport.testConnection()); + resumeFrame.release(); + + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be terminated + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME_OK) + .matches(ReferenceCounted::release); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); + + // disconnects for the second time + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + transport.connect().subscribe(); + + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(61)); + + final ByteBuf resumeFrame1 = + ResumeFrameCodec.encode(transport.alloc(), Unpooled.EMPTY_BUFFER, 0, 0); + session.resumeWith(resumeFrame1, transport.testConnection()); + resumeFrame1.release(); + + // should obtain new connection + assertThat(transport.testConnection().isDisposed()).isTrue(); + // timeout should be still active since no RESUME_OK frame has been received yet + assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); + assertThat(session.isDisposed()).isTrue(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.ERROR) + .matches(ReferenceCounted::release); + + resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); + transport.alloc().assertHasNoLeaks(); + } + + @Test + void shouldTerminateConnectionOnIllegalStateInKeepAliveFrame() { + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ServerRSocketSession session = + new ServerRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.testConnection(), + framesStore, + Duration.ofMinutes(1), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + keepAliveSupport.resumeState(session); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + final ByteBuf keepAliveFrame = + KeepAliveFrameCodec.encode(transport.alloc(), false, 1529, Unpooled.EMPTY_BUFFER); + keepAliveSupport.receive(keepAliveFrame); + keepAliveFrame.release(); + + assertThat(transport.testConnection().isDisposed()).isTrue(); + // timeout should be terminated + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isTrue(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.ERROR) + .matches(ReferenceCounted::release); + + resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); + + transport.alloc().assertHasNoLeaks(); + } +} From a1996a1b5b38a6cdba5fa0b8bd7a343a5f9e727e Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 9 Jun 2021 08:27:57 +0300 Subject: [PATCH 050/116] migrates to Sonotype releasing and adds Github Packages for snapshoting Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .github/workflows/gradle-release.yml | 16 +++---- build.gradle | 5 +-- gradle/bintray.gradle | 63 ---------------------------- gradle/github-pkg.gradle | 23 ++++++++++ gradle/publications.gradle | 3 +- gradle/sonotype.gradle | 36 ++++++++++++++++ rsocket-bom/build.gradle | 2 +- rsocket-core/build.gradle | 2 +- rsocket-load-balancer/build.gradle | 2 +- rsocket-micrometer/build.gradle | 2 +- rsocket-test/build.gradle | 2 +- rsocket-transport-local/build.gradle | 2 +- rsocket-transport-netty/build.gradle | 2 +- 13 files changed, 78 insertions(+), 82 deletions(-) delete mode 100644 gradle/bintray.gradle create mode 100644 gradle/github-pkg.gradle create mode 100644 gradle/sonotype.gradle diff --git a/.github/workflows/gradle-release.yml b/.github/workflows/gradle-release.yml index 08f2698dc..922eb0e3e 100644 --- a/.github/workflows/gradle-release.yml +++ b/.github/workflows/gradle-release.yml @@ -32,13 +32,13 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew clean build - - name: Publish Packages to Bintray - run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" -Pversion="${githubRef#refs/tags/}" -PbuildNumber="${buildNumber}" bintrayUpload + run: ./gradlew clean build -x test + - name: Publish Packages to Sonotype + run: ./gradlew -Pversion="${githubRef#refs/tags/}" -PbuildNumber="${buildNumber}" sign publishMavenPublicationToSonatypeRepository env: - bintrayUser: ${{ secrets.bintrayUser }} - bintrayKey: ${{ secrets.bintrayKey }} - sonatypeUsername: ${{ secrets.sonatypeUsername }} - sonatypePassword: ${{ secrets.sonatypePassword }} githubRef: ${{ github.ref }} - buildNumber: ${{ github.run_number }} \ No newline at end of file + buildNumber: ${{ github.run_number }} + ORG_GRADLE_PROJECT_signingKey: ${{secrets.signingKey}} + ORG_GRADLE_PROJECT_signingPassword: ${{secrets.signingPassword}} + ORG_GRADLE_PROJECT_sonatypeUsername: ${{secrets.sonatypeUsername}} + ORG_GRADLE_PROJECT_sonatypePassword: ${{secrets.sonatypePassword}} \ No newline at end of file diff --git a/build.gradle b/build.gradle index f8083a4ea..8a7d74156 100644 --- a/build.gradle +++ b/build.gradle @@ -16,10 +16,9 @@ plugins { id 'com.github.sherter.google-java-format' version '0.8' apply false - id 'com.jfrog.artifactory' version '4.15.2' apply false - id 'com.jfrog.bintray' version '1.8.5' apply false + id 'com.jfrog.artifactory' version '4.21.0' apply false id 'me.champeau.gradle.jmh' version '0.5.0' apply false - id 'io.spring.dependency-management' version '1.0.9.RELEASE' apply false + id 'io.spring.dependency-management' version '1.0.11.RELEASE' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false } diff --git a/gradle/bintray.gradle b/gradle/bintray.gradle deleted file mode 100644 index 5015f94e4..000000000 --- a/gradle/bintray.gradle +++ /dev/null @@ -1,63 +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. - */ - -if (project.hasProperty('bintrayUser') && project.hasProperty('bintrayKey') && - project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword')) { - - subprojects { - plugins.withId('com.jfrog.bintray') { - bintray { - user = project.property('bintrayUser') - key = project.property('bintrayKey') - - publications = ['maven'] - publish = true - override = true - - pkg { - repo = 'RSocket' - name = 'rsocket-java' - licenses = ['Apache-2.0'] - - issueTrackerUrl = 'https://github.com/rsocket/rsocket-java/issues' - websiteUrl = 'https://github.com/rsocket/rsocket-java' - vcsUrl = 'https://github.com/rsocket/rsocket-java.git' - - githubRepo = 'rsocket/rsocket-java' //Optional Github repository - githubReleaseNotesFile = 'README.md' //Optional Github readme file - - version { - name = project.version - released = new Date() - vcsTag = project.version - - gpg { - sign = true - } - - mavenCentralSync { - user = project.property('sonatypeUsername') - password = project.property('sonatypePassword') - } - } - } - } - tasks.named("bintrayUpload").configure { - onlyIf { System.getenv('SKIP_RELEASE') != "true" } - } - } - } -} diff --git a/gradle/github-pkg.gradle b/gradle/github-pkg.gradle new file mode 100644 index 000000000..a9c1a24ff --- /dev/null +++ b/gradle/github-pkg.gradle @@ -0,0 +1,23 @@ +subprojects { + + plugins.withType(JavaLibraryPlugin) { + plugins.withType(MavenPublishPlugin) { + publishing { + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/rsocket/rsocket-java") + credentials { + username = project.findProperty("gpr.user") ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") ?: System.getenv("TOKEN") + } + } + } + } + + tasks.named("publish").configure { + onlyIf { System.getenv('SKIP_RELEASE') != "true" } + } + } + } +} \ No newline at end of file diff --git a/gradle/publications.gradle b/gradle/publications.gradle index b12d9e9c2..383b81c77 100644 --- a/gradle/publications.gradle +++ b/gradle/publications.gradle @@ -1,5 +1,6 @@ apply from: "${rootDir}/gradle/artifactory.gradle" -apply from: "${rootDir}/gradle/bintray.gradle" +apply from: "${rootDir}/gradle/github-pkg.gradle" +apply from: "${rootDir}/gradle/sonotype.gradle" subprojects { plugins.withType(MavenPublishPlugin) { diff --git a/gradle/sonotype.gradle b/gradle/sonotype.gradle new file mode 100644 index 000000000..dc002d2c9 --- /dev/null +++ b/gradle/sonotype.gradle @@ -0,0 +1,36 @@ +subprojects { + if (project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword')) { + plugins.withType(JavaLibraryPlugin) { + plugins.withType(MavenPublishPlugin) { + plugins.withType(SigningPlugin) { + + signing { + //requiring signature if there is a publish task that is not to MavenLocal + required { gradle.taskGraph.allTasks.any { it.name.toLowerCase().contains("publish") && !it.name.contains("MavenLocal") } } + def signingKey = project.findProperty("signingKey") + def signingPassword = project.findProperty("signingPassword") + + useInMemoryPgpKeys(signingKey, signingPassword) + + afterEvaluate { + sign publishing.publications.maven + } + } + + publishing { + repositories { + maven { + name = "sonatype" + url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2" + credentials { + username project.findProperty("sonatypeUsername") + password project.findProperty("sonatypePassword") + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/rsocket-bom/build.gradle b/rsocket-bom/build.gradle index 2efc20a91..3f313b6bb 100755 --- a/rsocket-bom/build.gradle +++ b/rsocket-bom/build.gradle @@ -16,8 +16,8 @@ plugins { id 'java-platform' id 'maven-publish' + id 'signing' id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' } description = 'RSocket Java Bill of materials.' diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index 41adbd7a8..013c283b2 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -17,8 +17,8 @@ plugins { id 'java-library' id 'maven-publish' + id 'signing' id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' id 'io.morethan.jmhreport' id 'me.champeau.gradle.jmh' } diff --git a/rsocket-load-balancer/build.gradle b/rsocket-load-balancer/build.gradle index 748f95de6..c39967e64 100644 --- a/rsocket-load-balancer/build.gradle +++ b/rsocket-load-balancer/build.gradle @@ -17,8 +17,8 @@ plugins { id 'java-library' id 'maven-publish' + id 'signing' id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' } dependencies { diff --git a/rsocket-micrometer/build.gradle b/rsocket-micrometer/build.gradle index 4be616623..b451235ac 100644 --- a/rsocket-micrometer/build.gradle +++ b/rsocket-micrometer/build.gradle @@ -17,8 +17,8 @@ plugins { id 'java-library' id 'maven-publish' + id 'signing' id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' } dependencies { diff --git a/rsocket-test/build.gradle b/rsocket-test/build.gradle index 5ec1a8061..681e41189 100644 --- a/rsocket-test/build.gradle +++ b/rsocket-test/build.gradle @@ -17,8 +17,8 @@ plugins { id 'java-library' id 'maven-publish' + id 'signing' id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' } dependencies { diff --git a/rsocket-transport-local/build.gradle b/rsocket-transport-local/build.gradle index a5ba84d5c..1f9f972f9 100644 --- a/rsocket-transport-local/build.gradle +++ b/rsocket-transport-local/build.gradle @@ -17,8 +17,8 @@ plugins { id 'java-library' id 'maven-publish' + id 'signing' id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' } dependencies { diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 64e483c90..b03afbc6d 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -17,8 +17,8 @@ plugins { id 'java-library' id 'maven-publish' + id 'signing' id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' id "com.google.osdetector" version "1.4.0" } From ee5a93467a57eb72fa59d8419340b6c004cc8448 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 9 Jun 2021 11:07:56 +0300 Subject: [PATCH 051/116] replaces artifactory release with github packages for snapshots Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .github/workflows/gradle-all.yml | 5 ++- .github/workflows/gradle-main.yml | 5 ++- build.gradle | 1 - gradle/artifactory.gradle | 47 ---------------------------- gradle/github-pkg.gradle | 4 +-- gradle/publications.gradle | 1 - gradle/sonotype.gradle | 40 +++++++++++------------ rsocket-bom/build.gradle | 1 - rsocket-core/build.gradle | 1 - rsocket-load-balancer/build.gradle | 1 - rsocket-micrometer/build.gradle | 1 - rsocket-test/build.gradle | 1 - rsocket-transport-local/build.gradle | 1 - rsocket-transport-netty/build.gradle | 1 - 14 files changed, 25 insertions(+), 85 deletions(-) delete mode 100644 gradle/artifactory.gradle diff --git a/.github/workflows/gradle-all.yml b/.github/workflows/gradle-all.yml index 8540539bb..a1ad36d32 100644 --- a/.github/workflows/gradle-all.yml +++ b/.github/workflows/gradle-all.yml @@ -37,9 +37,8 @@ jobs: run: ./gradlew clean build - name: Publish Packages to Artifactory if: ${{ matrix.jdk == '1.8' }} - run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PversionSuffix="-${githubRef#refs/heads/}-SNAPSHOT" -PbuildNumber="${buildNumber}" artifactoryPublish --stacktrace + run: ./gradlew -PversionSuffix="-${githubRef#refs/heads/}-SNAPSHOT" -PbuildNumber="${buildNumber}" publishMavenPublicationToGitHubPackagesRepository --stacktrace env: - bintrayUser: ${{ secrets.bintrayUser }} - bintrayKey: ${{ secrets.bintrayKey }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} githubRef: ${{ github.ref }} buildNumber: ${{ github.run_number }} \ No newline at end of file diff --git a/.github/workflows/gradle-main.yml b/.github/workflows/gradle-main.yml index d8ba3c3d5..950dc80ce 100644 --- a/.github/workflows/gradle-main.yml +++ b/.github/workflows/gradle-main.yml @@ -37,10 +37,9 @@ jobs: run: ./gradlew clean build - name: Publish Packages to Artifactory if: ${{ matrix.jdk == '1.8' }} - run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PversionSuffix="-SNAPSHOT" -PbuildNumber="${buildNumber}" artifactoryPublish --stacktrace + run: ./gradlew -PversionSuffix="-SNAPSHOT" -PbuildNumber="${buildNumber}" publishMavenPublicationToGitHubPackagesRepository --stacktrace env: - bintrayUser: ${{ secrets.bintrayUser }} - bintrayKey: ${{ secrets.bintrayKey }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} buildNumber: ${{ github.run_number }} - name: Aggregate test reports with ciMate if: always() diff --git a/build.gradle b/build.gradle index 8a7d74156..00c5d2b9d 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,6 @@ plugins { id 'com.github.sherter.google-java-format' version '0.8' apply false - id 'com.jfrog.artifactory' version '4.21.0' apply false id 'me.champeau.gradle.jmh' version '0.5.0' apply false id 'io.spring.dependency-management' version '1.0.11.RELEASE' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false diff --git a/gradle/artifactory.gradle b/gradle/artifactory.gradle deleted file mode 100644 index cdffb2741..000000000 --- a/gradle/artifactory.gradle +++ /dev/null @@ -1,47 +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. - */ - -if (project.hasProperty('bintrayUser') && project.hasProperty('bintrayKey')) { - - subprojects { - plugins.withId('com.jfrog.artifactory') { - artifactory { - publish { - contextUrl = 'https://oss.jfrog.org' - - repository { - repoKey = 'oss-snapshot-local' - - // Credentials for oss.jfrog.org are a user's Bintray credentials - username = project.property('bintrayUser') - password = project.property('bintrayKey') - } - - defaults { - publications(publishing.publications.maven) - } - - if (project.hasProperty('buildNumber')) { - clientConfig.info.setBuildNumber(project.property('buildNumber').toString()) - } - } - } - tasks.named("artifactoryPublish").configure { - onlyIf { System.getenv('SKIP_RELEASE') != "true" } - } - } - } -} diff --git a/gradle/github-pkg.gradle b/gradle/github-pkg.gradle index a9c1a24ff..98a68ecdb 100644 --- a/gradle/github-pkg.gradle +++ b/gradle/github-pkg.gradle @@ -8,8 +8,8 @@ subprojects { name = "GitHubPackages" url = uri("https://maven.pkg.github.com/rsocket/rsocket-java") credentials { - username = project.findProperty("gpr.user") ?: System.getenv("USERNAME") - password = project.findProperty("gpr.key") ?: System.getenv("TOKEN") + username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") + password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") } } } diff --git a/gradle/publications.gradle b/gradle/publications.gradle index 383b81c77..c405a4032 100644 --- a/gradle/publications.gradle +++ b/gradle/publications.gradle @@ -1,4 +1,3 @@ -apply from: "${rootDir}/gradle/artifactory.gradle" apply from: "${rootDir}/gradle/github-pkg.gradle" apply from: "${rootDir}/gradle/sonotype.gradle" diff --git a/gradle/sonotype.gradle b/gradle/sonotype.gradle index dc002d2c9..1effd76b0 100644 --- a/gradle/sonotype.gradle +++ b/gradle/sonotype.gradle @@ -1,31 +1,29 @@ subprojects { if (project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword')) { - plugins.withType(JavaLibraryPlugin) { - plugins.withType(MavenPublishPlugin) { - plugins.withType(SigningPlugin) { + plugins.withType(MavenPublishPlugin) { + plugins.withType(SigningPlugin) { - signing { - //requiring signature if there is a publish task that is not to MavenLocal - required { gradle.taskGraph.allTasks.any { it.name.toLowerCase().contains("publish") && !it.name.contains("MavenLocal") } } - def signingKey = project.findProperty("signingKey") - def signingPassword = project.findProperty("signingPassword") + signing { + //requiring signature if there is a publish task that is not to MavenLocal + required { gradle.taskGraph.allTasks.any { it.name.toLowerCase().contains("publish") && !it.name.contains("MavenLocal") } } + def signingKey = project.findProperty("signingKey") + def signingPassword = project.findProperty("signingPassword") - useInMemoryPgpKeys(signingKey, signingPassword) + useInMemoryPgpKeys(signingKey, signingPassword) - afterEvaluate { - sign publishing.publications.maven - } + afterEvaluate { + sign publishing.publications.maven } + } - publishing { - repositories { - maven { - name = "sonatype" - url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2" - credentials { - username project.findProperty("sonatypeUsername") - password project.findProperty("sonatypePassword") - } + publishing { + repositories { + maven { + name = "sonatype" + url = "https://oss.sonatype.org/service/local/staging/deploy/maven2" + credentials { + username project.findProperty("sonatypeUsername") + password project.findProperty("sonatypePassword") } } } diff --git a/rsocket-bom/build.gradle b/rsocket-bom/build.gradle index 3f313b6bb..a75ab3bc8 100755 --- a/rsocket-bom/build.gradle +++ b/rsocket-bom/build.gradle @@ -17,7 +17,6 @@ plugins { id 'java-platform' id 'maven-publish' id 'signing' - id 'com.jfrog.artifactory' } description = 'RSocket Java Bill of materials.' diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index 013c283b2..fbeee37ce 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -18,7 +18,6 @@ plugins { id 'java-library' id 'maven-publish' id 'signing' - id 'com.jfrog.artifactory' id 'io.morethan.jmhreport' id 'me.champeau.gradle.jmh' } diff --git a/rsocket-load-balancer/build.gradle b/rsocket-load-balancer/build.gradle index c39967e64..d70e5b2cc 100644 --- a/rsocket-load-balancer/build.gradle +++ b/rsocket-load-balancer/build.gradle @@ -18,7 +18,6 @@ plugins { id 'java-library' id 'maven-publish' id 'signing' - id 'com.jfrog.artifactory' } dependencies { diff --git a/rsocket-micrometer/build.gradle b/rsocket-micrometer/build.gradle index b451235ac..fd89aeae0 100644 --- a/rsocket-micrometer/build.gradle +++ b/rsocket-micrometer/build.gradle @@ -18,7 +18,6 @@ plugins { id 'java-library' id 'maven-publish' id 'signing' - id 'com.jfrog.artifactory' } dependencies { diff --git a/rsocket-test/build.gradle b/rsocket-test/build.gradle index 681e41189..282a65829 100644 --- a/rsocket-test/build.gradle +++ b/rsocket-test/build.gradle @@ -18,7 +18,6 @@ plugins { id 'java-library' id 'maven-publish' id 'signing' - id 'com.jfrog.artifactory' } dependencies { diff --git a/rsocket-transport-local/build.gradle b/rsocket-transport-local/build.gradle index 1f9f972f9..8c855f26c 100644 --- a/rsocket-transport-local/build.gradle +++ b/rsocket-transport-local/build.gradle @@ -18,7 +18,6 @@ plugins { id 'java-library' id 'maven-publish' id 'signing' - id 'com.jfrog.artifactory' } dependencies { diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index b03afbc6d..201d56bbf 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -18,7 +18,6 @@ plugins { id 'java-library' id 'maven-publish' id 'signing' - id 'com.jfrog.artifactory' id "com.google.osdetector" version "1.4.0" } From c0ae0f5f356c005bd81febfd653f02746174c13b Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 9 Jun 2021 11:20:07 +0300 Subject: [PATCH 052/116] adds bom to ghp releases Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- gradle/github-pkg.gradle | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/gradle/github-pkg.gradle b/gradle/github-pkg.gradle index 98a68ecdb..f53413766 100644 --- a/gradle/github-pkg.gradle +++ b/gradle/github-pkg.gradle @@ -1,23 +1,21 @@ subprojects { - plugins.withType(JavaLibraryPlugin) { - plugins.withType(MavenPublishPlugin) { - publishing { - repositories { - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/rsocket/rsocket-java") - credentials { - username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") - password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") - } + plugins.withType(MavenPublishPlugin) { + publishing { + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/rsocket/rsocket-java") + credentials { + username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") + password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") } } } + } - tasks.named("publish").configure { - onlyIf { System.getenv('SKIP_RELEASE') != "true" } - } + tasks.named("publish").configure { + onlyIf { System.getenv('SKIP_RELEASE') != "true" } } } } \ No newline at end of file From b12d46c94eac3f9b5fe596079973e41f0642da15 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 9 Jun 2021 11:46:58 +0300 Subject: [PATCH 053/116] bumps versions Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- README.md | 12 ++++++------ gradle.properties | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3c7e87976..cda6d3c0a 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=develop)](https://travis-ci.org/rsocket/rsocket-java) +[![Main Branches Java CI](https://github.com/rsocket/rsocket-java/actions/workflows/gradle-main.yml/badge.svg?branch=1.0.x)](https://github.com/rsocket/rsocket-java/actions/workflows/gradle-main.yml) Releases and milestones are available via Maven Central. @@ -27,8 +27,8 @@ repositories { maven { url 'https://repo.spring.io/milestone' } // Reactor milestones (if needed) } dependencies { - implementation 'io.rsocket:rsocket-core:1.0.4' - implementation 'io.rsocket:rsocket-transport-netty:1.0.4' + implementation 'io.rsocket:rsocket-core:1.0.5' + implementation 'io.rsocket:rsocket-transport-netty:1.0.5' } ``` @@ -38,12 +38,12 @@ Example: ```groovy repositories { - maven { url 'https://oss.jfrog.org/oss-snapshot-local' } + maven { url 'https://maven.pkg.github.com/rsocket/rsocket-java' } maven { url 'https://repo.spring.io/snapshot' } // Reactor snapshots (if needed) } dependencies { - implementation 'io.rsocket:rsocket-core:1.0.5-SNAPSHOT' - implementation 'io.rsocket:rsocket-transport-netty:1.0.5-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:1.0.6-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-netty:1.0.6-SNAPSHOT' } ``` diff --git a/gradle.properties b/gradle.properties index f75f86589..4f89f4d88 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.5 -perfBaselineVersion=1.0.4 +version=1.0.6 +perfBaselineVersion=1.0.5 From f02867d1fda2f2050a35191c468aba764c64ec1d Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Wed, 9 Jun 2021 14:15:14 +0300 Subject: [PATCH 054/116] sets jcstress mode to quick --- rsocket-core/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index 71e64b04e..3d4759af0 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -46,7 +46,7 @@ dependencies { } jcstress { - mode = 'default' //quick, default, tough + mode = 'quick' //quick, default, tough jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.7" } @@ -56,4 +56,4 @@ jar { } } -description = "Core functionality for the RSocket library" \ No newline at end of file +description = "Core functionality for the RSocket library" From a87abdfb883a49e815b000d2ddeb493e3deb0966 Mon Sep 17 00:00:00 2001 From: Andrii Rodionov Date: Thu, 12 Aug 2021 18:03:56 +0300 Subject: [PATCH 055/116] fixes typo in LeaseSpec initialisation (#1024) --- rsocket-core/src/main/java/io/rsocket/core/LeaseSpec.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/LeaseSpec.java b/rsocket-core/src/main/java/io/rsocket/core/LeaseSpec.java index 3947f296a..ad4b36e3a 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/LeaseSpec.java +++ b/rsocket-core/src/main/java/io/rsocket/core/LeaseSpec.java @@ -38,7 +38,7 @@ public LeaseSpec sender(LeaseSender sender) { * no leases is available */ public LeaseSpec maxPendingRequests(int maxPendingRequests) { - this.maxPendingRequests = 0; + this.maxPendingRequests = maxPendingRequests; return this; } } From 1f7191456f2961d1d29f682fb609c7d0783ef9a2 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 9 Nov 2021 13:31:05 +0200 Subject: [PATCH 056/116] relaxes connection dispose to avoid dropped error Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/transport/netty/TcpDuplexConnection.java | 1 - .../io/rsocket/transport/netty/WebsocketDuplexConnection.java | 1 - 2 files changed, 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 f9ac705b1..85874f44d 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 @@ -64,7 +64,6 @@ public SocketAddress remoteAddress() { @Override protected void doOnClose() { - sender.dispose(); connection.dispose(); } 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 c81f040da..140cfc59f 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 @@ -69,7 +69,6 @@ public SocketAddress remoteAddress() { @Override protected void doOnClose() { - sender.dispose(); connection.dispose(); } From efd1269a149e14e1a4b6cfeacea5ccd740fd219c Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 9 Nov 2021 23:27:46 +0200 Subject: [PATCH 057/116] adds first frame handling timeout (#1027) Co-authored-by: Rossen Stoyanchev --- .github/workflows/gradle-all.yml | 5 +++- .../java/io/rsocket/core/RSocketServer.java | 30 +++++++++++++++---- .../java/io/rsocket/core/ServerSetup.java | 13 ++++++++ .../core/SetupHandlingDuplexConnection.java | 1 + .../io/rsocket/core/RSocketServerTest.java | 28 +++++++++++++++++ .../java/io/rsocket/test/TransportTest.java | 21 +++++++++++-- 6 files changed, 89 insertions(+), 9 deletions(-) diff --git a/.github/workflows/gradle-all.yml b/.github/workflows/gradle-all.yml index 03e6a4e68..8826f511a 100644 --- a/.github/workflows/gradle-all.yml +++ b/.github/workflows/gradle-all.yml @@ -142,7 +142,10 @@ jobs: run: chmod +x gradlew - name: Publish Packages to Artifactory if: ${{ matrix.jdk == '1.8' }} - run: ./gradlew -PversionSuffix="-${githubRef#refs/heads/}-SNAPSHOT" -PbuildNumber="${buildNumber}" publishMavenPublicationToGitHubPackagesRepository --no-daemon --stacktrace + run: | + githubRef="${githubRef#refs/heads/}" + githubRef="${githubRef////-}" + ./gradlew -PversionSuffix="-${githubRef}-SNAPSHOT" -PbuildNumber="${buildNumber}" publishMavenPublicationToGitHubPackagesRepository --no-daemon --stacktrace env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} githubRef: ${{ github.ref }} 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 5ec33e76f..3208bb4fd 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -41,6 +41,7 @@ import io.rsocket.plugins.RequestInterceptor; import io.rsocket.resume.SessionManager; import io.rsocket.transport.ServerTransport; +import java.time.Duration; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Supplier; @@ -70,6 +71,7 @@ public final class RSocketServer { private int mtu = 0; private int maxInboundPayloadSize = Integer.MAX_VALUE; private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; + private Duration timeout = Duration.ofMinutes(1); private RSocketServer() {} @@ -223,6 +225,23 @@ public RSocketServer maxInboundPayloadSize(int maxInboundPayloadSize) { return this; } + /** + * Specify the max time to wait for the first frame (e.g. {@code SETUP}) on an accepted + * connection. + * + *

    By default this is set to 1 minute. + * + * @param timeout duration + * @return the same instance for method chaining + */ + public RSocketServer maxTimeToFirstFrame(Duration timeout) { + if (timeout.isNegative() || timeout.isZero()) { + throw new IllegalArgumentException("Setup Handling Timeout should be greater than zero"); + } + this.timeout = timeout; + return this; + } + /** * When this is set, frames larger than the given maximum transmission unit (mtu) size value are * fragmented. @@ -287,7 +306,7 @@ public RSocketServer payloadDecoder(PayloadDecoder decoder) { public Mono bind(ServerTransport transport) { return Mono.defer( new Supplier>() { - final ServerSetup serverSetup = serverSetup(); + final ServerSetup serverSetup = serverSetup(timeout); @Override public Mono get() { @@ -326,7 +345,7 @@ public ServerTransport.ConnectionAcceptor asConnectionAcceptor() { public ServerTransport.ConnectionAcceptor asConnectionAcceptor(int maxFrameLength) { assertValidateSetup(maxFrameLength, maxInboundPayloadSize, mtu); return new ServerTransport.ConnectionAcceptor() { - private final ServerSetup serverSetup = serverSetup(); + private final ServerSetup serverSetup = serverSetup(timeout); @Override public Mono apply(DuplexConnection connection) { @@ -469,12 +488,13 @@ private Mono acceptSetup( }); } - private ServerSetup serverSetup() { - return resume != null ? createSetup() : new ServerSetup.DefaultServerSetup(); + private ServerSetup serverSetup(Duration timeout) { + return resume != null ? createSetup(timeout) : new ServerSetup.DefaultServerSetup(timeout); } - ServerSetup createSetup() { + ServerSetup createSetup(Duration timeout) { return new ServerSetup.ResumableServerSetup( + timeout, 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 e716b8fcb..2d367bd73 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java @@ -36,9 +36,16 @@ abstract class ServerSetup { + final Duration timeout; + + protected ServerSetup(Duration timeout) { + this.timeout = timeout; + } + Mono> init(DuplexConnection connection) { return Mono.>create( sink -> sink.onRequest(__ -> new SetupHandlingDuplexConnection(connection, sink))) + .timeout(this.timeout) .or(connection.onClose().then(Mono.error(ClosedChannelException::new))); } @@ -57,6 +64,10 @@ void sendError(DuplexConnection duplexConnection, RSocketErrorException exceptio static class DefaultServerSetup extends ServerSetup { + DefaultServerSetup(Duration timeout) { + super(timeout); + } + @Override public Mono acceptRSocketSetup( ByteBuf frame, @@ -86,11 +97,13 @@ static class ResumableServerSetup extends ServerSetup { private final boolean cleanupStoreOnKeepAlive; ResumableServerSetup( + Duration timeout, SessionManager sessionManager, Duration resumeSessionDuration, Duration resumeStreamTimeout, Function resumeStoreFactory, boolean cleanupStoreOnKeepAlive) { + super(timeout); this.sessionManager = sessionManager; this.resumeSessionDuration = resumeSessionDuration; this.resumeStreamTimeout = resumeStreamTimeout; diff --git a/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java index b6bc87513..2da572de3 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java @@ -96,6 +96,7 @@ public void request(long n) { @Override public void cancel() { + source.dispose(); s.cancel(); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java index 08555740c..24bf95215 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java @@ -29,11 +29,13 @@ import io.rsocket.test.util.TestServerTransport; import java.time.Duration; import java.util.Random; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import reactor.core.Scannable; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; import reactor.test.StepVerifier; +import reactor.test.scheduler.VirtualTimeScheduler; public class RSocketServerTest { @@ -60,6 +62,32 @@ public void unexpectedFramesBeforeSetupFrame() { .hasNoLeaks(); } + @Test + public void timeoutOnNoFirstFrame() { + final VirtualTimeScheduler scheduler = VirtualTimeScheduler.getOrSet(); + try { + TestServerTransport transport = new TestServerTransport(); + RSocketServer.create().maxTimeToFirstFrame(Duration.ofMinutes(2)).bind(transport).block(); + + final TestDuplexConnection duplexConnection = transport.connect(); + + scheduler.advanceTimeBy(Duration.ofMinutes(1)); + + Assertions.assertThat(duplexConnection.isDisposed()).isFalse(); + + scheduler.advanceTimeBy(Duration.ofMinutes(1)); + + StepVerifier.create(duplexConnection.onClose()) + .expectSubscription() + .expectComplete() + .verify(Duration.ofSeconds(10)); + + FrameAssert.assertThat(duplexConnection.pollFrame()).isNull(); + } finally { + VirtualTimeScheduler.reset(); + } + } + @Test public void ensuresMaxFrameLengthCanNotBeLessThenMtu() { RSocketServer.create() 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 5384c7e8d..902a5844c 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -39,6 +39,7 @@ import java.io.InputStreamReader; import java.net.SocketAddress; import java.time.Duration; +import java.util.Arrays; import java.util.concurrent.CancellationException; import java.util.concurrent.Executors; import java.util.concurrent.ThreadLocalRandom; @@ -96,7 +97,18 @@ default void close() { getTransportPair().responder.awaitAllInteractionTermination(getTimeout()); getTransportPair().dispose(); getTransportPair().awaitClosed(); - RuntimeException throwable = new RuntimeException(); + RuntimeException throwable = + new RuntimeException() { + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + + @Override + public String getMessage() { + return Arrays.toString(getSuppressed()); + } + }; try { getTransportPair().byteBufAllocator2.assertHasNoLeaks(); @@ -776,8 +788,11 @@ public void onSubscribe(Subscription s) { @Override public void onNext(ByteBuf buf) { - actual.onNext(buf); - buf.release(); + try { + actual.onNext(buf); + } finally { + buf.release(); + } } Mono onClose() { From 1927bf40164f99cb7bcedd9bde79548193f71aca Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Sat, 13 Nov 2021 14:57:03 +0200 Subject: [PATCH 058/116] bumps libs versions and provides few UnboundedProcessor fixes (#1028) --- build.gradle | 25 ++++++++++-------- gradle/wrapper/gradle-wrapper.properties | 2 +- .../rsocket/internal/UnboundedProcessor.java | 11 +++----- .../rsocket/resume/ClientRSocketSession.java | 18 ++++++++++++- .../resume/ResumableDuplexConnection.java | 2 +- .../java/io/rsocket/test/TransportTest.java | 26 ++++++++++++------- 6 files changed, 53 insertions(+), 31 deletions(-) diff --git a/build.gradle b/build.gradle index e8dbaedd0..5be434a4a 100644 --- a/build.gradle +++ b/build.gradle @@ -16,10 +16,11 @@ plugins { id 'com.github.sherter.google-java-format' version '0.9' apply false - id 'me.champeau.jmh' version '0.6.4' apply false + id 'me.champeau.jmh' version '0.6.6' apply false id 'io.spring.dependency-management' version '1.0.11.RELEASE' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false - id "io.github.reyerizo.gradle.jcstress" version "0.8.11" apply false + id 'io.github.reyerizo.gradle.jcstress' version '0.8.11' apply false + id 'com.github.vlsi.gradle-extensions' version '1.76' apply false } boolean isCiServer = ["CI", "CONTINUOUS_INTEGRATION", "TRAVIS", "CIRCLECI", "bamboo_planKey", "GITHUB_ACTION"].with { @@ -30,19 +31,20 @@ boolean isCiServer = ["CI", "CONTINUOUS_INTEGRATION", "TRAVIS", "CIRCLECI", "bam subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' + apply plugin: 'com.github.vlsi.gradle-extensions' - ext['reactor-bom.version'] = '2020.0.7' + ext['reactor-bom.version'] = '2020.0.12' ext['logback.version'] = '1.2.3' - ext['netty-bom.version'] = '4.1.64.Final' - ext['netty-boringssl.version'] = '2.0.39.Final' + ext['netty-bom.version'] = '4.1.70.Final' + ext['netty-boringssl.version'] = '2.0.45.Final' ext['hdrhistogram.version'] = '2.1.12' - ext['mockito.version'] = '3.10.0' + ext['mockito.version'] = '3.12.4' ext['slf4j.version'] = '1.7.30' - ext['jmh.version'] = '1.31' - ext['junit.version'] = '5.7.2' + ext['jmh.version'] = '1.33' + ext['junit.version'] = '5.8.1' ext['hamcrest.version'] = '1.3' - ext['micrometer.version'] = '1.6.7' - ext['assertj.version'] = '3.19.0' + ext['micrometer.version'] = '1.7.5' + ext['assertj.version'] = '3.21.0' ext['netflix.limits.version'] = '0.3.6' ext['bouncycastle-bcpkix.version'] = '1.68' @@ -123,6 +125,7 @@ subprojects { } plugins.withType(JavaPlugin) { + compileJava { sourceCompatibility = 1.8 @@ -198,7 +201,7 @@ subprojects { if (JavaVersion.current().isJava9Compatible()) { println "Java 9+: lowering MaxGCPauseMillis to 20ms in ${project.name} ${name}" println "Java 9+: enabling leak detection [ADVANCED]" - jvmArgs = ["-XX:MaxGCPauseMillis=20", "-Dio.netty.leakDetection.level=ADVANCED"] + jvmArgs = ["-XX:MaxGCPauseMillis=20", "-Dio.netty.leakDetection.level=ADVANCED", "-Dio.netty.leakDetection.samplingInterval=32"] } systemProperty("java.awt.headless", "true") diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0f80bbf51..e750102e0 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-7.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists 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 c3278a09c..c529b615d 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -146,10 +146,7 @@ public void onNextPrioritized(ByteBuf t) { return; } - if (isWorkInProgress(previousState) - || isCancelled(previousState) - || isDisposed(previousState) - || isTerminated(previousState)) { + if (isWorkInProgress(previousState)) { return; } @@ -185,10 +182,7 @@ public void onNext(ByteBuf t) { return; } - if (isWorkInProgress(previousState) - || isCancelled(previousState) - || isDisposed(previousState) - || isTerminated(previousState)) { + if (isWorkInProgress(previousState)) { return; } @@ -449,6 +443,7 @@ public void subscribe(CoreSubscriber actual) { if (isCancelled(previousState)) { clearAndFinalize(this); + return; } if (isDisposed(previousState)) { 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 8dcf67a0b..2f2f29001 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -36,6 +36,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.CoreSubscriber; +import reactor.core.Disposable; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; import reactor.util.function.Tuple2; @@ -58,6 +59,7 @@ public class ClientRSocketSession final boolean cleanupStoreOnKeepAlive; final ByteBuf resumeToken; final String session; + final Disposable reconnectDisposable; volatile Subscription s; static final AtomicReferenceFieldUpdater S = @@ -110,10 +112,22 @@ public ClientRSocketSession( this.resumableConnection = resumableDuplexConnection; resumableDuplexConnection.onClose().doFinally(__ -> dispose()).subscribe(); - resumableDuplexConnection.onActiveConnectionClosed().subscribe(this::reconnect); + + this.reconnectDisposable = + resumableDuplexConnection.onActiveConnectionClosed().subscribe(this::reconnect); } void reconnect(int index) { + if (this.s == Operators.cancelledSubscription()) { + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. Connection[{}] is lost. Reconnecting rejected since session is closed", + session, + index); + } + return; + } + keepAliveSupport.stop(); if (logger.isDebugEnabled()) { logger.debug( @@ -147,6 +161,8 @@ public void onImpliedPosition(long remoteImpliedPos) { @Override public void dispose() { Operators.terminate(S, this); + + reconnectDisposable.dispose(); resumableConnection.dispose(); resumableFramesStore.dispose(); 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 933ac09ca..7ade3e59b 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -253,7 +253,7 @@ void dispose(@Nullable Throwable e) { framesSaverDisposable.dispose(); activeReceivingSubscriber.dispose(); - savableFramesSender.dispose(); + savableFramesSender.onComplete(); onConnectionClosedSink.tryEmitComplete(); if (e != null) { 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 902a5844c..570a7de2f 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -57,6 +57,7 @@ import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Disposable; +import reactor.core.Disposables; import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.publisher.Flux; @@ -641,7 +642,11 @@ public String expectedPayloadMetadata() { } public void awaitClosed() { - server.onClose().and(client.onClose()).block(Duration.ofMinutes(1)); + server + .onClose() + .onErrorResume(__ -> Mono.empty()) + .and(client.onClose().onErrorResume(__ -> Mono.empty())) + .block(Duration.ofMinutes(1)); } private static class AsyncDuplexConnection implements DuplexConnection { @@ -706,6 +711,7 @@ private static class DisconnectingDuplexConnection implements DuplexConnection { private final String tag; final DuplexConnection source; final Duration delay; + final Disposable.Swap disposables = Disposables.swap(); DisconnectingDuplexConnection(String tag, DuplexConnection source, Duration delay) { this.tag = tag; @@ -715,6 +721,7 @@ private static class DisconnectingDuplexConnection implements DuplexConnection { @Override public void dispose() { + disposables.dispose(); source.dispose(); } @@ -743,14 +750,15 @@ public Flux receive() { bb -> { if (!receivedFirst) { receivedFirst = true; - Mono.delay(delay) - .takeUntilOther(source.onClose()) - .subscribe( - __ -> { - logger.warn( - "Tag {}. Disposing Connection[{}]", tag, source.hashCode()); - source.dispose(); - }); + disposables.replace( + Mono.delay(delay) + .takeUntilOther(source.onClose()) + .subscribe( + __ -> { + logger.warn( + "Tag {}. Disposing Connection[{}]", tag, source.hashCode()); + source.dispose(); + })); } }); } From 16fdb87bada4050c0ff470b50062c48a87ba7e10 Mon Sep 17 00:00:00 2001 From: olme04 <86063649+olme04@users.noreply.github.com> Date: Sat, 13 Nov 2021 16:17:38 +0300 Subject: [PATCH 059/116] eliminate boxing in RequesterResponderSupport when using IntObjectMap (#1029) --- .../core/RequesterResponderSupport.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java b/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java index 52db6e198..bea7dc1aa 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java @@ -7,6 +7,7 @@ import io.rsocket.RSocket; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.plugins.RequestInterceptor; +import java.util.Objects; import java.util.function.Function; import reactor.util.annotation.Nullable; @@ -118,9 +119,14 @@ public int addAndGetNextStreamId(FrameHandler frameHandler) { } public synchronized boolean add(int streamId, FrameHandler frameHandler) { - final FrameHandler previousHandler = this.activeStreams.putIfAbsent(streamId, frameHandler); - - return previousHandler == null; + final IntObjectMap activeStreams = this.activeStreams; + // copy of Map.putIfAbsent(key, value) without `streamId` boxing + final FrameHandler previousHandler = activeStreams.get(streamId); + if (previousHandler == null) { + activeStreams.put(streamId, frameHandler); + return true; + } + return false; } /** @@ -143,6 +149,13 @@ public synchronized FrameHandler get(int streamId) { * instance equals to the passed one */ public synchronized boolean remove(int streamId, FrameHandler frameHandler) { - return this.activeStreams.remove(streamId, frameHandler); + final IntObjectMap activeStreams = this.activeStreams; + // copy of Map.remove(key, value) without `streamId` boxing + final FrameHandler curValue = activeStreams.get(streamId); + if (!Objects.equals(curValue, frameHandler)) { + return false; + } + activeStreams.remove(streamId); + return true; } } From 4fba9d4e72bea4fab64a59ed9329e02b185bdd7d Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 25 Jan 2022 14:50:19 +0200 Subject: [PATCH 060/116] adds tests for WeightedLoadbalanceStrategy Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .../rsocket/loadbalance/FrugalQuantile.java | 11 +- .../java/io/rsocket/loadbalance/Median.java | 9 + .../WeightedLoadbalanceStrategy.java | 5 +- .../WeightedLoadbalanceStrategyTest.java | 237 ++++++++++++++++++ 4 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 rsocket-core/src/test/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategyTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/FrugalQuantile.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/FrugalQuantile.java index a15d88529..cdbdc19b3 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/FrugalQuantile.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/FrugalQuantile.java @@ -65,7 +65,8 @@ public synchronized void insert(double x) { estimate = x; sign = 1; } else { - double v = rnd.nextDouble(); + final double v = rnd.nextDouble(); + final double estimate = this.estimate; if (x > estimate && v > (1 - quantile)) { higher(x); @@ -76,6 +77,8 @@ public synchronized void insert(double x) { } private void higher(double x) { + double estimate = this.estimate; + step += sign * increment; if (step > 0) { @@ -94,9 +97,13 @@ private void higher(double x) { } sign = 1; + + this.estimate = estimate; } private void lower(double x) { + double estimate = this.estimate; + step -= sign * increment; if (step > 0) { @@ -115,6 +122,8 @@ private void lower(double x) { } sign = -1; + + this.estimate = estimate; } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/Median.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/Median.java index 42b125b41..5319706f9 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/Median.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/Median.java @@ -33,6 +33,7 @@ public synchronized void insert(double x) { estimate = x; sign = 1; } else { + final double estimate = this.estimate; if (x > estimate) { greaterThanZero(x); } else if (x < estimate) { @@ -42,6 +43,8 @@ public synchronized void insert(double x) { } private void greaterThanZero(double x) { + double estimate = this.estimate; + step += sign; if (step > 0) { @@ -60,9 +63,13 @@ private void greaterThanZero(double x) { } sign = 1; + + this.estimate = estimate; } private void lessThanZero(double x) { + double estimate = this.estimate; + step -= sign; if (step > 0) { @@ -81,6 +88,8 @@ private void lessThanZero(double x) { } sign = -1; + + this.estimate = estimate; } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java index c401818f9..c30c8ad6b 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java @@ -124,7 +124,10 @@ public RSocket select(List sockets) { private static double algorithmicWeight( RSocket rSocket, @Nullable final WeightedStats weightedStats) { - if (weightedStats == null || rSocket.isDisposed() || rSocket.availability() == 0.0) { + if (weightedStats == null) { + return 1.0; + } + if (rSocket.isDisposed() || rSocket.availability() == 0.0) { return 0.0; } final int pending = weightedStats.pending(); diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategyTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategyTest.java new file mode 100644 index 000000000..6640aea4e --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategyTest.java @@ -0,0 +1,237 @@ +package io.rsocket.loadbalance; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.RaceTestConstants; +import io.rsocket.core.RSocketConnector; +import io.rsocket.transport.ClientTransport; +import io.rsocket.util.Clock; +import io.rsocket.util.EmptyPayload; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.assertj.core.api.Assertions; +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; +import reactor.test.publisher.TestPublisher; + +public class WeightedLoadbalanceStrategyTest { + + @BeforeEach + void setUp() { + Hooks.onErrorDropped((__) -> {}); + } + + @AfterAll + static void afterAll() { + Hooks.resetOnErrorDropped(); + } + + @Test + public void allRequestsShouldGoToTheSocketWithHigherWeight() { + final AtomicInteger counter1 = new AtomicInteger(); + final AtomicInteger counter2 = new AtomicInteger(); + final ClientTransport mockTransport = Mockito.mock(ClientTransport.class); + final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + final WeightedTestRSocket rSocket1 = + new WeightedTestRSocket( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + counter1.incrementAndGet(); + return Mono.empty(); + } + }); + final WeightedTestRSocket rSocket2 = + new WeightedTestRSocket( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + counter2.incrementAndGet(); + return Mono.empty(); + } + }); + Mockito.when(rSocketConnectorMock.connect(Mockito.any(ClientTransport.class))) + .then(im -> Mono.just(rSocket1)) + .then(im -> Mono.just(rSocket2)); + + final TestPublisher> source = TestPublisher.create(); + final RSocketPool rSocketPool = + new RSocketPool( + rSocketConnectorMock, + source, + WeightedLoadbalanceStrategy.builder() + .weightedStatsResolver(r -> r instanceof WeightedStats ? (WeightedStats) r : null) + .build()); + + for (int j = 0; j < RaceTestConstants.REPEATS; j++) { + rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); + } + + source.next( + Arrays.asList( + LoadbalanceTarget.from("1", mockTransport), + LoadbalanceTarget.from("2", mockTransport))); + + Assertions.assertThat(counter1.get()).isCloseTo(1000, Offset.offset(1)); + Assertions.assertThat(counter2.get()).isCloseTo(0, Offset.offset(1)); + } + + @Test + public void shouldDeliverValuesToTheSocketWithTheHighestCalculatedWeight() { + final AtomicInteger counter1 = new AtomicInteger(); + final AtomicInteger counter2 = new AtomicInteger(); + final ClientTransport mockTransport1 = Mockito.mock(ClientTransport.class); + final ClientTransport mockTransport2 = Mockito.mock(ClientTransport.class); + final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + final WeightedTestRSocket rSocket1 = + new WeightedTestRSocket( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + counter1.incrementAndGet(); + return Mono.empty(); + } + }); + final WeightedTestRSocket rSocket2 = + new WeightedTestRSocket( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + counter1.incrementAndGet(); + return Mono.empty(); + } + }); + final WeightedTestRSocket rSocket3 = + new WeightedTestRSocket( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + counter2.incrementAndGet(); + return Mono.empty(); + } + }); + + Mockito.when(rSocketConnectorMock.connect(Mockito.any(ClientTransport.class))) + .then(im -> Mono.just(rSocket1)) + .then(im -> Mono.just(rSocket2)) + .then(im -> Mono.just(rSocket3)); + + final TestPublisher> source = TestPublisher.create(); + final RSocketPool rSocketPool = + new RSocketPool( + rSocketConnectorMock, + source, + WeightedLoadbalanceStrategy.builder() + .weightedStatsResolver(r -> r instanceof WeightedStats ? (WeightedStats) r : null) + .build()); + + for (int j = 0; j < RaceTestConstants.REPEATS; j++) { + rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); + } + + source.next(Collections.singletonList(LoadbalanceTarget.from("1", mockTransport1))); + + Assertions.assertThat(counter1.get()).isCloseTo(RaceTestConstants.REPEATS, Offset.offset(1)); + + source.next(Collections.emptyList()); + + for (int j = 0; j < RaceTestConstants.REPEATS; j++) { + rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); + } + + rSocket1.updateAvailability(0.0); + + source.next(Collections.singletonList(LoadbalanceTarget.from("1", mockTransport1))); + + Assertions.assertThat(counter1.get()) + .isCloseTo(RaceTestConstants.REPEATS * 2, Offset.offset(1)); + + source.next( + Arrays.asList( + LoadbalanceTarget.from("1", mockTransport1), + LoadbalanceTarget.from("2", mockTransport2))); + + for (int j = 0; j < RaceTestConstants.REPEATS; j++) { + final RSocket rSocket = rSocketPool.select(); + rSocket.fireAndForget(EmptyPayload.INSTANCE).subscribe(); + } + + Assertions.assertThat(counter1.get()) + .isCloseTo(RaceTestConstants.REPEATS * 3, Offset.offset(100)); + Assertions.assertThat(counter2.get()).isCloseTo(0, Offset.offset(100)); + + rSocket2.updateAvailability(0.0); + + source.next(Collections.singletonList(LoadbalanceTarget.from("2", mockTransport1))); + + for (int j = 0; j < RaceTestConstants.REPEATS; j++) { + rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); + } + + Assertions.assertThat(counter1.get()) + .isCloseTo(RaceTestConstants.REPEATS * 3, Offset.offset(100)); + Assertions.assertThat(counter2.get()).isCloseTo(RaceTestConstants.REPEATS, Offset.offset(100)); + + source.next( + Arrays.asList( + LoadbalanceTarget.from("1", mockTransport1), + LoadbalanceTarget.from("2", mockTransport2))); + + for (int j = 0; j < RaceTestConstants.REPEATS; j++) { + final RSocket rSocket = rSocketPool.select(); + rSocket.fireAndForget(EmptyPayload.INSTANCE).subscribe(); + } + + Assertions.assertThat(counter1.get()) + .isCloseTo(RaceTestConstants.REPEATS * 3, Offset.offset(100)); + Assertions.assertThat(counter2.get()) + .isCloseTo(RaceTestConstants.REPEATS * 2, Offset.offset(100)); + } + + static class WeightedTestRSocket extends BaseWeightedStats implements RSocket { + + final Sinks.Empty sink = Sinks.empty(); + + final RSocket rSocket; + + public WeightedTestRSocket(RSocket rSocket) { + this.rSocket = rSocket; + } + + @Override + public Mono fireAndForget(Payload payload) { + startRequest(); + final long startTime = Clock.now(); + return this.rSocket + .fireAndForget(payload) + .doFinally( + __ -> { + stopRequest(startTime); + record(Clock.now() - startTime); + updateAvailability(1.0); + }); + } + + @Override + public Mono onClose() { + return sink.asMono(); + } + + @Override + public void dispose() { + sink.tryEmitEmpty(); + } + + public RSocket source() { + return rSocket; + } + } +} From 21852cacfed67541683ec1e0e11c5b99adc217cc Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 25 Jan 2022 15:58:36 +0200 Subject: [PATCH 061/116] fixes test Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- build.gradle | 2 ++ rsocket-core/build.gradle | 1 + .../src/test/java/io/rsocket/core/ReconnectMonoTests.java | 2 ++ 3 files changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index 5be434a4a..f535c84f6 100644 --- a/build.gradle +++ b/build.gradle @@ -47,6 +47,7 @@ subprojects { ext['assertj.version'] = '3.21.0' ext['netflix.limits.version'] = '0.3.6' ext['bouncycastle-bcpkix.version'] = '1.68' + ext['awaitility.version'] = '4.1.1' group = "io.rsocket" @@ -80,6 +81,7 @@ subprojects { dependency "org.assertj:assertj-core:${ext['assertj.version']}" dependency "org.hdrhistogram:HdrHistogram:${ext['hdrhistogram.version']}" dependency "org.slf4j:slf4j-api:${ext['slf4j.version']}" + dependency "org.awaitility:awaitility:${ext['awaitility.version']}" dependencySet(group: 'org.mockito', version: ext['mockito.version']) { entry 'mockito-junit-jupiter' entry 'mockito-core' diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index 3d4759af0..ecd23296a 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -35,6 +35,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-params' testImplementation 'org.mockito:mockito-core' + testImplementation 'org.awaitility:awaitility' testRuntimeOnly 'ch.qos.logback:logback-classic' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' diff --git a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java index 672141eaa..3112a0943 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java @@ -17,6 +17,7 @@ package io.rsocket.core; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import io.rsocket.RaceTestConstants; import io.rsocket.internal.subscriber.AssertSubscriber; @@ -321,6 +322,7 @@ public String get() { assertThat(expired).hasSize(1).containsOnly("value_to_expire" + i); if (reconnectMono.resolvingInner.subscribers == ResolvingOperator.READY) { + await().atMost(Duration.ofSeconds(5)).until(() -> received.size() == 2); assertThat(received) .hasSize(2) .containsExactly( From 37fc68c68f4b61d826084330a7b0476a456b63da Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 25 Jan 2022 19:50:42 +0200 Subject: [PATCH 062/116] removes hamcrest from test dependencies Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- build.gradle | 2 - rsocket-core/build.gradle | 2 - .../io/rsocket/core/RSocketRequesterTest.java | 125 +++++++++--------- .../io/rsocket/core/RSocketResponderTest.java | 77 +++++------ .../io/rsocket/test/util/MockRSocket.java | 7 +- rsocket-examples/build.gradle | 1 - rsocket-load-balancer/build.gradle | 1 - .../io/rsocket/client/TimeoutClientTest.java | 8 +- 8 files changed, 107 insertions(+), 116 deletions(-) diff --git a/build.gradle b/build.gradle index f535c84f6..98298af9a 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,6 @@ subprojects { ext['slf4j.version'] = '1.7.30' ext['jmh.version'] = '1.33' ext['junit.version'] = '5.8.1' - ext['hamcrest.version'] = '1.3' ext['micrometer.version'] = '1.7.5' ext['assertj.version'] = '3.21.0' ext['netflix.limits.version'] = '0.3.6' @@ -86,7 +85,6 @@ subprojects { entry 'mockito-junit-jupiter' entry 'mockito-core' } - dependency "org.hamcrest:hamcrest-library:${ext['hamcrest.version']}" dependencySet(group: 'org.openjdk.jmh', version: ext['jmh.version']) { entry 'jmh-core' entry 'jmh-generator-annprocess' diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index ecd23296a..7a0ebfb32 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -40,8 +40,6 @@ dependencies { testRuntimeOnly 'ch.qos.logback:logback-classic' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' - testImplementation 'org.hamcrest:hamcrest-library' - jcstressImplementation(project(":rsocket-test")) jcstressImplementation "ch.qos.logback:logback-classic" } 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 f31b74800..6a1d07a67 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -32,10 +32,7 @@ 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; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @@ -131,7 +128,7 @@ public void tearDown() { @Timeout(2_000) public void testInvalidFrameOnStream0ShouldNotTerminateRSocket() { rule.connection.addToReceivedBuffer(RequestNFrameCodec.encode(rule.alloc(), 0, 10)); - Assertions.assertThat(rule.socket.isDisposed()).isFalse(); + assertThat(rule.socket.isDisposed()).isFalse(); rule.assertHasNoLeaks(); } @@ -149,19 +146,21 @@ protected void hookOnSubscribe(Subscription subscription) { }; stream.subscribe(subscriber); - Assertions.assertThat(rule.connection.getSent()).isEmpty(); + assertThat(rule.connection.getSent()).isEmpty(); subscriber.request(5); List sent = new ArrayList<>(rule.connection.getSent()); - assertThat("sent frame count", sent.size(), is(1)); + assertThat(sent.size()).describedAs("sent frame count").isEqualTo(1); ByteBuf f = sent.get(0); - assertThat("initial frame", frameType(f), is(REQUEST_STREAM)); - assertThat("initial request n", RequestStreamFrameCodec.initialRequestN(f), is(5L)); - assertThat("should be released", f.release(), is(true)); + assertThat(frameType(f)).describedAs("initial frame").isEqualTo(REQUEST_STREAM); + assertThat(RequestStreamFrameCodec.initialRequestN(f)) + .describedAs("initial request n") + .isEqualTo(5L); + assertThat(f.release()).describedAs("should be released").isEqualTo(true); rule.assertHasNoLeaks(); } @@ -189,7 +188,7 @@ public void testHandleApplicationException() { verify(responseSub).onError(any(ApplicationErrorException.class)); - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) // requestResponseFrame .hasSize(1) .allMatch(ReferenceCounted::release); @@ -210,7 +209,7 @@ public void testHandleValidFrame() { rule.alloc(), streamId, EmptyPayload.INSTANCE)); verify(sub).onComplete(); - Assertions.assertThat(rule.connection.getSent()).hasSize(1).allMatch(ReferenceCounted::release); + assertThat(rule.connection.getSent()).hasSize(1).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); } @@ -226,10 +225,13 @@ public void testRequestReplyWithCancel() { 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); + assertThat(frameType(sent.get(0))) + .describedAs("Unexpected frame sent on the connection.") + .isEqualTo(REQUEST_RESPONSE); + assertThat(frameType(sent.get(1))) + .describedAs("Unexpected frame sent on the connection.") + .isEqualTo(CANCEL); + assertThat(sent).hasSize(2).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); } @@ -282,7 +284,7 @@ public void testChannelRequestCancellation2() { Flux.error(new IllegalStateException("Channel request not cancelled")) .delaySubscription(Duration.ofSeconds(1))) .blockFirst(); - Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); } @@ -303,10 +305,9 @@ public void testChannelRequestServerSideCancellation() { .delaySubscription(Duration.ofSeconds(1))) .blockFirst(); - Assertions.assertThat( - request.scan(Scannable.Attr.TERMINATED) || request.scan(Scannable.Attr.CANCELLED)) + assertThat(request.scan(Scannable.Attr.TERMINATED) || request.scan(Scannable.Attr.CANCELLED)) .isTrue(); - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) .hasSize(1) .first() .matches(bb -> frameType(bb) == REQUEST_CHANNEL) @@ -336,14 +337,13 @@ protected void hookOnSubscribe(Subscription subscription) {} ByteBuf initialFrame = iterator.next(); - Assertions.assertThat(FrameHeaderCodec.frameType(initialFrame)).isEqualTo(REQUEST_CHANNEL); - Assertions.assertThat(RequestChannelFrameCodec.initialRequestN(initialFrame)) - .isEqualTo(Long.MAX_VALUE); - Assertions.assertThat(RequestChannelFrameCodec.data(initialFrame).toString(CharsetUtil.UTF_8)) + assertThat(FrameHeaderCodec.frameType(initialFrame)).isEqualTo(REQUEST_CHANNEL); + assertThat(RequestChannelFrameCodec.initialRequestN(initialFrame)).isEqualTo(Long.MAX_VALUE); + assertThat(RequestChannelFrameCodec.data(initialFrame).toString(CharsetUtil.UTF_8)) .isEqualTo("0"); - Assertions.assertThat(initialFrame.release()).isTrue(); + assertThat(initialFrame.release()).isTrue(); - Assertions.assertThat(iterator.hasNext()).isFalse(); + assertThat(iterator.hasNext()).isFalse(); rule.assertHasNoLeaks(); } @@ -364,7 +364,7 @@ public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmen .expectSubscription() .expectErrorSatisfies( t -> - Assertions.assertThat(t) + assertThat(t) .isInstanceOf(IllegalArgumentException.class) .hasMessage( String.format(INVALID_PAYLOAD_ERROR_MESSAGE, maxFrameLength))) @@ -406,11 +406,11 @@ static Stream>> prepareCalls() { }) .expectErrorSatisfies( t -> - Assertions.assertThat(t) + assertThat(t) .isInstanceOf(IllegalArgumentException.class) .hasMessage(String.format(INVALID_PAYLOAD_ERROR_MESSAGE, maxFrameLength))) .verify(); - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) // expect to be sent RequestChannelFrame // expect to be sent CancelFrame .hasSize(2) @@ -439,8 +439,7 @@ public void checkNoLeaksOnRacing( runner.accept(assertSubscriber, clientSocketRule); - Assertions.assertThat(clientSocketRule.connection.getSent()) - .allMatch(ReferenceCounted::release); + assertThat(clientSocketRule.connection.getSent()).allMatch(ReferenceCounted::release); clientSocketRule.assertHasNoLeaks(); } @@ -501,8 +500,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()) + assertThat(rule.connection.getSent()).hasSize(2); + assertThat(rule.connection.getSent()) .element(0) .matches( bb -> frameType(bb) == REQUEST_STREAM, @@ -511,7 +510,7 @@ private static Stream racingCases() { + "} but was {" + frameType(rule.connection.getSent().stream().findFirst().get()) + "}"); - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) .element(1) .matches( bb -> frameType(bb) == CANCEL, @@ -548,8 +547,8 @@ private static Stream racingCases() { int size = rule.connection.getSent().size(); if (size > 0) { - Assertions.assertThat(size).isLessThanOrEqualTo(3).isGreaterThanOrEqualTo(2); - Assertions.assertThat(rule.connection.getSent()) + assertThat(size).isLessThanOrEqualTo(3).isGreaterThanOrEqualTo(2); + assertThat(rule.connection.getSent()) .element(0) .matches( bb -> frameType(bb) == REQUEST_CHANNEL, @@ -559,7 +558,7 @@ private static Stream racingCases() { + frameType(rule.connection.getSent().stream().findFirst().get()) + "}"); if (size == 2) { - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) .element(1) .matches( bb -> frameType(bb) == CANCEL, @@ -570,7 +569,7 @@ private static Stream racingCases() { rule.connection.getSent().stream().skip(1).findFirst().get()) + "}"); } else { - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) .element(1) .matches( bb -> frameType(bb) == COMPLETE || frameType(bb) == CANCEL, @@ -582,7 +581,7 @@ private static Stream racingCases() { + frameType( rule.connection.getSent().stream().skip(1).findFirst().get()) + "}"); - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) .element(2) .matches( bb -> frameType(bb) == CANCEL || frameType(bb) == COMPLETE, @@ -720,7 +719,7 @@ public void simpleOnDiscardRequestChannelTest() { assertSubscriber.cancel(); - Assertions.assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); + assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); rule.assertHasNoLeaks(); } @@ -744,7 +743,7 @@ public void simpleOnDiscardRequestChannelTest2() { ErrorFrameCodec.encode( allocator, streamId, new CustomRSocketException(0x00000404, "test"))); - Assertions.assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); + assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); rule.assertHasNoLeaks(); } @@ -815,18 +814,18 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( testPublisher.next(ByteBufPayload.create("d" + i)); } - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) .describedAs( "Interaction Type :[%s]. Expected to observe %s frames sent", frameType, framesCnt) .hasSize(framesCnt) .allMatch(bb -> !FrameHeaderCodec.hasMetadata(bb)) .allMatch(ByteBuf::release); - Assertions.assertThat(assertSubscriber.isTerminated()) + assertThat(assertSubscriber.isTerminated()) .describedAs("Interaction Type :[%s]. Expected to be terminated", frameType) .isTrue(); - Assertions.assertThat(assertSubscriber.values()) + assertThat(assertSubscriber.values()) .describedAs( "Interaction Type :[%s]. Expected to observe %s frames received", frameType, responsesCnt) @@ -889,12 +888,12 @@ public void ensuresThatNoOpsMustHappenUntilSubscriptionInCaseOfFnfCall() { Payload payload2 = ByteBufPayload.create("abc2"); Mono fnf2 = rule.socket.fireAndForget(payload2); - Assertions.assertThat(rule.connection.getSent()).isEmpty(); + 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()) + assertThat(rule.connection.getSent()) .hasSize(1) .first() .matches(bb -> frameType(bb) == REQUEST_FNF) @@ -912,7 +911,7 @@ public void ensuresThatNoOpsMustHappenUntilSubscriptionInCaseOfFnfCall() { // 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()) + assertThat(rule.connection.getSent()) .hasSize(1) .first() .matches(bb -> frameType(bb) == REQUEST_FNF) @@ -936,7 +935,7 @@ public void ensuresThatNoOpsMustHappenUntilFirstRequestN( Payload payload2 = ByteBufPayload.create("abc2"); Publisher interaction2 = interaction.apply(rule, payload2); - Assertions.assertThat(rule.connection.getSent()).isEmpty(); + assertThat(rule.connection.getSent()).isEmpty(); AssertSubscriber assertSubscriber1 = AssertSubscriber.create(0); interaction1.subscribe(assertSubscriber1); @@ -945,12 +944,12 @@ public void ensuresThatNoOpsMustHappenUntilFirstRequestN( assertSubscriber1.assertNotTerminated().assertNoError(); assertSubscriber2.assertNotTerminated().assertNoError(); // even though we subscribed, nothing should happen until the first requestN - Assertions.assertThat(rule.connection.getSent()).isEmpty(); + 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()) + assertThat(rule.connection.getSent()) .hasSize(frameType == REQUEST_CHANNEL ? 2 : 1) .first() .matches(bb -> frameType(bb) == frameType) @@ -979,7 +978,7 @@ public void ensuresThatNoOpsMustHappenUntilFirstRequestN( .matches(ReferenceCounted::release); if (frameType == REQUEST_CHANNEL) { - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) .element(1) .matches(bb -> frameType(bb) == COMPLETE) .matches( @@ -993,7 +992,7 @@ public void ensuresThatNoOpsMustHappenUntilFirstRequestN( rule.connection.clearSendReceiveBuffers(); assertSubscriber1.request(1); - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) .hasSize(frameType == REQUEST_CHANNEL ? 2 : 1) .first() .matches(bb -> frameType(bb) == frameType) @@ -1022,7 +1021,7 @@ public void ensuresThatNoOpsMustHappenUntilFirstRequestN( .matches(ReferenceCounted::release); if (frameType == REQUEST_CHANNEL) { - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) .element(1) .matches(bb -> frameType(bb) == COMPLETE) .matches( @@ -1068,7 +1067,7 @@ public void ensuresCorrectOrderOfStreamIdIssuingInCaseOfRacing( () -> publisher1.subscribe(AssertSubscriber.create()), () -> publisher2.subscribe(AssertSubscriber.create())); - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) .extracting(FrameHeaderCodec::streamId) .containsExactly(i, i + 2); rule.connection.getSent().forEach(bb -> bb.release()); @@ -1180,11 +1179,11 @@ public void shouldTerminateAllStreamsIfThereRacingBetweenDisposeAndRequests( } } - Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.connection.getSent().clear(); - Assertions.assertThat(payload1.refCnt()).isZero(); - Assertions.assertThat(payload2.refCnt()).isZero(); + assertThat(payload1.refCnt()).isZero(); + assertThat(payload2.refCnt()).isZero(); } } @@ -1199,13 +1198,13 @@ public void testWorkaround858() { rule.connection.addToReceivedBuffer( ErrorFrameCodec.encode(rule.alloc(), 1, new RuntimeException("test"))); - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) .hasSize(1) .first() .matches(bb -> FrameHeaderCodec.frameType(bb) == REQUEST_RESPONSE) .matches(ByteBuf::release); - Assertions.assertThat(rule.socket.isDisposed()).isFalse(); + assertThat(rule.socket.isDisposed()).isFalse(); rule.assertHasNoLeaks(); } @@ -1382,9 +1381,9 @@ public void testWorkaround959(String type) { assertSubscriber.request(1); }); - Assertions.assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); + assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); - Assertions.assertThat(rule.socket.isDisposed()).isFalse(); + assertThat(rule.socket.isDisposed()).isFalse(); assertSubscriber.values().forEach(ReferenceCountUtil::safeRelease); assertSubscriber.assertNoError(); @@ -1412,7 +1411,9 @@ protected RSocketRequester newRSocket() { } public int getStreamIdForRequestType(FrameType expectedFrameType) { - assertThat("Unexpected frames sent.", connection.getSent(), hasSize(greaterThanOrEqualTo(1))); + assertThat(connection.getSent().size()) + .describedAs("Unexpected frames sent.") + .isGreaterThanOrEqualTo(1); List framesFound = new ArrayList<>(); for (ByteBuf frame : connection.getSent()) { FrameType frameType = frameType(frame); 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 4c44d827d..c0f64469c 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -34,10 +34,7 @@ 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; -import static org.hamcrest.Matchers.anyOf; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.is; +import static org.assertj.core.api.Assertions.assertThat; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -76,7 +73,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; -import org.assertj.core.api.Assertions; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -126,12 +122,13 @@ public void testHandleKeepAlive() { rule.connection.addToReceivedBuffer( KeepAliveFrameCodec.encode(rule.alloc(), true, 0, Unpooled.EMPTY_BUFFER)); ByteBuf sent = rule.connection.awaitFrame(); - assertThat("Unexpected frame sent.", frameType(sent), is(FrameType.KEEPALIVE)); + assertThat(frameType(sent)) + .describedAs("Unexpected frame sent.") + .isEqualTo(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.", - KeepAliveFrameCodec.respondFlag(sent), - is(false)); + assertThat(KeepAliveFrameCodec.respondFlag(sent)) + .describedAs("Unexpected keep-alive frame respond flag.") + .isEqualTo(false); } @Test @@ -149,10 +146,9 @@ public Mono requestResponse(Payload payload) { }); rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE); testPublisher.complete(); - assertThat( - "Unexpected frame sent.", - frameType(rule.connection.awaitFrame()), - anyOf(is(FrameType.COMPLETE), is(FrameType.NEXT_COMPLETE))); + assertThat(frameType(rule.connection.awaitFrame())) + .describedAs("Unexpected frame sent.") + .isIn(FrameType.COMPLETE, FrameType.NEXT_COMPLETE); testPublisher.assertWasNotCancelled(); } @@ -162,8 +158,9 @@ public void testHandlerEmitsError() { final int streamId = 4; rule.prefetch = 1; rule.sendRequest(streamId, FrameType.REQUEST_STREAM); - assertThat( - "Unexpected frame sent.", frameType(rule.connection.awaitFrame()), is(FrameType.ERROR)); + assertThat(frameType(rule.connection.awaitFrame())) + .describedAs("Unexpected frame sent.") + .isEqualTo(FrameType.ERROR); } @Test @@ -182,12 +179,12 @@ public Mono requestResponse(Payload payload) { }); rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE); - assertThat("Unexpected frame sent.", rule.connection.getSent(), is(empty())); + assertThat(rule.connection.getSent()).describedAs("Unexpected frame sent.").isEmpty(); rule.connection.addToReceivedBuffer(CancelFrameCodec.encode(allocator, streamId)); - assertThat("Unexpected frame sent.", rule.connection.getSent(), is(empty())); - assertThat("Subscription not cancelled.", cancelled.get(), is(true)); + assertThat(rule.connection.getSent()).describedAs("Unexpected frame sent.").isEmpty(); + assertThat(cancelled.get()).describedAs("Subscription not cancelled.").isTrue(); rule.assertHasNoLeaks(); } @@ -243,7 +240,7 @@ protected void hookOnSubscribe(Subscription subscription) { for (Runnable runnable : runnables) { rule.connection.clearSendReceiveBuffers(); runnable.run(); - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) .hasSize(1) .first() .matches(bb -> FrameHeaderCodec.frameType(bb) == FrameType.ERROR) @@ -253,7 +250,7 @@ protected void hookOnSubscribe(Subscription subscription) { .contains(String.format(INVALID_PAYLOAD_ERROR_MESSAGE, maxFrameLength))) .matches(ReferenceCounted::release); - assertThat("Subscription not cancelled.", cancelled.get(), is(true)); + assertThat(cancelled.get()).describedAs("Subscription not cancelled.").isTrue(); } rule.assertHasNoLeaks(); @@ -308,9 +305,9 @@ public Flux requestChannel(Publisher payloads) { sink.tryEmitEmpty(); }); - Assertions.assertThat(assertSubscriber.values()).allMatch(ReferenceCounted::release); + assertThat(assertSubscriber.values()).allMatch(ReferenceCounted::release); - Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); testRequestInterceptor.expectOnStart(1, REQUEST_CHANNEL).expectOnComplete(1).expectNothing(); @@ -353,7 +350,7 @@ public Flux requestChannel(Publisher payloads) { sink.complete(); }); - Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); testRequestInterceptor.expectOnStart(1, REQUEST_CHANNEL).expectOnCancel(1).expectNothing(); @@ -398,7 +395,7 @@ public Flux requestChannel(Publisher payloads) { sink.complete(); }); - Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); testRequestInterceptor.expectOnStart(1, REQUEST_CHANNEL).expectOnCancel(1).expectNothing(); rule.assertHasNoLeaks(); } @@ -483,13 +480,13 @@ public Flux requestChannel(Publisher payloads) { sink.error(new RuntimeException()); }); - Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); assertSubscriber .assertTerminated() .assertError(CancellationException.class) .assertErrorMessage("Outbound has terminated with an error"); - Assertions.assertThat(assertSubscriber.values()) + assertThat(assertSubscriber.values()) .allMatch( msg -> { ReferenceCountUtil.safeRelease(msg); @@ -531,7 +528,7 @@ public Flux requestStream(Payload payload) { sink.next(ByteBufPayload.create("d3", "m3")); }); - Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); @@ -573,7 +570,7 @@ public void subscribe(CoreSubscriber actual) { sources[0].complete(ByteBufPayload.create("d1", "m1")); }); - Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); @@ -581,7 +578,7 @@ public void subscribe(CoreSubscriber actual) { .expectOnStart(1, REQUEST_RESPONSE) .assertNext( e -> - Assertions.assertThat(e.eventType) + assertThat(e.eventType) .isIn( TestRequestInterceptor.EventType.ON_COMPLETE, TestRequestInterceptor.EventType.ON_CANCEL)) @@ -614,7 +611,7 @@ public Flux requestStream(Payload payload) { sink.next(ByteBufPayload.create("d3", "m3")); rule.connection.addToReceivedBuffer(cancelFrame); - Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); } @@ -660,7 +657,7 @@ public Flux requestChannel(Publisher payloads) { rule.connection.addToReceivedBuffer(cancelFrame); - Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); } @@ -730,8 +727,7 @@ public Flux requestChannel(Publisher payloads) { } if (responsesCnt > 0) { - Assertions.assertThat( - rule.connection.getSent().stream().filter(bb -> frameType(bb) != REQUEST_N)) + 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) @@ -739,8 +735,7 @@ public Flux requestChannel(Publisher payloads) { } if (framesCnt > 1) { - Assertions.assertThat( - rule.connection.getSent().stream().filter(bb -> frameType(bb) == REQUEST_N)) + 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) @@ -749,9 +744,9 @@ public Flux requestChannel(Publisher payloads) { .matches(bb -> RequestNFrameCodec.requestN(bb) == (framesCnt - 1)); } - Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); - Assertions.assertThat(assertSubscriber.awaitAndAssertNextValueCount(framesCnt).values()) + assertThat(assertSubscriber.awaitAndAssertNextValueCount(framesCnt).values()) .hasSize(framesCnt) .allMatch(p -> !p.hasMetadata()) .allMatch(ReferenceCounted::release); @@ -796,7 +791,7 @@ public Flux requestChannel(Publisher payloads) { rule.sendRequest(1, frameType); - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) .hasSize(1) .first() .matches( @@ -837,13 +832,13 @@ public Flux requestChannel(Publisher payloads) { rule.connection.addToReceivedBuffer( ErrorFrameCodec.encode(rule.alloc(), 1, new RuntimeException("test"))); - Assertions.assertThat(rule.connection.getSent()) + assertThat(rule.connection.getSent()) .hasSize(1) .first() .matches(bb -> FrameHeaderCodec.frameType(bb) == REQUEST_N) .matches(ReferenceCounted::release); - Assertions.assertThat(rule.socket.isDisposed()).isFalse(); + assertThat(rule.socket.isDisposed()).isFalse(); testPublisher.assertWasCancelled(); rule.assertHasNoLeaks(); diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/MockRSocket.java b/rsocket-core/src/test/java/io/rsocket/test/util/MockRSocket.java index 179afff58..a33c4c4b3 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/MockRSocket.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/MockRSocket.java @@ -16,8 +16,7 @@ package io.rsocket.test.util; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; +import static org.assertj.core.api.Assertions.assertThat; import io.rsocket.Payload; import io.rsocket.RSocket; @@ -116,6 +115,8 @@ public void assertMetadataPushCount(int expected) { } private static void assertCount(int expected, String type, AtomicInteger counter) { - assertThat("Unexpected invocations for " + type + '.', counter.get(), is(expected)); + assertThat(counter.get()) + .describedAs("Unexpected invocations for " + type + '.') + .isEqualTo(expected); } } diff --git a/rsocket-examples/build.gradle b/rsocket-examples/build.gradle index e5d74494f..d03524cd9 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -34,7 +34,6 @@ dependencies { testImplementation 'org.assertj:assertj-core' testImplementation 'io.projectreactor:reactor-test' - testImplementation 'org.hamcrest:hamcrest-library' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' } diff --git a/rsocket-load-balancer/build.gradle b/rsocket-load-balancer/build.gradle index c247486be..6d91324ae 100644 --- a/rsocket-load-balancer/build.gradle +++ b/rsocket-load-balancer/build.gradle @@ -32,7 +32,6 @@ dependencies { testImplementation 'org.assertj:assertj-core' testImplementation 'io.projectreactor:reactor-test' - testImplementation 'org.hamcrest:hamcrest-library' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' testRuntimeOnly 'ch.qos.logback:logback-classic' } diff --git a/rsocket-load-balancer/src/test/java/io/rsocket/client/TimeoutClientTest.java b/rsocket-load-balancer/src/test/java/io/rsocket/client/TimeoutClientTest.java index e6c8aa313..b8866b1f6 100644 --- a/rsocket-load-balancer/src/test/java/io/rsocket/client/TimeoutClientTest.java +++ b/rsocket-load-balancer/src/test/java/io/rsocket/client/TimeoutClientTest.java @@ -16,14 +16,13 @@ package io.rsocket.client; -import static org.hamcrest.Matchers.instanceOf; +import static org.assertj.core.api.Assertions.assertThat; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.client.filter.RSockets; import io.rsocket.util.EmptyPayload; import java.time.Duration; -import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Test; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -50,8 +49,9 @@ public void onNext(Payload payload) { @Override public void onError(Throwable t) { - MatcherAssert.assertThat( - "Unexpected exception in onError", t, instanceOf(TimeoutException.class)); + assertThat(t) + .describedAs("Unexpected exception in onError") + .isInstanceOf(TimeoutException.class); } @Override From bf0c60850b7b4a217082c1bf950a68dbe8954331 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 18 Mar 2022 11:37:28 +0200 Subject: [PATCH 063/116] migrates from deprecated api, updates dependencies Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- build.gradle | 20 +- rsocket-core/build.gradle | 5 +- .../io/rsocket/core/StressSubscriber.java | 9 + .../UnboundedProcessorStressTest.java | 304 +++++++++++++++++- .../core/RequestChannelRequesterFlux.java | 4 +- .../rsocket/internal/UnboundedProcessor.java | 43 +-- .../resume/ResumableDuplexConnection.java | 2 +- .../io/rsocket/core/RSocketRequesterTest.java | 2 +- .../rsocket/frame/ByteBufRepresentation.java | 11 +- .../org.junit.jupiter.api.extension.Extension | 1 + .../server/BaseWebsocketServerTransport.java | 5 +- .../server/WebsocketServerTransportTest.java | 2 +- 12 files changed, 350 insertions(+), 58 deletions(-) create mode 100644 rsocket-core/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension diff --git a/build.gradle b/build.gradle index 98298af9a..a3ff46a4f 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ plugins { id 'me.champeau.jmh' version '0.6.6' apply false id 'io.spring.dependency-management' version '1.0.11.RELEASE' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false - id 'io.github.reyerizo.gradle.jcstress' version '0.8.11' apply false + id 'io.github.reyerizo.gradle.jcstress' version '0.8.13' apply false id 'com.github.vlsi.gradle-extensions' version '1.76' apply false } @@ -33,19 +33,19 @@ subprojects { apply plugin: 'com.github.sherter.google-java-format' apply plugin: 'com.github.vlsi.gradle-extensions' - ext['reactor-bom.version'] = '2020.0.12' - ext['logback.version'] = '1.2.3' - ext['netty-bom.version'] = '4.1.70.Final' - ext['netty-boringssl.version'] = '2.0.45.Final' + ext['reactor-bom.version'] = '2020.0.17' + ext['logback.version'] = '1.2.10' + ext['netty-bom.version'] = '4.1.75.Final' + ext['netty-boringssl.version'] = '2.0.51.Final' ext['hdrhistogram.version'] = '2.1.12' - ext['mockito.version'] = '3.12.4' - ext['slf4j.version'] = '1.7.30' + ext['mockito.version'] = '4.4.0' + ext['slf4j.version'] = '1.7.36' ext['jmh.version'] = '1.33' ext['junit.version'] = '5.8.1' - ext['micrometer.version'] = '1.7.5' - ext['assertj.version'] = '3.21.0' + ext['micrometer.version'] = '1.8.4' + ext['assertj.version'] = '3.22.0' ext['netflix.limits.version'] = '0.3.6' - ext['bouncycastle-bcpkix.version'] = '1.68' + ext['bouncycastle-bcpkix.version'] = '1.70' ext['awaitility.version'] = '4.1.1' group = "io.rsocket" diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index 7a0ebfb32..cd8595216 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2022 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. @@ -42,11 +42,12 @@ dependencies { jcstressImplementation(project(":rsocket-test")) jcstressImplementation "ch.qos.logback:logback-classic" + jcstressImplementation 'io.projectreactor:reactor-test' } jcstress { mode = 'quick' //quick, default, tough - jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.7" + jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.15" } jar { diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscriber.java b/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscriber.java index 31fd44374..883077f77 100644 --- a/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscriber.java +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscriber.java @@ -224,6 +224,11 @@ public void onError(Throwable throwable) { } else { GUARD.compareAndSet(this, Operation.ON_ERROR, null); } + + if (done) { + throw new IllegalStateException("Already done"); + } + error = throwable; done = true; q.offer(throwable); @@ -241,6 +246,10 @@ public void onComplete() { } else { GUARD.compareAndSet(this, Operation.ON_COMPLETE, null); } + if (done) { + throw new IllegalStateException("Already done"); + } + done = true; ON_COMPLETE_CALLS.incrementAndGet(this); diff --git a/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java index 39ed2e4cb..bdbdc7a3b 100644 --- a/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java +++ b/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java @@ -13,9 +13,14 @@ import org.openjdk.jcstress.infra.results.LLL_Result; import org.openjdk.jcstress.infra.results.L_Result; import reactor.core.Fuseable; +import reactor.core.publisher.Hooks; public abstract class UnboundedProcessorStressTest { + static { + Hooks.onErrorDropped(t -> {}); + } + final UnboundedProcessor unboundedProcessor = new UnboundedProcessor(); @JCStressTest @@ -95,7 +100,6 @@ public void request() { stressSubscriber.request(1); stressSubscriber.request(1); stressSubscriber.request(1); - stressSubscriber.request(1); } @Actor @@ -221,7 +225,6 @@ public void request() { stressSubscriber.request(1); stressSubscriber.request(1); stressSubscriber.request(1); - stressSubscriber.request(1); } @Actor @@ -328,6 +331,110 @@ public void subscribeAndRequest() { stressSubscriber.request(1); stressSubscriber.request(1); stressSubscriber.request(1); + } + + @Actor + public void dispose() { + unboundedProcessor.dispose(); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Actor + public void complete() { + unboundedProcessor.onComplete(); + } + + @Actor + public void error() { + unboundedProcessor.onError(testException); + } + + @Arbiter + public void arbiter(LLL_Result r) { + r.r1 = stressSubscriber.onNextCalls; + r.r2 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + if (stressSubscriber.onCompleteCalls > 0 && stressSubscriber.onErrorCalls > 0) { + throw new RuntimeException("boom"); + } + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = { + "0, 1, 0", + "1, 1, 0", + "2, 1, 0", + "3, 1, 0", + "4, 1, 0", + + // dropped error scenarios + "0, 4, 0", + "1, 4, 0", + "2, 4, 0", + "3, 4, 0", + "4, 4, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onComplete() before dispose() || onError()") + @Outcome( + id = { + "0, 2, 0", "1, 2, 0", "2, 2, 0", "3, 2, 0", "4, 2, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onError() before dispose() || onComplete()") + @Outcome( + id = { + "0, 2, 0", + "1, 2, 0", + "2, 2, 0", + "3, 2, 0", + "4, 2, 0", + // dropped error + "0, 5, 0", + "1, 5, 0", + "2, 5, 0", + "3, 5, 0", + "4, 5, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "dispose() before onError() || onComplete()") + @State + public static class Smoke24StressTest extends UnboundedProcessorStressTest { + + static final RuntimeException testException = new RuntimeException("test"); + + final StressSubscriber stressSubscriber = new StressSubscriber<>(0, Fuseable.NONE); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + @Actor + public void subscribeAndRequest() { + unboundedProcessor.subscribe(stressSubscriber); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); stressSubscriber.request(1); } @@ -597,6 +704,197 @@ public void arbiter(LLL_Result r) { } } + @JCStressTest + @Outcome( + id = { + "0, 1, 0", "1, 1, 0", "2, 1, 0", "3, 1, 0", "4, 1, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onComplete()") + @Outcome( + id = { + "0, 0, 0", + "1, 0, 0", + "2, 0, 0", + "3, 0, 0", + "4, 0, 0", + // interleave with error or complete happened first but dispose suppressed them + "0, 3, 0", + "1, 3, 0", + "2, 3, 0", + "3, 3, 0", + "4, 3, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "cancel() before or interleave with onComplete()") + @State + public static class Smoke30StressTest extends UnboundedProcessorStressTest { + + final StressSubscriber stressSubscriber = new StressSubscriber<>(0, Fuseable.NONE); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + { + unboundedProcessor.subscribe(stressSubscriber); + } + + @Actor + public void subscribeAndRequest() { + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + } + + @Actor + public void cancel() { + stressSubscriber.cancel(); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Actor + public void complete() { + unboundedProcessor.onComplete(); + } + + @Arbiter + public void arbiter(LLL_Result r) { + r.r1 = stressSubscriber.onNextCalls; + r.r2 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = { + "0, 1, 0", "1, 1, 0", "2, 1, 0", "3, 1, 0", "4, 1, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onComplete()") + @State + public static class Smoke31StressTest extends UnboundedProcessorStressTest { + + final StressSubscriber stressSubscriber = new StressSubscriber<>(0, Fuseable.NONE); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + { + unboundedProcessor.subscribe(stressSubscriber); + } + + @Actor + public void subscribeAndRequest() { + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + stressSubscriber.request(1); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Actor + public void complete() { + unboundedProcessor.onComplete(); + } + + @Arbiter + public void arbiter(LLL_Result r) { + r.r1 = stressSubscriber.onNextCalls; + r.r2 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + + @JCStressTest + @Outcome( + id = { + "0, 1, 0", "1, 1, 0", "2, 1, 0", "3, 1, 0", "4, 1, 0", + }, + expect = Expect.ACCEPTABLE, + desc = "onComplete()") + @State + public static class Smoke32StressTest extends UnboundedProcessorStressTest { + + final StressSubscriber stressSubscriber = + new StressSubscriber<>(Long.MAX_VALUE, Fuseable.NONE); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + + { + unboundedProcessor.subscribe(stressSubscriber); + } + + @Actor + public void next1() { + unboundedProcessor.onNext(byteBuf1); + unboundedProcessor.onNextPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.onNextPrioritized(byteBuf3); + unboundedProcessor.onNext(byteBuf4); + } + + @Actor + public void complete() { + unboundedProcessor.onComplete(); + } + + @Arbiter + public void arbiter(LLL_Result r) { + r.r1 = stressSubscriber.onNextCalls; + r.r2 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + stressSubscriber.values.forEach(ByteBuf::release); + + r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + } + } + @JCStressTest @Outcome( id = { @@ -997,7 +1295,6 @@ public void subscribeAndRequest() { stressSubscriber.request(1); stressSubscriber.request(1); stressSubscriber.request(1); - stressSubscriber.request(1); } @Actor @@ -1130,7 +1427,6 @@ public void subscribeAndRequest() { stressSubscriber.request(1); stressSubscriber.request(1); stressSubscriber.request(1); - stressSubscriber.request(1); } @Actor diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java index eee1346eb..809125402 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java @@ -48,6 +48,7 @@ import reactor.util.annotation.NonNull; import reactor.util.annotation.Nullable; import reactor.util.context.Context; +import reactor.util.context.ContextView; final class RequestChannelRequesterFlux extends Flux implements RequesterFrameHandler, @@ -763,7 +764,8 @@ public Context currentContext() { if (isSubscribedOrTerminated(state)) { Context cachedContext = this.cachedContext; if (cachedContext == null) { - cachedContext = this.inboundSubscriber.currentContext().putAll(DISCARD_CONTEXT); + cachedContext = + this.inboundSubscriber.currentContext().putAll((ContextView) DISCARD_CONTEXT); this.cachedContext = cachedContext; } return cachedContext; 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 c529b615d..520ff318a 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -26,10 +26,11 @@ import java.util.stream.Stream; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; +import reactor.core.Disposable; import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.Scannable; -import reactor.core.publisher.FluxProcessor; +import reactor.core.publisher.Flux; import reactor.core.publisher.Operators; import reactor.util.annotation.Nullable; import reactor.util.concurrent.Queues; @@ -40,8 +41,12 @@ * *

    The implementation keeps the order of signals. */ -public final class UnboundedProcessor extends FluxProcessor - implements Fuseable.QueueSubscription, Fuseable { +public final class UnboundedProcessor extends Flux + implements Scannable, + Disposable, + CoreSubscriber, + Fuseable.QueueSubscription, + Fuseable { final Queue queue; final Queue priorityQueue; @@ -98,11 +103,6 @@ public UnboundedProcessor(Runnable onFinalizedHook) { this.priorityQueue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); } - @Override - public int getBufferSize() { - return Integer.MAX_VALUE; - } - @Override public Stream inners() { return hasDownstreams() ? Stream.of(Scannable.from(this.actual)) : Stream.empty(); @@ -118,7 +118,7 @@ public Object scanUnsafe(Attr key) { return isCancelled(state) || isDisposed(state); } - return super.scanUnsafe(key); + return null; } public void onNextPrioritized(ByteBuf t) { @@ -622,30 +622,7 @@ public boolean isDisposed() { return isFinalized(this.state); } - @Override - public boolean isTerminated() { - return this.done || isTerminated(this.state); - } - - @Override - @Nullable - public Throwable getError() { - //noinspection unused - final long state = this.state; - if (this.done) { - return this.error; - } else { - return null; - } - } - - @Override - public long downstreamCount() { - return hasDownstreams() ? 1L : 0L; - } - - @Override - public boolean hasDownstreams() { + boolean hasDownstreams() { final long state = this.state; return !isTerminated(state) && isSubscriberReady(state); } 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 7ade3e59b..933ac09ca 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -253,7 +253,7 @@ void dispose(@Nullable Throwable e) { framesSaverDisposable.dispose(); activeReceivingSubscriber.dispose(); - savableFramesSender.onComplete(); + savableFramesSender.dispose(); onConnectionClosedSink.tryEmitComplete(); if (e != null) { 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 6a1d07a67..327172255 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -1265,7 +1265,7 @@ void reassembleMetadata( .then(() -> rule.connection.addToReceivedBuffer(fragments.toArray(new ByteBuf[0]))) .assertNext( responsePayload -> { - PayloadAssert.assertThat(requestPayload).isEqualTo(metadataOnlyPayload).hasNoLeaks(); + PayloadAssert.assertThat(responsePayload).isEqualTo(metadataOnlyPayload).hasNoLeaks(); metadataOnlyPayload.release(); }) .thenCancel() 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 75aa2a5b2..b12d72b51 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java @@ -18,9 +18,18 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.util.IllegalReferenceCountException; +import org.assertj.core.api.Assertions; import org.assertj.core.presentation.StandardRepresentation; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; -public final class ByteBufRepresentation extends StandardRepresentation { +public final class ByteBufRepresentation extends StandardRepresentation + implements BeforeAllCallback { + + @Override + public void beforeAll(ExtensionContext context) { + Assertions.useRepresentation(this); + } @Override protected String fallbackToStringOf(Object object) { diff --git a/rsocket-core/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/rsocket-core/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 000000000..2b51ba0de --- /dev/null +++ b/rsocket-core/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +io.rsocket.frame.ByteBufRepresentation \ No newline at end of file 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 index 5f04eb575..33cff28b4 100644 --- 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 @@ -24,10 +24,7 @@ abstract class BaseWebsocketServerTransport< private static final ChannelHandler pongHandler = new PongHandler(); static Function serverConfigurer = - server -> - server.tcpConfiguration( - tcpServer -> - tcpServer.doOnConnection(connection -> connection.addHandlerLast(pongHandler))); + server -> server.doOnConnection(connection -> connection.addHandlerLast(pongHandler)); final WebsocketServerSpec.Builder specBuilder = WebsocketServerSpec.builder().maxFramePayloadLength(FRAME_LENGTH_MASK); 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 b9b6201b8..540076704 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 @@ -41,7 +41,7 @@ public void testThatSetupWithUnSpecifiedFrameSizeShouldSetMaxFrameSize() { ArgumentCaptor httpHandlerCaptor = ArgumentCaptor.forClass(BiFunction.class); HttpServer server = Mockito.spy(HttpServer.create()); Mockito.doAnswer(a -> server).when(server).handle(httpHandlerCaptor.capture()); - Mockito.doAnswer(a -> server).when(server).tcpConfiguration(any()); + Mockito.doAnswer(a -> server).when(server).doOnConnection(any()); Mockito.doAnswer(a -> Mono.empty()).when(server).bind(); WebsocketServerTransport serverTransport = WebsocketServerTransport.create(server); From d8cccbe4e69e5ff358b0f75321fc7589e693dff9 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Fri, 25 Mar 2022 11:36:42 +0200 Subject: [PATCH 064/116] adds routing example with TaggingMetadata and CompositeMetadata (#1021) --- .../routing/CompositeMetadataExample.java | 102 ++++++++++++++++++ .../routing/RoutingMetadataExample.java | 83 ++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/CompositeMetadataExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/RoutingMetadataExample.java diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/CompositeMetadataExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/CompositeMetadataExample.java new file mode 100644 index 000000000..a0a02a946 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/CompositeMetadataExample.java @@ -0,0 +1,102 @@ +/* + * Copyright 2015-Present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.examples.transport.tcp.metadata.routing; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.metadata.CompositeMetadata; +import io.rsocket.metadata.CompositeMetadataCodec; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TaggingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.ByteBufPayload; +import java.util.Collections; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +public class CompositeMetadataExample { + static final Logger logger = LoggerFactory.getLogger(CompositeMetadataExample.class); + + public static void main(String[] args) { + RSocketServer.create( + SocketAcceptor.forRequestResponse( + payload -> { + final String route = decodeRoute(payload.sliceMetadata()); + + logger.info("Received RequestResponse[route={}]", route); + + payload.release(); + + if ("my.test.route".equals(route)) { + return Mono.just(ByteBufPayload.create("Hello From My Test Route")); + } + + return Mono.error(new IllegalArgumentException("Route " + route + " not found")); + })) + .bindNow(TcpServerTransport.create("localhost", 7000)); + + RSocket socket = + RSocketConnector.create() + // here we specify that every metadata payload will be encoded using + // CompositeMetadata layout as specified in the following subspec + // https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md + .metadataMimeType(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString()) + .connect(TcpClientTransport.create("localhost", 7000)) + .block(); + + final ByteBuf routeMetadata = + TaggingMetadataCodec.createTaggingContent( + ByteBufAllocator.DEFAULT, Collections.singletonList("my.test.route")); + final CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataCodec.encodeAndAddMetadata( + compositeMetadata, + ByteBufAllocator.DEFAULT, + WellKnownMimeType.MESSAGE_RSOCKET_ROUTING, + routeMetadata); + + socket + .requestResponse( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), compositeMetadata)) + .log() + .block(); + } + + static String decodeRoute(ByteBuf metadata) { + final CompositeMetadata compositeMetadata = new CompositeMetadata(metadata, false); + + for (CompositeMetadata.Entry metadatum : compositeMetadata) { + if (Objects.requireNonNull(metadatum.getMimeType()) + .equals(WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString())) { + return new RoutingMetadata(metadatum.getContent()).iterator().next(); + } + } + + return null; + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/RoutingMetadataExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/RoutingMetadataExample.java new file mode 100644 index 000000000..2aee18bf9 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/RoutingMetadataExample.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015-Present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.examples.transport.tcp.metadata.routing; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TaggingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.ByteBufPayload; +import java.util.Collections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +public class RoutingMetadataExample { + static final Logger logger = LoggerFactory.getLogger(RoutingMetadataExample.class); + + public static void main(String[] args) { + RSocketServer.create( + SocketAcceptor.forRequestResponse( + payload -> { + final String route = decodeRoute(payload.sliceMetadata()); + + logger.info("Received RequestResponse[route={}]", route); + + payload.release(); + + if ("my.test.route".equals(route)) { + return Mono.just(ByteBufPayload.create("Hello From My Test Route")); + } + + return Mono.error(new IllegalArgumentException("Route " + route + " not found")); + })) + .bindNow(TcpServerTransport.create("localhost", 7000)); + + RSocket socket = + RSocketConnector.create() + // here we specify that route will be encoded using + // Routing&Tagging Metadata layout specified at this + // subspec https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md + .metadataMimeType(WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()) + .connect(TcpClientTransport.create("localhost", 7000)) + .block(); + + final ByteBuf routeMetadata = + TaggingMetadataCodec.createTaggingContent( + ByteBufAllocator.DEFAULT, Collections.singletonList("my.test.route")); + socket + .requestResponse( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), routeMetadata)) + .log() + .block(); + } + + static String decodeRoute(ByteBuf metadata) { + final RoutingMetadata routingMetadata = new RoutingMetadata(metadata); + + return routingMetadata.iterator().next(); + } +} From 80a05f873c21f4e557d1980b1734c8a18fea411f Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 26 Mar 2022 11:03:04 +0200 Subject: [PATCH 065/116] adds jdk 17 instead of 16 in build matrix Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .github/workflows/gradle-all.yml | 10 +++++----- .github/workflows/gradle-main.yml | 10 +++++----- .github/workflows/gradle-pr.yml | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/gradle-all.yml b/.github/workflows/gradle-all.yml index 8826f511a..f2df20620 100644 --- a/.github/workflows/gradle-all.yml +++ b/.github/workflows/gradle-all.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -42,7 +42,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -69,7 +69,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -96,7 +96,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -123,7 +123,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: diff --git a/.github/workflows/gradle-main.yml b/.github/workflows/gradle-main.yml index 34d3e65f0..904c45fb7 100644 --- a/.github/workflows/gradle-main.yml +++ b/.github/workflows/gradle-main.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -42,7 +42,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -69,7 +69,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -96,7 +96,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -123,7 +123,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: diff --git a/.github/workflows/gradle-pr.yml b/.github/workflows/gradle-pr.yml index fd88ad76f..cecca085f 100644 --- a/.github/workflows/gradle-pr.yml +++ b/.github/workflows/gradle-pr.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -36,7 +36,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -63,7 +63,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -90,7 +90,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: From 9a504a882a6d43bb26298660c5b0b7a2f71caeb2 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Mon, 28 Mar 2022 18:16:07 +0300 Subject: [PATCH 066/116] fixes `block()` in MetadataPushRequesterMono/FnfRequesterMono (#1044) --- .../core/FireAndForgetRequesterMono.java | 5 ++ .../core/MetadataPushRequesterMono.java | 12 +++- .../io/rsocket/core/RSocketRequesterTest.java | 63 ++++++++++++++++++- 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java b/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java index eceb0976c..a5d527f5c 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java +++ b/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java @@ -185,6 +185,11 @@ public Void block(Duration m) { return block(); } + /** + * This method is deliberately non-blocking regardless it is named as `.block`. The main intent to + * keep this method along with the {@link #subscribe()} is to eliminate redundancy which comes + * with a default block method implementation. + */ @Override @Nullable public Void block() { diff --git a/rsocket-core/src/main/java/io/rsocket/core/MetadataPushRequesterMono.java b/rsocket-core/src/main/java/io/rsocket/core/MetadataPushRequesterMono.java index 226e9a0af..e2512e995 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/MetadataPushRequesterMono.java +++ b/rsocket-core/src/main/java/io/rsocket/core/MetadataPushRequesterMono.java @@ -120,6 +120,11 @@ public Void block(Duration m) { return block(); } + /** + * This method is deliberately non-blocking regardless it is named as `.block`. The main intent to + * keep this method along with the {@link #subscribe()} is to eliminate redundancy which comes + * with a default block method implementation. + */ @Override @Nullable public Void block() { @@ -133,15 +138,16 @@ public Void block() { try { final boolean hasMetadata = p.hasMetadata(); metadata = p.metadata(); - if (hasMetadata) { + if (!hasMetadata) { lazyTerminate(STATE, this); p.release(); - throw new IllegalArgumentException("Metadata push does not support metadata field"); + throw new IllegalArgumentException("Metadata push should have metadata field present"); } if (!isValidMetadata(this.maxFrameLength, metadata)) { lazyTerminate(STATE, this); p.release(); - throw new IllegalArgumentException("Too Big Payload size"); + throw new IllegalArgumentException( + String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)); } } catch (IllegalReferenceCountException e) { lazyTerminate(STATE, this); 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 327172255..183785d2f 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -33,6 +33,7 @@ import static io.rsocket.frame.FrameType.REQUEST_RESPONSE; import static io.rsocket.frame.FrameType.REQUEST_STREAM; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @@ -81,7 +82,6 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Stream; -import org.assertj.core.api.Assertions; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -169,7 +169,7 @@ protected void hookOnSubscribe(Subscription subscription) { public void testHandleSetupException() { rule.connection.addToReceivedBuffer( ErrorFrameCodec.encode(rule.alloc(), 0, new RejectedSetupException("boom"))); - Assertions.assertThatThrownBy(() -> rule.socket.onClose().block()) + assertThatThrownBy(() -> rule.socket.onClose().block()) .isInstanceOf(RejectedSetupException.class); rule.assertHasNoLeaks(); } @@ -373,6 +373,65 @@ public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmen }); } + @ParameterizedTest + @ValueSource(ints = {128, 256, FRAME_LENGTH_MASK}) + public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmentation1( + int maxFrameLength) { + rule.setMaxFrameLength(maxFrameLength); + prepareCalls() + .forEach( + generator -> { + byte[] metadata = new byte[maxFrameLength]; + byte[] data = new byte[maxFrameLength]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + + assertThatThrownBy( + () -> { + final Publisher source = + generator.apply(rule.socket, DefaultPayload.create(data, metadata)); + + if (source instanceof Mono) { + ((Mono) source).block(); + } else { + ((Flux) source).blockLast(); + } + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(String.format(INVALID_PAYLOAD_ERROR_MESSAGE, maxFrameLength)); + + rule.assertHasNoLeaks(); + }); + } + + @Test + public void shouldRejectCallOfNoMetadataPayload() { + final ByteBuf data = rule.allocator.buffer(10); + final Payload payload = ByteBufPayload.create(data); + StepVerifier.create(rule.socket.metadataPush(payload)) + .expectSubscription() + .expectErrorSatisfies( + t -> + assertThat(t) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metadata push should have metadata field present")) + .verify(); + PayloadAssert.assertThat(payload).isReleased(); + rule.assertHasNoLeaks(); + } + + @Test + public void shouldRejectCallOfNoMetadataPayloadBlocking() { + final ByteBuf data = rule.allocator.buffer(10); + final Payload payload = ByteBufPayload.create(data); + + assertThatThrownBy(() -> rule.socket.metadataPush(payload).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metadata push should have metadata field present"); + PayloadAssert.assertThat(payload).isReleased(); + rule.assertHasNoLeaks(); + } + static Stream>> prepareCalls() { return Stream.of( RSocket::fireAndForget, From 571af15e2c9c6c89964113d2e0c6b528ffc662d5 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 28 Mar 2022 18:39:29 +0300 Subject: [PATCH 067/116] eliminates dropped error Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .../io/rsocket/resume/ResumableDuplexConnection.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 933ac09ca..f061857ff 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -192,7 +192,8 @@ public void sendErrorAndClose(RSocketErrorException rSocketErrorException) { t -> { framesSaverDisposable.dispose(); activeReceivingSubscriber.dispose(); - savableFramesSender.dispose(); + savableFramesSender.onComplete(); + savableFramesSender.cancel(); onConnectionClosedSink.tryEmitComplete(); onClose.tryEmitError(t); @@ -200,7 +201,8 @@ public void sendErrorAndClose(RSocketErrorException rSocketErrorException) { () -> { framesSaverDisposable.dispose(); activeReceivingSubscriber.dispose(); - savableFramesSender.dispose(); + savableFramesSender.onComplete(); + savableFramesSender.cancel(); onConnectionClosedSink.tryEmitComplete(); final Throwable cause = rSocketErrorException.getCause(); @@ -253,7 +255,8 @@ void dispose(@Nullable Throwable e) { framesSaverDisposable.dispose(); activeReceivingSubscriber.dispose(); - savableFramesSender.dispose(); + savableFramesSender.onComplete(); + savableFramesSender.cancel(); onConnectionClosedSink.tryEmitComplete(); if (e != null) { From 40c1dbd249072b69be92fc17c4d67a3cce3c4e9c Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Sat, 6 Aug 2022 14:27:46 +0300 Subject: [PATCH 068/116] adds Micrometer Observation API integration (#1056) Co-authored-by: Marcin Grzejszczak --- build.gradle | 35 ++- gradle.properties | 2 +- rsocket-examples/build.gradle | 7 + .../ObservationIntegrationTest.java | 236 ++++++++++++++++++ rsocket-micrometer/build.gradle | 2 + .../micrometer/observation/ByteBufGetter.java | 36 +++ .../micrometer/observation/ByteBufSetter.java | 33 +++ .../observation/CompositeMetadataUtils.java | 40 +++ .../DefaultRSocketObservationConvention.java | 49 ++++ ...RSocketRequesterObservationConvention.java | 62 +++++ ...RSocketResponderObservationConvention.java | 61 +++++ .../ObservationRequesterRSocketProxy.java | 224 +++++++++++++++++ .../ObservationResponderRSocketProxy.java | 167 +++++++++++++ .../micrometer/observation/PayloadUtils.java | 73 ++++++ .../observation/RSocketContext.java | 76 ++++++ .../RSocketDocumentedObservation.java | 231 +++++++++++++++++ ...RSocketRequesterObservationConvention.java | 35 +++ ...ketRequesterTracingObservationHandler.java | 124 +++++++++ ...RSocketResponderObservationConvention.java | 35 +++ ...ketResponderTracingObservationHandler.java | 149 +++++++++++ 20 files changed, 1675 insertions(+), 2 deletions(-) create mode 100644 rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufGetter.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufSetter.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/CompositeMetadataUtils.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/PayloadUtils.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketContext.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java diff --git a/build.gradle b/build.gradle index a3ff46a4f..2e890e032 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,8 @@ subprojects { ext['slf4j.version'] = '1.7.36' ext['jmh.version'] = '1.33' ext['junit.version'] = '5.8.1' - ext['micrometer.version'] = '1.8.4' + ext['micrometer.version'] = '1.10.0-SNAPSHOT' + ext['micrometer-tracing.version'] = '1.0.0-SNAPSHOT' ext['assertj.version'] = '3.22.0' ext['netflix.limits.version'] = '0.3.6' ext['bouncycastle-bcpkix.version'] = '1.70' @@ -77,6 +78,10 @@ subprojects { dependency "io.netty:netty-tcnative-boringssl-static:${ext['netty-boringssl.version']}" dependency "org.bouncycastle:bcpkix-jdk15on:${ext['bouncycastle-bcpkix.version']}" dependency "io.micrometer:micrometer-core:${ext['micrometer.version']}" + dependency "io.micrometer:micrometer-observation:${ext['micrometer.version']}" + dependency "io.micrometer:micrometer-test:${ext['micrometer.version']}" + dependency "io.micrometer:micrometer-tracing:${ext['micrometer-tracing.version']}" + dependency "io.micrometer:micrometer-tracing-integration-test:${ext['micrometer-tracing.version']}" dependency "org.assertj:assertj-core:${ext['assertj.version']}" dependency "org.hdrhistogram:HdrHistogram:${ext['hdrhistogram.version']}" dependency "org.slf4j:slf4j-api:${ext['slf4j.version']}" @@ -117,6 +122,7 @@ subprojects { if (version.endsWith('SNAPSHOT') || project.hasProperty('versionSuffix')) { maven { url 'https://repo.spring.io/libs-snapshot' } maven { url 'https://oss.jfrog.org/artifactory/oss-snapshot-local' } + mavenLocal() } } @@ -256,4 +262,31 @@ description = 'RSocket: Stream Oriented Messaging Passing with Reactive Stream S repositories { mavenCentral() + + maven { url 'https://repo.spring.io/snapshot' } + mavenLocal() +} + +configurations { + adoc +} + +dependencies { + adoc "io.micrometer:micrometer-docs-generator-spans:1.0.0-SNAPSHOT" + adoc "io.micrometer:micrometer-docs-generator-metrics:1.0.0-SNAPSHOT" +} + +task generateObservabilityDocs(dependsOn: ["generateObservabilityMetricsDocs", "generateObservabilitySpansDocs"]) { +} + +task generateObservabilityMetricsDocs(type: JavaExec) { + mainClass = "io.micrometer.docs.metrics.DocsFromSources" + classpath configurations.adoc + args project.rootDir.getAbsolutePath(), ".*", project.rootProject.buildDir.getAbsolutePath() +} + +task generateObservabilitySpansDocs(type: JavaExec) { + mainClass = "io.micrometer.docs.spans.DocsFromSources" + classpath configurations.adoc + args project.rootDir.getAbsolutePath(), ".*", project.rootProject.buildDir.getAbsolutePath() } diff --git a/gradle.properties b/gradle.properties index e9219dfe6..3b8caafcc 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.1.2 +version=1.2.0-SNAPSHOT perfBaselineVersion=1.1.1 diff --git a/rsocket-examples/build.gradle b/rsocket-examples/build.gradle index d03524cd9..e339b170a 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -24,6 +24,11 @@ dependencies { implementation project(':rsocket-transport-local') implementation project(':rsocket-transport-netty') + implementation "io.micrometer:micrometer-core" + implementation "io.micrometer:micrometer-tracing" + implementation project(":rsocket-micrometer") + testImplementation 'org.awaitility:awaitility' + implementation 'com.netflix.concurrency-limits:concurrency-limits-core' runtimeOnly 'ch.qos.logback:logback-classic' @@ -33,6 +38,8 @@ dependencies { testImplementation 'org.mockito:mockito-core' testImplementation 'org.assertj:assertj-core' testImplementation 'io.projectreactor:reactor-test' + testImplementation "io.micrometer:micrometer-test" + testImplementation "io.micrometer:micrometer-tracing-integration-test" testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' } diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java new file mode 100644 index 000000000..2bf5e42e7 --- /dev/null +++ b/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java @@ -0,0 +1,236 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.integration.observation; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.core.tck.MeterRegistryAssert; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.test.SampleTestRunner; +import io.micrometer.tracing.test.reporter.BuildingBlocks; +import io.micrometer.tracing.test.simple.SpansAssert; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.micrometer.observation.ByteBufGetter; +import io.rsocket.micrometer.observation.ByteBufSetter; +import io.rsocket.micrometer.observation.ObservationRequesterRSocketProxy; +import io.rsocket.micrometer.observation.ObservationResponderRSocketProxy; +import io.rsocket.micrometer.observation.RSocketRequesterTracingObservationHandler; +import io.rsocket.micrometer.observation.RSocketResponderTracingObservationHandler; +import io.rsocket.plugins.RSocketInterceptor; +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 java.util.Deque; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class ObservationIntegrationTest extends SampleTestRunner { + private static final MeterRegistry registry = new SimpleMeterRegistry(); + private static final ObservationRegistry observationRegistry = ObservationRegistry.create(); + + static { + observationRegistry + .observationConfig() + .observationHandler(new DefaultMeterObservationHandler(registry)); + } + + private final RSocketInterceptor requesterInterceptor; + private final RSocketInterceptor responderInterceptor; + + ObservationIntegrationTest() { + super(SampleRunnerConfig.builder().build(), observationRegistry, registry); + requesterInterceptor = + reactiveSocket -> new ObservationRequesterRSocketProxy(reactiveSocket, observationRegistry); + + responderInterceptor = + reactiveSocket -> new ObservationResponderRSocketProxy(reactiveSocket, observationRegistry); + } + + private CloseableChannel server; + private RSocket client; + private AtomicInteger counter; + + @Override + public BiConsumer>> + customizeObservationHandlers() { + return (buildingBlocks, observationHandlers) -> { + observationHandlers.addFirst( + new RSocketRequesterTracingObservationHandler( + buildingBlocks.getTracer(), + buildingBlocks.getPropagator(), + new ByteBufSetter(), + false)); + observationHandlers.addFirst( + new RSocketResponderTracingObservationHandler( + buildingBlocks.getTracer(), + buildingBlocks.getPropagator(), + new ByteBufGetter(), + false)); + }; + } + + @AfterEach + public void teardown() { + if (server != null) { + server.dispose(); + } + } + + private void testRequest() { + counter.set(0); + client.requestResponse(DefaultPayload.create("REQUEST", "META")).block(); + assertThat(counter).as("Server did not see the request.").hasValue(1); + } + + private void testStream() { + counter.set(0); + client.requestStream(DefaultPayload.create("start")).blockLast(); + + assertThat(counter).as("Server did not see the request.").hasValue(1); + } + + private void testRequestChannel() { + counter.set(0); + client.requestChannel(Mono.just(DefaultPayload.create("start"))).blockFirst(); + assertThat(counter).as("Server did not see the request.").hasValue(1); + } + + private void testFireAndForget() { + counter.set(0); + client.fireAndForget(DefaultPayload.create("start")).subscribe(); + Awaitility.await().atMost(Duration.ofSeconds(50)).until(() -> counter.get() == 1); + assertThat(counter).as("Server did not see the request.").hasValue(1); + } + + @Override + public SampleTestRunnerConsumer yourCode() { + return (bb, meterRegistry) -> { + counter = new AtomicInteger(); + server = + RSocketServer.create( + (setup, sendingSocket) -> { + sendingSocket.onClose().subscribe(); + + return Mono.just( + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + payload.release(); + counter.incrementAndGet(); + return Mono.just(DefaultPayload.create("RESPONSE", "METADATA")); + } + + @Override + public Flux requestStream(Payload payload) { + payload.release(); + counter.incrementAndGet(); + return Flux.range(1, 10_000) + .map(i -> DefaultPayload.create("data -> " + i)); + } + + @Override + public Flux requestChannel(Publisher payloads) { + counter.incrementAndGet(); + return Flux.from(payloads); + } + + @Override + public Mono fireAndForget(Payload payload) { + payload.release(); + counter.incrementAndGet(); + return Mono.empty(); + } + }); + }) + .interceptors(registry -> registry.forResponder(responderInterceptor)) + .bind(TcpServerTransport.create("localhost", 0)) + .block(); + + client = + RSocketConnector.create() + .interceptors(registry -> registry.forRequester(requesterInterceptor)) + .connect(TcpClientTransport.create(server.address())) + .block(); + + testRequest(); + + testStream(); + + testRequestChannel(); + + testFireAndForget(); + + // @formatter:off + SpansAssert.assertThat(bb.getFinishedSpans()) + .haveSameTraceId() + // "request_*" + "handle" x 4 + .hasNumberOfSpansEqualTo(8) + .hasNumberOfSpansWithNameEqualTo("handle", 4) + .forAllSpansWithNameEqualTo("handle", span -> span.hasTagWithKey("rsocket.request-type")) + .hasASpanWithNameIgnoreCase("request_stream") + .thenASpanWithNameEqualToIgnoreCase("request_stream") + .hasTag("rsocket.request-type", "REQUEST_STREAM") + .backToSpans() + .hasASpanWithNameIgnoreCase("request_channel") + .thenASpanWithNameEqualToIgnoreCase("request_channel") + .hasTag("rsocket.request-type", "REQUEST_CHANNEL") + .backToSpans() + .hasASpanWithNameIgnoreCase("request_fnf") + .thenASpanWithNameEqualToIgnoreCase("request_fnf") + .hasTag("rsocket.request-type", "REQUEST_FNF") + .backToSpans() + .hasASpanWithNameIgnoreCase("request_response") + .thenASpanWithNameEqualToIgnoreCase("request_response") + .hasTag("rsocket.request-type", "REQUEST_RESPONSE"); + + MeterRegistryAssert.assertThat(registry) + .hasTimerWithNameAndTags( + "rsocket.response", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_RESPONSE"))) + .hasTimerWithNameAndTags( + "rsocket.fnf", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_FNF"))) + .hasTimerWithNameAndTags( + "rsocket.request", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_RESPONSE"))) + .hasTimerWithNameAndTags( + "rsocket.channel", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_CHANNEL"))) + .hasTimerWithNameAndTags( + "rsocket.stream", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_STREAM"))); + // @formatter:on + }; + } +} diff --git a/rsocket-micrometer/build.gradle b/rsocket-micrometer/build.gradle index 128aa1aa5..77bdcd08f 100644 --- a/rsocket-micrometer/build.gradle +++ b/rsocket-micrometer/build.gradle @@ -22,7 +22,9 @@ plugins { dependencies { api project(':rsocket-core') + api 'io.micrometer:micrometer-observation' api 'io.micrometer:micrometer-core' + compileOnly 'io.micrometer:micrometer-tracing' implementation 'org.slf4j:slf4j-api' diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufGetter.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufGetter.java new file mode 100644 index 000000000..09c8ba316 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufGetter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.tracing.propagation.Propagator; +import io.netty.buffer.ByteBuf; +import io.netty.util.CharsetUtil; +import io.rsocket.metadata.CompositeMetadata; + +public class ByteBufGetter implements Propagator.Getter { + + @Override + public String get(ByteBuf carrier, String key) { + final CompositeMetadata compositeMetadata = new CompositeMetadata(carrier, false); + for (CompositeMetadata.Entry entry : compositeMetadata) { + if (key.equals(entry.getMimeType())) { + return entry.getContent().toString(CharsetUtil.UTF_8); + } + } + return null; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufSetter.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufSetter.java new file mode 100644 index 000000000..678bdb1ed --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufSetter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.tracing.propagation.Propagator; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.metadata.CompositeMetadataCodec; + +public class ByteBufSetter implements Propagator.Setter { + + @Override + public void set(CompositeByteBuf carrier, String key, String value) { + final ByteBufAllocator alloc = carrier.alloc(); + CompositeMetadataCodec.encodeAndAddMetadataWithCompression( + carrier, alloc, key, ByteBufUtil.writeUtf8(alloc, value)); + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/CompositeMetadataUtils.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/CompositeMetadataUtils.java new file mode 100644 index 000000000..357be8f15 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/CompositeMetadataUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.core.lang.Nullable; +import io.netty.buffer.ByteBuf; +import io.rsocket.metadata.CompositeMetadata; + +final class CompositeMetadataUtils { + + private CompositeMetadataUtils() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + @Nullable + static ByteBuf extract(ByteBuf metadata, String key) { + final CompositeMetadata compositeMetadata = new CompositeMetadata(metadata, false); + for (CompositeMetadata.Entry entry : compositeMetadata) { + final String entryKey = entry.getMimeType(); + if (key.equals(entryKey)) { + return entry.getContent(); + } + } + return null; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java new file mode 100644 index 000000000..c7ba1c772 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.rsocket.frame.FrameType; + +/** + * Default {@link RSocketRequesterObservationConvention} implementation. + * + * @author Marcin Grzejszczak + * @since 2.0.0 + */ +class DefaultRSocketObservationConvention { + + private final RSocketContext rSocketContext; + + public DefaultRSocketObservationConvention(RSocketContext rSocketContext) { + this.rSocketContext = rSocketContext; + } + + String getName() { + if (this.rSocketContext.frameType == FrameType.REQUEST_FNF) { + return "rsocket.fnf"; + } else if (this.rSocketContext.frameType == FrameType.REQUEST_STREAM) { + return "rsocket.stream"; + } else if (this.rSocketContext.frameType == FrameType.REQUEST_CHANNEL) { + return "rsocket.channel"; + } + return "%s"; + } + + protected RSocketContext getRSocketContext() { + return this.rSocketContext; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java new file mode 100644 index 000000000..824023b86 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java @@ -0,0 +1,62 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.util.StringUtils; +import io.micrometer.observation.Observation; +import io.rsocket.frame.FrameType; + +/** + * Default {@link RSocketRequesterObservationConvention} implementation. + * + * @author Marcin Grzejszczak + * @since 2.0.0 + */ +public class DefaultRSocketRequesterObservationConvention + extends DefaultRSocketObservationConvention implements RSocketRequesterObservationConvention { + + public DefaultRSocketRequesterObservationConvention(RSocketContext rSocketContext) { + super(rSocketContext); + } + + @Override + public KeyValues getLowCardinalityKeyValues(RSocketContext context) { + KeyValues values = + KeyValues.of( + RSocketDocumentedObservation.ResponderTags.REQUEST_TYPE.withValue( + context.frameType.name())); + if (StringUtils.isNotBlank(context.route)) { + values = + values.and(RSocketDocumentedObservation.ResponderTags.ROUTE.withValue(context.route)); + } + return values; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext; + } + + @Override + public String getName() { + if (getRSocketContext().frameType == FrameType.REQUEST_RESPONSE) { + return "rsocket.request"; + } + return super.getName(); + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java new file mode 100644 index 000000000..a6fa387b1 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.util.StringUtils; +import io.micrometer.observation.Observation; +import io.rsocket.frame.FrameType; + +/** + * Default {@link RSocketRequesterObservationConvention} implementation. + * + * @author Marcin Grzejszczak + * @since 2.0.0 + */ +public class DefaultRSocketResponderObservationConvention + extends DefaultRSocketObservationConvention implements RSocketResponderObservationConvention { + + public DefaultRSocketResponderObservationConvention(RSocketContext rSocketContext) { + super(rSocketContext); + } + + @Override + public KeyValues getLowCardinalityKeyValues(RSocketContext context) { + KeyValues tags = + KeyValues.of( + RSocketDocumentedObservation.ResponderTags.REQUEST_TYPE.withValue( + context.frameType.name())); + if (StringUtils.isNotBlank(context.route)) { + tags = tags.and(RSocketDocumentedObservation.ResponderTags.ROUTE.withValue(context.route)); + } + return tags; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext; + } + + @Override + public String getName() { + if (getRSocketContext().frameType == FrameType.REQUEST_RESPONSE) { + return "rsocket.response"; + } + return super.getName(); + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java new file mode 100644 index 000000000..f0d72cbee --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java @@ -0,0 +1,224 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.common.util.StringUtils; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.DocumentedObservation; +import io.netty.buffer.ByteBuf; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.RSocketProxy; +import java.util.Iterator; +import java.util.function.Function; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** + * Tracing representation of a {@link RSocketProxy} for the requester. + * + * @author Marcin Grzejszczak + * @author Oleh Dokuka + * @since 3.1.0 + */ +public class ObservationRequesterRSocketProxy extends RSocketProxy { + + private final ObservationRegistry observationRegistry; + + private RSocketRequesterObservationConvention observationConvention; + + public ObservationRequesterRSocketProxy(RSocket source, ObservationRegistry observationRegistry) { + super(source); + this.observationRegistry = observationRegistry; + } + + @Override + public Mono fireAndForget(Payload payload) { + return setObservation( + super::fireAndForget, + payload, + FrameType.REQUEST_FNF, + RSocketDocumentedObservation.RSOCKET_REQUESTER_FNF); + } + + @Override + public Mono requestResponse(Payload payload) { + return setObservation( + super::requestResponse, + payload, + FrameType.REQUEST_RESPONSE, + RSocketDocumentedObservation.RSOCKET_REQUESTER_REQUEST_RESPONSE); + } + + Mono setObservation( + Function> input, + Payload payload, + FrameType frameType, + DocumentedObservation observation) { + return Mono.deferContextual( + contextView -> { + if (contextView.hasKey(Observation.class)) { + Observation parent = contextView.get(Observation.class); + try (Observation.Scope scope = parent.openScope()) { + return observe(input, payload, frameType, observation); + } + } + return observe(input, payload, frameType, observation); + }); + } + + private String route(Payload payload) { + if (payload.hasMetadata()) { + try { + ByteBuf extracted = + CompositeMetadataUtils.extract( + payload.sliceMetadata(), WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()); + final RoutingMetadata routingMetadata = new RoutingMetadata(extracted); + final Iterator iterator = routingMetadata.iterator(); + return iterator.next(); + } catch (Exception e) { + + } + } + return null; + } + + private Mono observe( + Function> input, + Payload payload, + FrameType frameType, + DocumentedObservation obs) { + String route = route(payload); + RSocketContext rSocketContext = + new RSocketContext( + payload, payload.sliceMetadata(), frameType, route, RSocketContext.Side.REQUESTER); + Observation observation = + obs.start( + this.observationConvention, + new DefaultRSocketRequesterObservationConvention(rSocketContext), + rSocketContext, + observationRegistry); + setContextualName(frameType, route, observation); + Payload newPayload = payload; + if (rSocketContext.modifiedPayload != null) { + newPayload = rSocketContext.modifiedPayload; + } + return input + .apply(newPayload) + .doOnError(observation::error) + .doFinally(signalType -> observation.stop()); + } + + private Observation observation(ContextView contextView) { + if (contextView.hasKey(Observation.class)) { + return contextView.get(Observation.class); + } + return null; + } + + @Override + public Flux requestStream(Payload payload) { + return Flux.deferContextual( + contextView -> + setObservation( + super::requestStream, + payload, + contextView, + FrameType.REQUEST_STREAM, + RSocketDocumentedObservation.RSOCKET_REQUESTER_REQUEST_STREAM)); + } + + @Override + public Flux requestChannel(Publisher inbound) { + return Flux.from(inbound) + .switchOnFirst( + (firstSignal, flux) -> { + final Payload firstPayload = firstSignal.get(); + if (firstPayload != null) { + return setObservation( + p -> super.requestChannel(flux.skip(1).startWith(p)), + firstPayload, + firstSignal.getContextView(), + FrameType.REQUEST_CHANNEL, + RSocketDocumentedObservation.RSOCKET_REQUESTER_REQUEST_CHANNEL); + } + return flux; + }); + } + + private Flux setObservation( + Function> input, + Payload payload, + ContextView contextView, + FrameType frameType, + DocumentedObservation obs) { + Observation parentObservation = observation(contextView); + if (parentObservation == null) { + return observationFlux(input, payload, frameType, obs); + } + try (Observation.Scope scope = parentObservation.openScope()) { + return observationFlux(input, payload, frameType, obs); + } + } + + private Flux observationFlux( + Function> input, + Payload payload, + FrameType frameType, + DocumentedObservation obs) { + return Flux.deferContextual( + contextView -> { + String route = route(payload); + RSocketContext rSocketContext = + new RSocketContext( + payload, + payload.sliceMetadata(), + frameType, + route, + RSocketContext.Side.REQUESTER); + Observation newObservation = + obs.start( + this.observationConvention, + new DefaultRSocketRequesterObservationConvention(rSocketContext), + rSocketContext, + this.observationRegistry); + setContextualName(frameType, route, newObservation); + return input + .apply(rSocketContext.modifiedPayload) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + }); + } + + private void setContextualName(FrameType frameType, String route, Observation newObservation) { + if (StringUtils.isNotBlank(route)) { + newObservation.contextualName(frameType.name() + " " + route); + } else { + newObservation.contextualName(frameType.name()); + } + } + + public void setObservationConvention(RSocketRequesterObservationConvention convention) { + this.observationConvention = convention; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java new file mode 100644 index 000000000..968ddc014 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java @@ -0,0 +1,167 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.common.util.StringUtils; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.netty.buffer.ByteBuf; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.RSocketProxy; +import java.util.Iterator; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Tracing representation of a {@link RSocketProxy} for the responder. + * + * @author Marcin Grzejszczak + * @author Oleh Dokuka + * @since 3.1.0 + */ +public class ObservationResponderRSocketProxy extends RSocketProxy { + + private final ObservationRegistry observationRegistry; + + private RSocketResponderObservationConvention observationConvention; + + public ObservationResponderRSocketProxy(RSocket source, ObservationRegistry observationRegistry) { + super(source); + this.observationRegistry = observationRegistry; + } + + @Override + public Mono fireAndForget(Payload payload) { + // called on Netty EventLoop + // there can't be observation in thread local here + ByteBuf sliceMetadata = payload.sliceMetadata(); + String route = route(payload, sliceMetadata); + RSocketContext rSocketContext = + new RSocketContext( + payload, + payload.sliceMetadata(), + FrameType.REQUEST_FNF, + route, + RSocketContext.Side.RESPONDER); + Observation newObservation = + startObservation(RSocketDocumentedObservation.RSOCKET_RESPONDER_FNF, rSocketContext); + return super.fireAndForget(rSocketContext.modifiedPayload) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + } + + private Observation startObservation( + RSocketDocumentedObservation observation, RSocketContext rSocketContext) { + return observation.start( + this.observationConvention, + new DefaultRSocketResponderObservationConvention(rSocketContext), + rSocketContext, + this.observationRegistry); + } + + @Override + public Mono requestResponse(Payload payload) { + ByteBuf sliceMetadata = payload.sliceMetadata(); + String route = route(payload, sliceMetadata); + RSocketContext rSocketContext = + new RSocketContext( + payload, + payload.sliceMetadata(), + FrameType.REQUEST_RESPONSE, + route, + RSocketContext.Side.RESPONDER); + Observation newObservation = + startObservation( + RSocketDocumentedObservation.RSOCKET_RESPONDER_REQUEST_RESPONSE, rSocketContext); + return super.requestResponse(rSocketContext.modifiedPayload) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + } + + @Override + public Flux requestStream(Payload payload) { + ByteBuf sliceMetadata = payload.sliceMetadata(); + String route = route(payload, sliceMetadata); + RSocketContext rSocketContext = + new RSocketContext( + payload, sliceMetadata, FrameType.REQUEST_STREAM, route, RSocketContext.Side.RESPONDER); + Observation newObservation = + startObservation( + RSocketDocumentedObservation.RSOCKET_RESPONDER_REQUEST_STREAM, rSocketContext); + return super.requestStream(rSocketContext.modifiedPayload) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + .switchOnFirst( + (firstSignal, flux) -> { + final Payload firstPayload = firstSignal.get(); + if (firstPayload != null) { + ByteBuf sliceMetadata = firstPayload.sliceMetadata(); + String route = route(firstPayload, sliceMetadata); + RSocketContext rSocketContext = + new RSocketContext( + firstPayload, + firstPayload.sliceMetadata(), + FrameType.REQUEST_CHANNEL, + route, + RSocketContext.Side.RESPONDER); + Observation newObservation = + startObservation( + RSocketDocumentedObservation.RSOCKET_RESPONDER_REQUEST_CHANNEL, + rSocketContext); + if (StringUtils.isNotBlank(route)) { + newObservation.contextualName(rSocketContext.frameType.name() + " " + route); + } + return super.requestChannel(flux.skip(1).startWith(rSocketContext.modifiedPayload)) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + } + return flux; + }); + } + + private String route(Payload payload, ByteBuf headers) { + if (payload.hasMetadata()) { + try { + final ByteBuf extract = + CompositeMetadataUtils.extract( + headers, WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()); + if (extract != null) { + final RoutingMetadata routingMetadata = new RoutingMetadata(extract); + final Iterator iterator = routingMetadata.iterator(); + return iterator.next(); + } + } catch (Exception e) { + + } + } + return null; + } + + public void setObservationConvention(RSocketResponderObservationConvention convention) { + this.observationConvention = convention; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/PayloadUtils.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/PayloadUtils.java new file mode 100644 index 000000000..e5286a53f --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/PayloadUtils.java @@ -0,0 +1,73 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.Payload; +import io.rsocket.metadata.CompositeMetadata; +import io.rsocket.metadata.CompositeMetadata.Entry; +import io.rsocket.metadata.CompositeMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.ByteBufPayload; +import io.rsocket.util.DefaultPayload; +import java.util.HashSet; +import java.util.Set; + +final class PayloadUtils { + + private PayloadUtils() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + static CompositeByteBuf cleanTracingMetadata(Payload payload, Set fields) { + Set fieldsWithDefaultZipkin = new HashSet<>(fields); + fieldsWithDefaultZipkin.add(WellKnownMimeType.MESSAGE_RSOCKET_TRACING_ZIPKIN.getString()); + final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + if (payload.hasMetadata()) { + try { + final CompositeMetadata entries = new CompositeMetadata(payload.metadata(), false); + for (Entry entry : entries) { + if (!fieldsWithDefaultZipkin.contains(entry.getMimeType())) { + CompositeMetadataCodec.encodeAndAddMetadataWithCompression( + metadata, + ByteBufAllocator.DEFAULT, + entry.getMimeType(), + entry.getContent().retain()); + } + } + } catch (Exception e) { + + } + } + return metadata; + } + + static Payload payload(Payload payload, CompositeByteBuf metadata) { + final Payload newPayload; + try { + if (payload instanceof ByteBufPayload) { + newPayload = ByteBufPayload.create(payload.data().retain(), metadata); + } else { + newPayload = DefaultPayload.create(payload.data().retain(), metadata); + } + } finally { + payload.release(); + } + return newPayload; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketContext.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketContext.java new file mode 100644 index 000000000..8622cdfa5 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketContext.java @@ -0,0 +1,76 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.common.lang.Nullable; +import io.micrometer.observation.Observation; +import io.netty.buffer.ByteBuf; +import io.rsocket.Payload; +import io.rsocket.frame.FrameType; + +public class RSocketContext extends Observation.Context { + + final Payload payload; + + final ByteBuf metadata; + + final FrameType frameType; + + final String route; + + final Side side; + + Payload modifiedPayload; + + RSocketContext( + Payload payload, ByteBuf metadata, FrameType frameType, @Nullable String route, Side side) { + this.payload = payload; + this.metadata = metadata; + this.frameType = frameType; + this.route = route; + this.side = side; + } + + public enum Side { + REQUESTER, + RESPONDER + } + + public Payload getPayload() { + return payload; + } + + public ByteBuf getMetadata() { + return metadata; + } + + public FrameType getFrameType() { + return frameType; + } + + public String getRoute() { + return route; + } + + public Side getSide() { + return side; + } + + public Payload getModifiedPayload() { + return modifiedPayload; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java new file mode 100644 index 000000000..18440ad81 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java @@ -0,0 +1,231 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.docs.DocumentedObservation; + +enum RSocketDocumentedObservation implements DocumentedObservation { + + /** Observation created on the RSocket responder side. */ + RSOCKET_RESPONDER { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + }, + + /** Observation created on the RSocket requester side for Fire and Forget frame type. */ + RSOCKET_REQUESTER_FNF { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketRequesterObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return RequesterTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket responder side for Fire and Forget frame type. */ + RSOCKET_RESPONDER_FNF { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ResponderTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket requester side for Request Response frame type. */ + RSOCKET_REQUESTER_REQUEST_RESPONSE { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketRequesterObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return RequesterTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket responder side for Request Response frame type. */ + RSOCKET_RESPONDER_REQUEST_RESPONSE { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ResponderTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket requester side for Request Stream frame type. */ + RSOCKET_REQUESTER_REQUEST_STREAM { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketRequesterObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return RequesterTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket responder side for Request Stream frame type. */ + RSOCKET_RESPONDER_REQUEST_STREAM { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ResponderTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket requester side for Request Channel frame type. */ + RSOCKET_REQUESTER_REQUEST_CHANNEL { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketRequesterObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return RequesterTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket responder side for Request Channel frame type. */ + RSOCKET_RESPONDER_REQUEST_CHANNEL { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ResponderTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }; + + enum RequesterTags implements KeyName { + + /** Name of the RSocket route. */ + ROUTE { + @Override + public String asString() { + return "rsocket.route"; + } + }, + + /** Name of the RSocket request type. */ + REQUEST_TYPE { + @Override + public String asString() { + return "rsocket.request-type"; + } + }, + + /** Name of the RSocket content type. */ + CONTENT_TYPE { + @Override + public String asString() { + return "rsocket.content-type"; + } + } + } + + enum ResponderTags implements KeyName { + + /** Name of the RSocket route. */ + ROUTE { + @Override + public String asString() { + return "rsocket.route"; + } + }, + + /** Name of the RSocket request type. */ + REQUEST_TYPE { + @Override + public String asString() { + return "rsocket.request-type"; + } + } + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java new file mode 100644 index 000000000..512c19abf --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.observation.Observation; + +/** + * {@link Observation.ObservationConvention} for RSocket requester {@link RSocketContext}. + * + * @author Marcin Grzejszczak + * @since 2.0.0 + */ +public interface RSocketRequesterObservationConvention + extends Observation.ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext + && ((RSocketContext) context).side == RSocketContext.Side.REQUESTER; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java new file mode 100644 index 000000000..3f6b5dc52 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java @@ -0,0 +1,124 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.TracingObservationHandler; +import io.micrometer.tracing.internal.EncodingUtils; +import io.micrometer.tracing.propagation.Propagator; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.Payload; +import io.rsocket.metadata.TracingMetadataCodec; +import java.util.HashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RSocketRequesterTracingObservationHandler + implements TracingObservationHandler { + private static final Logger log = + LoggerFactory.getLogger(RSocketRequesterTracingObservationHandler.class); + + private final Propagator propagator; + + private final Propagator.Setter setter; + + private final Tracer tracer; + + private final boolean isZipkinPropagationEnabled; + + public RSocketRequesterTracingObservationHandler( + Tracer tracer, + Propagator propagator, + Propagator.Setter setter, + boolean isZipkinPropagationEnabled) { + this.tracer = tracer; + this.propagator = propagator; + this.setter = setter; + this.isZipkinPropagationEnabled = isZipkinPropagationEnabled; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext + && ((RSocketContext) context).side == RSocketContext.Side.REQUESTER; + } + + @Override + public Tracer getTracer() { + return this.tracer; + } + + @Override + public void onStart(RSocketContext context) { + Payload payload = context.payload; + Span.Builder spanBuilder = this.tracer.spanBuilder(); + Span span = spanBuilder.kind(Span.Kind.PRODUCER).start(); + log.debug("Extracted result from context or thread local {}", span); + // TODO: newmetadata returns an empty composite byte buf + final CompositeByteBuf newMetadata = + PayloadUtils.cleanTracingMetadata(payload, new HashSet<>(propagator.fields())); + TraceContext traceContext = span.context(); + if (this.isZipkinPropagationEnabled) { + injectDefaultZipkinRSocketHeaders(newMetadata, traceContext); + } + this.propagator.inject(traceContext, newMetadata, this.setter); + context.modifiedPayload = PayloadUtils.payload(payload, newMetadata); + getTracingContext(context).setSpan(span); + } + + @Override + public void onError(RSocketContext context) { + context.getError().ifPresent(throwable -> getRequiredSpan(context).error(throwable)); + } + + @Override + public void onStop(RSocketContext context) { + Span span = getRequiredSpan(context); + tagSpan(context, span); + span.name(context.getContextualName()).end(); + } + + private void injectDefaultZipkinRSocketHeaders( + CompositeByteBuf newMetadata, TraceContext traceContext) { + TracingMetadataCodec.Flags flags = + traceContext.sampled() == null + ? TracingMetadataCodec.Flags.UNDECIDED + : traceContext.sampled() + ? TracingMetadataCodec.Flags.SAMPLE + : TracingMetadataCodec.Flags.NOT_SAMPLE; + String traceId = traceContext.traceId(); + long[] traceIds = EncodingUtils.fromString(traceId); + long[] spanId = EncodingUtils.fromString(traceContext.spanId()); + long[] parentSpanId = EncodingUtils.fromString(traceContext.parentId()); + boolean isTraceId128Bit = traceIds.length == 2; + if (isTraceId128Bit) { + TracingMetadataCodec.encode128( + newMetadata.alloc(), + traceIds[0], + traceIds[1], + spanId[0], + EncodingUtils.fromString(traceContext.parentId())[0], + flags); + } else { + TracingMetadataCodec.encode64( + newMetadata.alloc(), traceIds[0], spanId[0], parentSpanId[0], flags); + } + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java new file mode 100644 index 000000000..9599429f5 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.observation.Observation; + +/** + * {@link Observation.ObservationConvention} for RSocket responder {@link RSocketContext}. + * + * @author Marcin Grzejszczak + * @since 2.0.0 + */ +public interface RSocketResponderObservationConvention + extends Observation.ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext + && ((RSocketContext) context).side == RSocketContext.Side.RESPONDER; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java new file mode 100644 index 000000000..ae06a4307 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java @@ -0,0 +1,149 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.TracingObservationHandler; +import io.micrometer.tracing.internal.EncodingUtils; +import io.micrometer.tracing.propagation.Propagator; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.Payload; +import io.rsocket.frame.FrameType; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TracingMetadata; +import io.rsocket.metadata.TracingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import java.util.HashSet; +import java.util.Iterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RSocketResponderTracingObservationHandler + implements TracingObservationHandler { + + private static final Logger log = + LoggerFactory.getLogger(RSocketResponderTracingObservationHandler.class); + + private final Propagator propagator; + + private final Propagator.Getter getter; + + private final Tracer tracer; + + private final boolean isZipkinPropagationEnabled; + + public RSocketResponderTracingObservationHandler( + Tracer tracer, + Propagator propagator, + Propagator.Getter getter, + boolean isZipkinPropagationEnabled) { + this.tracer = tracer; + this.propagator = propagator; + this.getter = getter; + this.isZipkinPropagationEnabled = isZipkinPropagationEnabled; + } + + @Override + public void onStart(RSocketContext context) { + Span handle = consumerSpanBuilder(context.payload, context.metadata, context.frameType); + CompositeByteBuf bufs = + PayloadUtils.cleanTracingMetadata(context.payload, new HashSet<>(propagator.fields())); + context.modifiedPayload = PayloadUtils.payload(context.payload, bufs); + getTracingContext(context).setSpan(handle); + } + + @Override + public void onError(RSocketContext context) { + context.getError().ifPresent(throwable -> getRequiredSpan(context).error(throwable)); + } + + @Override + public void onStop(RSocketContext context) { + Span span = getRequiredSpan(context); + tagSpan(context, span); + span.end(); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext + && ((RSocketContext) context).side == RSocketContext.Side.RESPONDER; + } + + @Override + public Tracer getTracer() { + return this.tracer; + } + + private Span consumerSpanBuilder(Payload payload, ByteBuf headers, FrameType requestType) { + Span.Builder consumerSpanBuilder = consumerSpanBuilder(payload, headers); + log.debug("Extracted result from headers {}", consumerSpanBuilder); + String name = "handle"; + if (payload.hasMetadata()) { + try { + final ByteBuf extract = + CompositeMetadataUtils.extract( + headers, WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()); + if (extract != null) { + final RoutingMetadata routingMetadata = new RoutingMetadata(extract); + final Iterator iterator = routingMetadata.iterator(); + name = requestType.name() + " " + iterator.next(); + } + } catch (Exception e) { + + } + } + return consumerSpanBuilder.kind(Span.Kind.CONSUMER).name(name).start(); + } + + private Span.Builder consumerSpanBuilder(Payload payload, ByteBuf headers) { + if (this.isZipkinPropagationEnabled && payload.hasMetadata()) { + try { + ByteBuf extract = + CompositeMetadataUtils.extract( + headers, WellKnownMimeType.MESSAGE_RSOCKET_TRACING_ZIPKIN.getString()); + if (extract != null) { + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(extract); + Span.Builder builder = this.tracer.spanBuilder(); + String traceId = EncodingUtils.fromLong(tracingMetadata.traceId()); + long traceIdHigh = tracingMetadata.traceIdHigh(); + if (traceIdHigh != 0L) { + // ExtendedTraceId + traceId = EncodingUtils.fromLong(traceIdHigh) + traceId; + } + TraceContext.Builder parentBuilder = + this.tracer + .traceContextBuilder() + .sampled(tracingMetadata.isDebug() || tracingMetadata.isSampled()) + .traceId(traceId) + .spanId(EncodingUtils.fromLong(tracingMetadata.spanId())) + .parentId(EncodingUtils.fromLong(tracingMetadata.parentId())); + return builder.setParent(parentBuilder.build()); + } else { + return this.propagator.extract(headers, this.getter); + } + } catch (Exception e) { + + } + } + return this.propagator.extract(headers, this.getter); + } +} From ef826de051864c3ee2492711ca572e2a2e1b619a Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Sat, 6 Aug 2022 14:29:46 +0300 Subject: [PATCH 069/116] updates version --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b495b6b45..96c155bc2 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ repositories { maven { url 'https://repo.spring.io/milestone' } // Reactor milestones (if needed) } dependencies { - implementation 'io.rsocket:rsocket-core:1.1.1' - implementation 'io.rsocket:rsocket-transport-netty:1.1.1' + implementation 'io.rsocket:rsocket-core:1.1.2' + implementation 'io.rsocket:rsocket-transport-netty:1.1.2' } ``` @@ -44,8 +44,8 @@ repositories { maven { url 'https://repo.spring.io/snapshot' } // Reactor snapshots (if needed) } dependencies { - implementation 'io.rsocket:rsocket-core:1.1.2-SNAPSHOT' - implementation 'io.rsocket:rsocket-transport-netty:1.1.2-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:1.1.3-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-netty:1.1.3-SNAPSHOT' } ``` From 6426e45619ec32acef0c6c7184020776a9063ffe Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 16 Aug 2022 10:54:21 +0300 Subject: [PATCH 070/116] moves error propagation out of the synchronise to avoid deadlock (#1060) --- .../java/io/rsocket/core/RSocketRequester.java | 16 +++++++++------- .../java/io/rsocket/core/RSocketResponder.java | 11 +++++++---- 2 files changed, 16 insertions(+), 11 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 c10e86d56..bf298706a 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -345,15 +345,17 @@ private void terminate(Throwable e) { requesterLeaseTracker.dispose(e); } + final Collection activeStreamsCopy; synchronized (this) { final IntObjectMap activeStreams = this.activeStreams; - final Collection activeStreamsCopy = new ArrayList<>(activeStreams.values()); - for (FrameHandler handler : activeStreamsCopy) { - if (handler != null) { - try { - handler.handleError(e); - } catch (Throwable ignored) { - } + activeStreamsCopy = new ArrayList<>(activeStreams.values()); + } + + for (FrameHandler handler : activeStreamsCopy) { + if (handler != null) { + try { + handler.handleError(e); + } catch (Throwable ignored) { } } } 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 b2f084f51..ce4fe70a3 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -185,15 +185,18 @@ final void doOnDispose() { requestHandler.dispose(); } - private synchronized void cleanUpSendingSubscriptions() { - final IntObjectMap activeStreams = this.activeStreams; - final Collection activeStreamsCopy = new ArrayList<>(activeStreams.values()); + private void cleanUpSendingSubscriptions() { + final Collection activeStreamsCopy; + synchronized (this) { + final IntObjectMap activeStreams = this.activeStreams; + activeStreamsCopy = new ArrayList<>(activeStreams.values()); + } + for (FrameHandler handler : activeStreamsCopy) { if (handler != null) { handler.handleCancel(); } } - activeStreams.clear(); } final void handleFrame(ByteBuf frame) { From 1fe2d644acf15d2c8e08606b3161c8def1477d50 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 16 Aug 2022 10:54:21 +0300 Subject: [PATCH 071/116] moves error propagation out of the synchronise to avoid deadlock (#1060) (cherry picked from commit 6426e45619ec32acef0c6c7184020776a9063ffe) --- .../java/io/rsocket/core/RSocketRequester.java | 16 +++++++++------- .../java/io/rsocket/core/RSocketResponder.java | 11 +++++++---- 2 files changed, 16 insertions(+), 11 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 c10e86d56..bf298706a 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -345,15 +345,17 @@ private void terminate(Throwable e) { requesterLeaseTracker.dispose(e); } + final Collection activeStreamsCopy; synchronized (this) { final IntObjectMap activeStreams = this.activeStreams; - final Collection activeStreamsCopy = new ArrayList<>(activeStreams.values()); - for (FrameHandler handler : activeStreamsCopy) { - if (handler != null) { - try { - handler.handleError(e); - } catch (Throwable ignored) { - } + activeStreamsCopy = new ArrayList<>(activeStreams.values()); + } + + for (FrameHandler handler : activeStreamsCopy) { + if (handler != null) { + try { + handler.handleError(e); + } catch (Throwable ignored) { } } } 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 b2f084f51..ce4fe70a3 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -185,15 +185,18 @@ final void doOnDispose() { requestHandler.dispose(); } - private synchronized void cleanUpSendingSubscriptions() { - final IntObjectMap activeStreams = this.activeStreams; - final Collection activeStreamsCopy = new ArrayList<>(activeStreams.values()); + private void cleanUpSendingSubscriptions() { + final Collection activeStreamsCopy; + synchronized (this) { + final IntObjectMap activeStreams = this.activeStreams; + activeStreamsCopy = new ArrayList<>(activeStreams.values()); + } + for (FrameHandler handler : activeStreamsCopy) { if (handler != null) { handler.handleCancel(); } } - activeStreams.clear(); } final void handleFrame(ByteBuf frame) { From d330a324a7c506091b8e1da22b8676db1bee5042 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Sat, 20 Aug 2022 13:02:47 +0300 Subject: [PATCH 072/116] improves BaseDuplexConnection and fixes PingClient impl (#1062) --- .../io/rsocket/internal/BaseDuplexConnection.java | 11 ++++------- .../src/main/java/io/rsocket/test/PingClient.java | 10 +++++++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java index 98bed7ba7..fc679c259 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java @@ -22,13 +22,10 @@ import reactor.core.publisher.Sinks; public abstract class BaseDuplexConnection implements DuplexConnection { - protected Sinks.Empty onClose = Sinks.empty(); + protected final Sinks.Empty onClose = Sinks.empty(); + protected final UnboundedProcessor sender = new UnboundedProcessor(onClose::tryEmitEmpty); - protected UnboundedProcessor sender = new UnboundedProcessor(); - - public BaseDuplexConnection() { - onClose().doFinally(s -> doOnClose()).subscribe(); - } + public BaseDuplexConnection() {} @Override public void sendFrame(int streamId, ByteBuf frame) { @@ -48,7 +45,7 @@ public final Mono onClose() { @Override public final void dispose() { - onClose.tryEmitEmpty(); + doOnClose(); } @Override 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 9017e854b..14740950a 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/PingClient.java +++ b/rsocket-test/src/main/java/io/rsocket/test/PingClient.java @@ -63,8 +63,8 @@ Flux pingPong( BiFunction> interaction, int count, final Recorder histogram) { - return client - .flatMapMany( + return Flux.usingWhen( + client, rsocket -> Flux.range(1, count) .flatMap( @@ -78,7 +78,11 @@ Flux pingPong( histogram.recordValue(diff); }); }, - 64)) + 64), + rsocket -> { + rsocket.dispose(); + return rsocket.onClose(); + }) .doOnError(Throwable::printStackTrace); } } From 95ad3b78ed788c4cf479cc9a7199fb4537ce5d72 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 20 Aug 2022 14:32:27 +0300 Subject: [PATCH 073/116] fixes version name Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3b8caafcc..1334b1a16 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.2.0-SNAPSHOT +version=1.2.0 perfBaselineVersion=1.1.1 From 81429871ba1da2052a789afc843a450453cbcae0 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Sat, 20 Aug 2022 14:34:16 +0300 Subject: [PATCH 074/116] updates GA config Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- .github/workflows/gradle-all.yml | 2 +- .github/workflows/gradle-main.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gradle-all.yml b/.github/workflows/gradle-all.yml index f2df20620..abbd14106 100644 --- a/.github/workflows/gradle-all.yml +++ b/.github/workflows/gradle-all.yml @@ -5,7 +5,7 @@ on: # but only for the non master/1.0.x branches push: branches-ignore: - - 1.0.x + - 1.1.x - master jobs: diff --git a/.github/workflows/gradle-main.yml b/.github/workflows/gradle-main.yml index 904c45fb7..469ccb103 100644 --- a/.github/workflows/gradle-main.yml +++ b/.github/workflows/gradle-main.yml @@ -2,11 +2,11 @@ name: Main Branches Java CI on: # Trigger the workflow on push - # but only for the master/1.0.x branch + # but only for the master/1.1.x branch push: branches: - master - - 1.0.x + - 1.1.x jobs: build: From 187cf547e99c2306102d44cc3e66834016d40164 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sun, 21 Aug 2022 23:31:22 +0300 Subject: [PATCH 075/116] fixes build Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 2e890e032..6a2ca537d 100644 --- a/build.gradle +++ b/build.gradle @@ -106,6 +106,7 @@ subprojects { maven { url 'https://repo.spring.io/milestone' content { + includeGroup "io.micrometer" includeGroup "io.projectreactor" includeGroup "io.projectreactor.netty" } @@ -114,6 +115,7 @@ subprojects { maven { url 'https://repo.spring.io/snapshot' content { + includeGroup "io.micrometer" includeGroup "io.projectreactor" includeGroup "io.projectreactor.netty" } From 52f458310f9ab6a721c5c8120cad5dd2e3446100 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Wed, 7 Sep 2022 18:32:20 +0300 Subject: [PATCH 076/116] ensures setupframe is available for future use (#1046) --- .../src/main/java/io/rsocket/core/RSocketConnector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 edd13b48c..432c0f0f5 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -590,7 +590,7 @@ public Mono connect(Supplier transportSupplier) { dataMimeType, setupPayload); - sourceConnection.sendFrame(0, setupFrame.retain()); + sourceConnection.sendFrame(0, setupFrame.retainedSlice()); return clientSetup .init(sourceConnection) From af021d9fd9b0ff02ed9bb2f3406523816b5070d5 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Wed, 7 Sep 2022 22:42:17 +0300 Subject: [PATCH 077/116] adds message counting to protect against malicious overflow (#1067) Co-authored-by: Rossen Stoyanchev --- .../core/RequestChannelRequesterFlux.java | 25 +++ .../RequestChannelResponderSubscriber.java | 64 ++++++++ .../core/RequestStreamRequesterFlux.java | 32 ++++ .../core/RequestChannelRequesterFluxTest.java | 73 +++++++++ ...RequestChannelResponderSubscriberTest.java | 152 +++++++++++++++++- .../core/RequestStreamRequesterFluxTest.java | 83 +++++++++- 6 files changed, 425 insertions(+), 4 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java index 809125402..aab491793 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java @@ -86,6 +86,8 @@ final class RequestChannelRequesterFlux extends Flux Context cachedContext; CoreSubscriber inboundSubscriber; boolean inboundDone; + long requested; + long produced; CompositeByteBuf frames; @@ -138,6 +140,8 @@ public final void request(long n) { return; } + this.requested = Operators.addCap(this.requested, n); + long previousState = addRequestN(STATE, this, n, this.requesterLeaseTracker == null); if (isTerminated(previousState)) { return; @@ -706,6 +710,27 @@ public final void handlePayload(Payload value) { return; } + final long produced = this.produced; + if (this.requested == produced) { + value.release(); + if (!tryCancel()) { + return; + } + + final Throwable cause = + Exceptions.failWithOverflow( + "The number of messages received exceeds the number requested"); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, cause); + } + + this.inboundSubscriber.onError(cause); + return; + } + + this.produced = produced + 1; + this.inboundSubscriber.onNext(value); } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java index 8dac9858d..32128fee4 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java @@ -88,6 +88,8 @@ final class RequestChannelResponderSubscriber extends Flux boolean inboundDone; boolean outboundDone; + long requested; + long produced; public RequestChannelResponderSubscriber( int streamId, @@ -179,6 +181,8 @@ public void request(long n) { return; } + this.requested = Operators.addCap(this.requested, n); + long previousState = StateUtils.addRequestN(STATE, this, n); if (isTerminated(previousState)) { // full termination can be the result of both sides completion / cancelFrame / remote or local @@ -196,6 +200,9 @@ public void request(long n) { Payload firstPayload = this.firstPayload; if (firstPayload != null) { this.firstPayload = null; + + this.produced++; + inboundSubscriber.onNext(firstPayload); } @@ -216,6 +223,8 @@ public void request(long n) { final Payload firstPayload = this.firstPayload; this.firstPayload = null; + this.produced++; + inboundSubscriber.onNext(firstPayload); inboundSubscriber.onComplete(); @@ -238,6 +247,9 @@ public void request(long n) { final Payload firstPayload = this.firstPayload; this.firstPayload = null; + + this.produced++; + inboundSubscriber.onNext(firstPayload); previousState = markFirstFrameSent(STATE, this); @@ -416,6 +428,58 @@ final void handlePayload(Payload p) { return; } + final long produced = this.produced; + if (this.requested == produced) { + p.release(); + + this.inboundDone = true; + + final Throwable cause = + Exceptions.failWithOverflow( + "The number of messages received exceeds the number requested"); + boolean wasThrowableAdded = Exceptions.addThrowable(INBOUND_ERROR, this, cause); + + long previousState = markTerminated(STATE, this); + if (isTerminated(previousState)) { + if (!wasThrowableAdded) { + Operators.onErrorDropped(cause, this.inboundSubscriber.currentContext()); + } + return; + } + + this.requesterResponderSupport.remove(this.streamId, this); + + this.connection.sendFrame( + streamId, + ErrorFrameCodec.encode( + this.allocator, streamId, new CanceledException(cause.getMessage()))); + + if (!isSubscribed(previousState)) { + final Payload firstPayload = this.firstPayload; + this.firstPayload = null; + firstPayload.release(); + } else if (isFirstFrameSent(previousState) && !isInboundTerminated(previousState)) { + Throwable inboundError = Exceptions.terminate(INBOUND_ERROR, this); + if (inboundError != TERMINATED) { + //noinspection ConstantConditions + this.inboundSubscriber.onError(inboundError); + } + } + + // this is downstream subscription so need to cancel it just in case error signal has not + // reached it + // needs for disconnected upstream and downstream case + this.outboundSubscription.cancel(); + + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(this.streamId, FrameType.REQUEST_CHANNEL, cause); + } + return; + } + + this.produced = produced + 1; + this.inboundSubscriber.onNext(p); } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java index 424451a58..55ec43feb 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java @@ -65,6 +65,8 @@ final class RequestStreamRequesterFlux extends Flux CoreSubscriber inboundSubscriber; CompositeByteBuf frames; boolean done; + long requested; + long produced; RequestStreamRequesterFlux(Payload payload, RequesterResponderSupport requesterResponderSupport) { this.allocator = requesterResponderSupport.getAllocator(); @@ -134,6 +136,8 @@ public final void request(long n) { return; } + this.requested = Operators.addCap(this.requested, n); + final RequesterLeaseTracker requesterLeaseTracker = this.requesterLeaseTracker; final boolean leaseEnabled = requesterLeaseTracker != null; final long previousState = addRequestN(STATE, this, n, !leaseEnabled); @@ -295,6 +299,34 @@ public final void handlePayload(Payload p) { return; } + final long produced = this.produced; + if (this.requested == produced) { + p.release(); + + long previousState = markTerminated(STATE, this); + if (isTerminated(previousState)) { + return; + } + + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); + + final IllegalStateException cause = + Exceptions.failWithOverflow( + "The number of messages received exceeds the number requested"); + this.connection.sendFrame(streamId, CancelFrameCodec.encode(this.allocator, streamId)); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, cause); + } + + this.inboundSubscriber.onError(cause); + return; + } + + this.produced = produced + 1; + this.inboundSubscriber.onNext(p); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelRequesterFluxTest.java b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelRequesterFluxTest.java index e39b0d690..c1e0a6876 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelRequesterFluxTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelRequesterFluxTest.java @@ -16,6 +16,7 @@ package io.rsocket.core; import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; +import static io.rsocket.frame.FrameType.CANCEL; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -40,6 +41,7 @@ import java.util.stream.Stream; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeAll; +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; @@ -513,6 +515,77 @@ public void errorShouldTerminateExecution(String terminationMode) { stateAssert.isTerminated(); } + @Test + public void failOnOverflow() { + final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); + final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + final TestPublisher publisher = TestPublisher.create(); + + final RequestChannelRequesterFlux requestChannelRequesterFlux = + new RequestChannelRequesterFlux(publisher, activeStreams); + final StateAssert stateAssert = + StateAssert.assertThat(requestChannelRequesterFlux); + + // state machine check + + stateAssert.isUnsubscribed(); + activeStreams.assertNoActiveStreams(); + + final AssertSubscriber assertSubscriber = + requestChannelRequesterFlux.subscribeWith(AssertSubscriber.create(0)); + activeStreams.assertNoActiveStreams(); + + // state machine check + stateAssert.hasSubscribedFlagOnly(); + + assertSubscriber.request(1); + stateAssert.hasSubscribedFlag().hasRequestN(1).hasNoFirstFrameSentFlag(); + activeStreams.assertNoActiveStreams(); + + Payload payload1 = TestRequesterResponderSupport.randomPayload(allocator); + + publisher.next(payload1.retain()); + + FrameAssert.assertThat(sender.awaitFrame()) + .typeOf(FrameType.REQUEST_CHANNEL) + .hasPayload(payload1) + .hasRequestN(1) + .hasNoLeaks(); + payload1.release(); + + stateAssert.hasSubscribedFlag().hasRequestN(1).hasFirstFrameSentFlag(); + activeStreams.assertHasStream(1, requestChannelRequesterFlux); + + publisher.assertMaxRequested(1); + + Payload nextPayload = TestRequesterResponderSupport.genericPayload(allocator); + requestChannelRequesterFlux.handlePayload(nextPayload); + + Payload unrequestedPayload = TestRequesterResponderSupport.genericPayload(allocator); + requestChannelRequesterFlux.handlePayload(unrequestedPayload); + + final ByteBuf cancelFrame = sender.awaitFrame(); + FrameAssert.assertThat(cancelFrame) + .isNotNull() + .typeOf(CANCEL) + .hasClientSideStreamId() + .hasStreamId(1) + .hasNoLeaks(); + + assertSubscriber + .assertValuesWith(p -> PayloadAssert.assertThat(p).isSameAs(nextPayload).hasNoLeaks()) + .assertError() + .assertErrorMessage("The number of messages received exceeds the number requested"); + + publisher.assertWasCancelled(); + + activeStreams.assertNoActiveStreams(); + // state machine check + stateAssert.isTerminated(); + Assertions.assertThat(sender.isEmpty()).isTrue(); + } + /* * +--------------------------------+ * | Racing Test Cases | diff --git a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java index 32af4e3b6..890458caf 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java @@ -263,6 +263,143 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String comp allocator.assertHasNoLeaks(); } + @Test + public void failOnOverflow() { + final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); + final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + final Payload firstPayload = TestRequesterResponderSupport.genericPayload(allocator); + final TestPublisher publisher = TestPublisher.create(); + + final RequestChannelResponderSubscriber requestChannelResponderSubscriber = + new RequestChannelResponderSubscriber(1, 1, firstPayload, activeStreams); + final StateAssert stateAssert = + StateAssert.assertThat(requestChannelResponderSubscriber); + activeStreams.activeStreams.put(1, requestChannelResponderSubscriber); + + // state machine check + stateAssert.isUnsubscribed().hasRequestN(0); + activeStreams.assertHasStream(1, requestChannelResponderSubscriber); + + publisher.subscribe(requestChannelResponderSubscriber); + publisher.assertMaxRequested(1); + // state machine check + stateAssert.isUnsubscribed().hasRequestN(0); + + final AssertSubscriber assertSubscriber = + requestChannelResponderSubscriber.subscribeWith(AssertSubscriber.create(0)); + Assertions.assertThat(firstPayload.refCnt()).isOne(); + + // state machine check + stateAssert.hasSubscribedFlagOnly().hasRequestN(0); + + assertSubscriber.request(1); + + // state machine check + stateAssert.hasSubscribedFlag().hasFirstFrameSentFlag().hasRequestN(1); + + // should not send requestN since 1 is remaining + Assertions.assertThat(sender.isEmpty()).isTrue(); + + assertSubscriber.request(1); + + stateAssert.hasSubscribedFlag().hasRequestN(2).hasFirstFrameSentFlag(); + + // should not send requestN since 1 is remaining + FrameAssert.assertThat(sender.awaitFrame()) + .typeOf(REQUEST_N) + .hasStreamId(1) + .hasRequestN(1) + .hasNoLeaks(); + + Payload nextPayload = TestRequesterResponderSupport.genericPayload(allocator); + requestChannelResponderSubscriber.handlePayload(nextPayload); + + Payload unrequestedPayload = TestRequesterResponderSupport.genericPayload(allocator); + requestChannelResponderSubscriber.handlePayload(unrequestedPayload); + + final ByteBuf cancelErrorFrame = sender.awaitFrame(); + FrameAssert.assertThat(cancelErrorFrame) + .isNotNull() + .typeOf(ERROR) + .hasData("The number of messages received exceeds the number requested") + .hasClientSideStreamId() + .hasStreamId(1) + .hasNoLeaks(); + + assertSubscriber + .assertValuesWith( + p -> PayloadAssert.assertThat(p).isSameAs(firstPayload).hasNoLeaks(), + p -> PayloadAssert.assertThat(p).isSameAs(nextPayload).hasNoLeaks()) + .assertErrorMessage("The number of messages received exceeds the number requested"); + + Assertions.assertThat(firstPayload.refCnt()).isZero(); + Assertions.assertThat(nextPayload.refCnt()).isZero(); + Assertions.assertThat(unrequestedPayload.refCnt()).isZero(); + stateAssert.isTerminated(); + activeStreams.assertNoActiveStreams(); + + Assertions.assertThat(sender.isEmpty()).isTrue(); + allocator.assertHasNoLeaks(); + } + + @Test + public void failOnOverflowBeforeFirstPayloadIsSent() { + final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); + final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + final Payload firstPayload = TestRequesterResponderSupport.genericPayload(allocator); + final TestPublisher publisher = TestPublisher.create(); + + final RequestChannelResponderSubscriber requestChannelResponderSubscriber = + new RequestChannelResponderSubscriber(1, 1, firstPayload, activeStreams); + final StateAssert stateAssert = + StateAssert.assertThat(requestChannelResponderSubscriber); + activeStreams.activeStreams.put(1, requestChannelResponderSubscriber); + + // state machine check + stateAssert.isUnsubscribed().hasRequestN(0); + activeStreams.assertHasStream(1, requestChannelResponderSubscriber); + + publisher.subscribe(requestChannelResponderSubscriber); + publisher.assertMaxRequested(1); + // state machine check + stateAssert.isUnsubscribed().hasRequestN(0); + + final AssertSubscriber assertSubscriber = + requestChannelResponderSubscriber.subscribeWith(AssertSubscriber.create(0)); + Assertions.assertThat(firstPayload.refCnt()).isOne(); + + // state machine check + stateAssert.hasSubscribedFlagOnly().hasRequestN(0); + + Payload unrequestedPayload = TestRequesterResponderSupport.genericPayload(allocator); + requestChannelResponderSubscriber.handlePayload(unrequestedPayload); + + final ByteBuf cancelErrorFrame = sender.awaitFrame(); + FrameAssert.assertThat(cancelErrorFrame) + .isNotNull() + .typeOf(ERROR) + .hasData("The number of messages received exceeds the number requested") + .hasClientSideStreamId() + .hasStreamId(1) + .hasNoLeaks(); + + assertSubscriber.request(1); + + assertSubscriber + .assertValuesWith(p -> PayloadAssert.assertThat(p).isSameAs(firstPayload).hasNoLeaks()) + .assertErrorMessage("The number of messages received exceeds the number requested"); + + Assertions.assertThat(firstPayload.refCnt()).isZero(); + Assertions.assertThat(unrequestedPayload.refCnt()).isZero(); + stateAssert.isTerminated(); + activeStreams.assertNoActiveStreams(); + + Assertions.assertThat(sender.isEmpty()).isTrue(); + allocator.assertHasNoLeaks(); + } + /* * +--------------------------------+ * | Racing Test Cases | @@ -664,7 +801,7 @@ public void shouldHaveNoLeaksOnReassemblyAndCancelRacing(String terminationMode) ; final TestPublisher publisher = TestPublisher.createNoncompliant(DEFER_CANCELLATION, CLEANUP_ON_TERMINATE); - final AssertSubscriber assertSubscriber = new AssertSubscriber<>(1); + final AssertSubscriber assertSubscriber = new AssertSubscriber<>(2); Payload firstPayload = TestRequesterResponderSupport.genericPayload(allocator); final RequestChannelResponderSubscriber requestOperator = @@ -725,8 +862,17 @@ public void shouldHaveNoLeaksOnReassemblyAndCancelRacing(String terminationMode) assertSubscriber.assertTerminated().assertError(); } - final ByteBuf frame = sender.awaitFrame(); - FrameAssert.assertThat(frame) + final ByteBuf requstFrame = sender.awaitFrame(); + FrameAssert.assertThat(requstFrame) + .isNotNull() + .typeOf(REQUEST_N) + .hasRequestN(1) + .hasClientSideStreamId() + .hasStreamId(1) + .hasNoLeaks(); + + final ByteBuf terminalFrame = sender.awaitFrame(); + FrameAssert.assertThat(terminalFrame) .isNotNull() .typeOf(terminationMode.equals("cancel") ? CANCEL : ERROR) .hasClientSideStreamId() diff --git a/rsocket-core/src/test/java/io/rsocket/core/RequestStreamRequesterFluxTest.java b/rsocket-core/src/test/java/io/rsocket/core/RequestStreamRequesterFluxTest.java index 88dd5441e..8702d1a80 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequestStreamRequesterFluxTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequestStreamRequesterFluxTest.java @@ -926,7 +926,7 @@ public void shouldErrorOnIncorrectRefCntInGivenPayloadLatePhase() { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); final TestDuplexConnection sender = activeStreams.getDuplexConnection(); - ; + final Payload payload = ByteBufPayload.create(""); final RequestStreamRequesterFlux requestStreamRequesterFlux = @@ -1129,6 +1129,87 @@ static Stream> shouldErrorIfNoAvailabilityS .isInstanceOf(RuntimeException.class)); } + @Test + public void failOnOverflow() { + final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); + final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + final Payload payload = TestRequesterResponderSupport.genericPayload(allocator); + + final RequestStreamRequesterFlux requestStreamRequesterFlux = + new RequestStreamRequesterFlux(payload, activeStreams); + final StateAssert stateAssert = + StateAssert.assertThat(requestStreamRequesterFlux); + + // state machine check + + stateAssert.isUnsubscribed(); + activeStreams.assertNoActiveStreams(); + + final AssertSubscriber assertSubscriber = + requestStreamRequesterFlux.subscribeWith(AssertSubscriber.create(0)); + Assertions.assertThat(payload.refCnt()).isOne(); + activeStreams.assertNoActiveStreams(); + // state machine check + stateAssert.hasSubscribedFlagOnly(); + + assertSubscriber.request(1); + + Assertions.assertThat(payload.refCnt()).isZero(); + activeStreams.assertHasStream(1, requestStreamRequesterFlux); + + // state machine check + stateAssert.hasSubscribedFlag().hasRequestN(1).hasFirstFrameSentFlag(); + + final ByteBuf frame = sender.awaitFrame(); + FrameAssert.assertThat(frame) + .isNotNull() + .hasPayloadSize( + "testData".getBytes(CharsetUtil.UTF_8).length + + "testMetadata".getBytes(CharsetUtil.UTF_8).length) + .hasMetadata("testMetadata") + .hasData("testData") + .hasNoFragmentsFollow() + .hasRequestN(1) + .typeOf(FrameType.REQUEST_STREAM) + .hasClientSideStreamId() + .hasStreamId(1) + .hasNoLeaks(); + + Assertions.assertThat(sender.isEmpty()).isTrue(); + + Payload requestedPayload = TestRequesterResponderSupport.randomPayload(allocator); + requestStreamRequesterFlux.handlePayload(requestedPayload); + + Payload unrequestedPayload = TestRequesterResponderSupport.randomPayload(allocator); + requestStreamRequesterFlux.handlePayload(unrequestedPayload); + + final ByteBuf cancelFrame = sender.awaitFrame(); + FrameAssert.assertThat(cancelFrame) + .isNotNull() + .typeOf(FrameType.CANCEL) + .hasClientSideStreamId() + .hasStreamId(1) + .hasNoLeaks(); + + assertSubscriber + .assertValuesWith(p -> PayloadAssert.assertThat(p).isEqualTo(requestedPayload).hasNoLeaks()) + .assertError() + .assertErrorMessage("The number of messages received exceeds the number requested"); + + PayloadAssert.assertThat(requestedPayload).isReleased(); + PayloadAssert.assertThat(unrequestedPayload).isReleased(); + + Assertions.assertThat(payload.refCnt()).isZero(); + activeStreams.assertNoActiveStreams(); + + Assertions.assertThat(sender.isEmpty()).isTrue(); + + // state machine check + stateAssert.isTerminated(); + allocator.assertHasNoLeaks(); + } + @Test public void checkName() { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); From f15c14a06bd5b36db532e059c678f02aa05d08be Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 13 Sep 2022 19:25:37 +0300 Subject: [PATCH 078/116] Adds reflection hints for native-image support 1.1.x (#1073) --- .../rsocket-core/reflect-config.json | 130 ++++++++++++++++++ .../reflect-config.json | 16 +++ 2 files changed, 146 insertions(+) create mode 100644 rsocket-core/src/main/resources/META-INF/native-image/io.rsocket/rsocket-core/reflect-config.json create mode 100644 rsocket-transport-netty/src/main/resources/META-INF/native-image/io.rsocket/rsocket-transport-netty/reflect-config.json diff --git a/rsocket-core/src/main/resources/META-INF/native-image/io.rsocket/rsocket-core/reflect-config.json b/rsocket-core/src/main/resources/META-INF/native-image/io.rsocket/rsocket-core/reflect-config.json new file mode 100644 index 000000000..0a3844451 --- /dev/null +++ b/rsocket-core/src/main/resources/META-INF/native-image/io.rsocket/rsocket-core/reflect-config.json @@ -0,0 +1,130 @@ +[ + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.BaseLinkedQueueConsumerNodeRef" + }, + "name": "io.rsocket.internal.jctools.queues.BaseLinkedQueueConsumerNodeRef", + "fields": [ + { + "name": "consumerNode" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.BaseLinkedQueueProducerNodeRef" + }, + "name": "io.rsocket.internal.jctools.queues.BaseLinkedQueueProducerNodeRef", + "fields": [ + { + "name": "producerNode" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields" + }, + "name": "io.rsocket.internal.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields", + "fields": [ + { + "name": "producerLimit" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.BaseMpscLinkedArrayQueueConsumerFields" + }, + "name": "io.rsocket.internal.jctools.queues.BaseMpscLinkedArrayQueueConsumerFields", + "fields": [ + { + "name": "consumerIndex" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.BaseMpscLinkedArrayQueueProducerFields" + }, + "name": "io.rsocket.internal.jctools.queues.BaseMpscLinkedArrayQueueProducerFields", + "fields": [ + { + "name": "producerIndex" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.LinkedQueueNode" + }, + "name": "io.rsocket.internal.jctools.queues.LinkedQueueNode", + "fields": [ + { + "name": "next" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.MpscArrayQueueConsumerIndexField" + }, + "name": "io.rsocket.internal.jctools.queues.MpscArrayQueueConsumerIndexField", + "fields": [ + { + "name": "consumerIndex" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.MpscArrayQueueProducerIndexField" + }, + "name": "io.rsocket.internal.jctools.queues.MpscArrayQueueProducerIndexField", + "fields": [ + { + "name": "producerIndex" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.MpscArrayQueueProducerLimitField" + }, + "name": "io.rsocket.internal.jctools.queues.MpscArrayQueueProducerLimitField", + "fields": [ + { + "name": "producerLimit" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.UnsafeAccess" + }, + "name": "sun.misc.Unsafe", + "fields": [ + { + "name": "theUnsafe" + } + ], + "queriedMethods": [ + { + "name": "getAndAddLong", + "parameterTypes": [ + "java.lang.Object", + "long", + "long" + ] + }, + { + "name": "getAndSetObject", + "parameterTypes": [ + "java.lang.Object", + "long", + "java.lang.Object" + ] + } + ] + } +] \ No newline at end of file diff --git a/rsocket-transport-netty/src/main/resources/META-INF/native-image/io.rsocket/rsocket-transport-netty/reflect-config.json b/rsocket-transport-netty/src/main/resources/META-INF/native-image/io.rsocket/rsocket-transport-netty/reflect-config.json new file mode 100644 index 000000000..3a2baa440 --- /dev/null +++ b/rsocket-transport-netty/src/main/resources/META-INF/native-image/io.rsocket/rsocket-transport-netty/reflect-config.json @@ -0,0 +1,16 @@ +[ + { + "condition": { + "typeReachable": "io.rsocket.transport.netty.RSocketLengthCodec" + }, + "name": "io.rsocket.transport.netty.RSocketLengthCodec", + "queryAllPublicMethods": true + }, + { + "condition": { + "typeReachable": "io.rsocket.transport.netty.server.BaseWebsocketServerTransport$PongHandler" + }, + "name": "io.rsocket.transport.netty.server.BaseWebsocketServerTransport$PongHandler", + "queryAllPublicMethods": true + } +] \ No newline at end of file From bde2a1b03f77c4f9165a202b0da00756b543d50c Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 13 Sep 2022 21:22:18 +0300 Subject: [PATCH 079/116] updates versions Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- build.gradle | 12 ++++++------ gradle.properties | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index a3ff46a4f..8399c27e1 100644 --- a/build.gradle +++ b/build.gradle @@ -16,8 +16,8 @@ plugins { id 'com.github.sherter.google-java-format' version '0.9' apply false - id 'me.champeau.jmh' version '0.6.6' apply false - id 'io.spring.dependency-management' version '1.0.11.RELEASE' apply false + id 'me.champeau.jmh' version '0.6.7' apply false + id 'io.spring.dependency-management' version '1.0.13.RELEASE' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false id 'io.github.reyerizo.gradle.jcstress' version '0.8.13' apply false id 'com.github.vlsi.gradle-extensions' version '1.76' apply false @@ -33,14 +33,14 @@ subprojects { apply plugin: 'com.github.sherter.google-java-format' apply plugin: 'com.github.vlsi.gradle-extensions' - ext['reactor-bom.version'] = '2020.0.17' + ext['reactor-bom.version'] = '2020.0.23' ext['logback.version'] = '1.2.10' - ext['netty-bom.version'] = '4.1.75.Final' - ext['netty-boringssl.version'] = '2.0.51.Final' + ext['netty-bom.version'] = '4.1.81.Final' + ext['netty-boringssl.version'] = '2.0.54.Final' ext['hdrhistogram.version'] = '2.1.12' ext['mockito.version'] = '4.4.0' ext['slf4j.version'] = '1.7.36' - ext['jmh.version'] = '1.33' + ext['jmh.version'] = '1.35' ext['junit.version'] = '5.8.1' ext['micrometer.version'] = '1.8.4' ext['assertj.version'] = '3.22.0' diff --git a/gradle.properties b/gradle.properties index e9219dfe6..7b5ac2349 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.1.2 -perfBaselineVersion=1.1.1 +version=1.1.3 +perfBaselineVersion=1.1.2 From 84af16070106f2727cb906ad4551e75b6cdc48d4 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Wed, 14 Sep 2022 00:56:34 +0300 Subject: [PATCH 080/116] Adapts to ObservationConvention location change (#1071) --- .../ObservationIntegrationTest.java | 12 +++++++++++- .../RSocketDocumentedObservation.java | 19 ++++++++++--------- ...RSocketRequesterObservationConvention.java | 5 +++-- ...RSocketResponderObservationConvention.java | 5 +++-- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java index 2bf5e42e7..870ecf0cd 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java @@ -69,7 +69,7 @@ public class ObservationIntegrationTest extends SampleTestRunner { private final RSocketInterceptor responderInterceptor; ObservationIntegrationTest() { - super(SampleRunnerConfig.builder().build(), observationRegistry, registry); + super(SampleRunnerConfig.builder().build()); requesterInterceptor = reactiveSocket -> new ObservationRequesterRSocketProxy(reactiveSocket, observationRegistry); @@ -233,4 +233,14 @@ public Mono fireAndForget(Payload payload) { // @formatter:on }; } + + @Override + protected MeterRegistry getMeterRegistry() { + return registry; + } + + @Override + protected ObservationRegistry getObservationRegistry() { + return observationRegistry; + } } diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java index 18440ad81..5d6661baf 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java @@ -18,6 +18,7 @@ import io.micrometer.common.docs.KeyName; import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.docs.DocumentedObservation; enum RSocketDocumentedObservation implements DocumentedObservation { @@ -25,7 +26,7 @@ enum RSocketDocumentedObservation implements DocumentedObservation { /** Observation created on the RSocket responder side. */ RSOCKET_RESPONDER { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketResponderObservationConvention.class; } @@ -34,7 +35,7 @@ enum RSocketDocumentedObservation implements DocumentedObservation { /** Observation created on the RSocket requester side for Fire and Forget frame type. */ RSOCKET_REQUESTER_FNF { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketRequesterObservationConvention.class; } @@ -53,7 +54,7 @@ public String getPrefix() { /** Observation created on the RSocket responder side for Fire and Forget frame type. */ RSOCKET_RESPONDER_FNF { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketResponderObservationConvention.class; } @@ -72,7 +73,7 @@ public String getPrefix() { /** Observation created on the RSocket requester side for Request Response frame type. */ RSOCKET_REQUESTER_REQUEST_RESPONSE { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketRequesterObservationConvention.class; } @@ -91,7 +92,7 @@ public String getPrefix() { /** Observation created on the RSocket responder side for Request Response frame type. */ RSOCKET_RESPONDER_REQUEST_RESPONSE { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketResponderObservationConvention.class; } @@ -110,7 +111,7 @@ public String getPrefix() { /** Observation created on the RSocket requester side for Request Stream frame type. */ RSOCKET_REQUESTER_REQUEST_STREAM { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketRequesterObservationConvention.class; } @@ -129,7 +130,7 @@ public String getPrefix() { /** Observation created on the RSocket responder side for Request Stream frame type. */ RSOCKET_RESPONDER_REQUEST_STREAM { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketResponderObservationConvention.class; } @@ -148,7 +149,7 @@ public String getPrefix() { /** Observation created on the RSocket requester side for Request Channel frame type. */ RSOCKET_REQUESTER_REQUEST_CHANNEL { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketRequesterObservationConvention.class; } @@ -167,7 +168,7 @@ public String getPrefix() { /** Observation created on the RSocket responder side for Request Channel frame type. */ RSOCKET_RESPONDER_REQUEST_CHANNEL { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketResponderObservationConvention.class; } diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java index 512c19abf..83bbef9b1 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java @@ -17,15 +17,16 @@ package io.rsocket.micrometer.observation; import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; /** - * {@link Observation.ObservationConvention} for RSocket requester {@link RSocketContext}. + * {@link ObservationConvention} for RSocket requester {@link RSocketContext}. * * @author Marcin Grzejszczak * @since 2.0.0 */ public interface RSocketRequesterObservationConvention - extends Observation.ObservationConvention { + extends ObservationConvention { @Override default boolean supportsContext(Observation.Context context) { diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java index 9599429f5..f2bbf8716 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java @@ -17,15 +17,16 @@ package io.rsocket.micrometer.observation; import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; /** - * {@link Observation.ObservationConvention} for RSocket responder {@link RSocketContext}. + * {@link ObservationConvention} for RSocket responder {@link RSocketContext}. * * @author Marcin Grzejszczak * @since 2.0.0 */ public interface RSocketResponderObservationConvention - extends Observation.ObservationConvention { + extends ObservationConvention { @Override default boolean supportsContext(Observation.Context context) { From ac96b8ebb324c08916cb17dbba55ec4cead8dfe9 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 30 Aug 2022 12:02:46 +0300 Subject: [PATCH 081/116] introduces `onClose` listener for RSocketClient Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .../rsocket/core/ReconnectMonoStressTest.java | 2 +- .../io/rsocket/core/DefaultRSocketClient.java | 15 ++++ .../java/io/rsocket/core/RSocketClient.java | 9 ++- .../io/rsocket/core/RSocketClientAdapter.java | 5 ++ .../loadbalance/LoadbalanceRSocketClient.java | 5 ++ .../io/rsocket/loadbalance/PooledRSocket.java | 23 +++++- .../io/rsocket/loadbalance/RSocketPool.java | 19 ++++- .../core/DefaultRSocketClientTests.java | 52 ++++++++++-- .../rsocket/loadbalance/LoadbalanceTest.java | 80 +++++++++++++++++++ 9 files changed, 199 insertions(+), 11 deletions(-) diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java index e01b1d704..1d3b72170 100644 --- a/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java @@ -517,7 +517,7 @@ public void arbiter(IIIIII_Result r) { id = {"1, 0, 1, 0, 1, 2"}, expect = ACCEPTABLE) @State - public static class SubscribeBlockRace extends BaseStressTest { + public static class SubscribeBlockConnectRace extends BaseStressTest { String receivedValue; diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java index 4dc250158..5119814cd 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java @@ -35,6 +35,7 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.MonoOperator; import reactor.core.publisher.Operators; +import reactor.core.publisher.Sinks; import reactor.util.annotation.Nullable; import reactor.util.context.Context; @@ -65,6 +66,8 @@ class DefaultRSocketClient extends ResolvingOperator final Mono source; + final Sinks.Empty onDisposeSink; + volatile Subscription s; static final AtomicReferenceFieldUpdater S = @@ -72,12 +75,18 @@ class DefaultRSocketClient extends ResolvingOperator DefaultRSocketClient(Mono source) { this.source = unwrapReconnectMono(source); + this.onDisposeSink = Sinks.empty(); } private Mono unwrapReconnectMono(Mono source) { return source instanceof ReconnectMono ? ((ReconnectMono) source).getSource() : source; } + @Override + public Mono onClose() { + return this.onDisposeSink.asMono(); + } + @Override public Mono source() { return Mono.fromDirect(this); @@ -194,6 +203,12 @@ protected void doOnValueExpired(RSocket value) { @Override protected void doOnDispose() { Operators.terminate(S, this); + final RSocket value = this.value; + if (value != null) { + value.onClose().subscribe(null, onDisposeSink::tryEmitError, onDisposeSink::tryEmitEmpty); + } else { + onDisposeSink.tryEmitEmpty(); + } } static final class FlatMapMain implements CoreSubscriber, Context, Scannable { diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java index 81392e661..b21a06b8b 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java @@ -15,12 +15,13 @@ */ package io.rsocket.core; +import io.rsocket.Closeable; import io.rsocket.Payload; import io.rsocket.RSocket; import org.reactivestreams.Publisher; -import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import sun.reflect.generics.reflectiveObjects.NotImplementedException; /** * Contract for performing RSocket requests. @@ -74,7 +75,11 @@ * @since 1.1 * @see io.rsocket.loadbalance.LoadbalanceRSocketClient */ -public interface RSocketClient extends Disposable { +public interface RSocketClient extends Closeable { + + default Mono onClose() { + return Mono.error(new NotImplementedException()); + } /** Return the underlying source used to obtain a shared {@link RSocket} connection. */ Mono source(); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java index cc94f4102..1537da3f8 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java @@ -46,6 +46,11 @@ public Mono source() { return Mono.just(rsocket); } + @Override + public Mono onClose() { + return rsocket.onClose(); + } + @Override public Mono fireAndForget(Mono payloadMono) { return payloadMono.flatMap(rsocket::fireAndForget); diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java index 1b677edba..0f70df06a 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java @@ -40,6 +40,11 @@ private LoadbalanceRSocketClient(RSocketPool rSocketPool) { this.rSocketPool = rSocketPool; } + @Override + public Mono onClose() { + return rSocketPool.onClose(); + } + /** Return {@code Mono} that selects an RSocket from the underlying pool. */ @Override public Mono source() { diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java index 1e7f09ec4..a77329d31 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java @@ -26,6 +26,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; +import reactor.core.publisher.Sinks; import reactor.util.context.Context; /** Default implementation of {@link RSocket} stored in {@link RSocketPool} */ @@ -35,6 +36,7 @@ final class PooledRSocket extends ResolvingOperator final RSocketPool parent; final Mono rSocketSource; final LoadbalanceTarget loadbalanceTarget; + final Sinks.Empty onCloseSink; volatile Subscription s; @@ -46,6 +48,7 @@ final class PooledRSocket extends ResolvingOperator this.parent = parent; this.rSocketSource = rSocketSource; this.loadbalanceTarget = loadbalanceTarget; + this.onCloseSink = Sinks.unsafe().empty(); } @Override @@ -155,6 +158,12 @@ void doCleanup(Throwable t) { break; } } + + if (t == ON_DISPOSE) { + this.onCloseSink.tryEmitEmpty(); + } else { + this.onCloseSink.tryEmitError(t); + } } @Override @@ -165,6 +174,13 @@ protected void doOnValueExpired(RSocket value) { @Override protected void doOnDispose() { Operators.terminate(S, this); + + final RSocket value = this.value; + if (value != null) { + value.onClose().subscribe(null, onCloseSink::tryEmitError, onCloseSink::tryEmitEmpty); + } else { + onCloseSink.tryEmitEmpty(); + } } @Override @@ -193,7 +209,12 @@ public Mono metadataPush(Payload payload) { } LoadbalanceTarget target() { - return loadbalanceTarget; + return this.loadbalanceTarget; + } + + @Override + public Mono onClose() { + return this.onCloseSink.asMono(); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java index bf6f53830..59d9678d0 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java @@ -16,6 +16,7 @@ package io.rsocket.loadbalance; import io.netty.util.ReferenceCountUtil; +import io.rsocket.Closeable; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.core.RSocketConnector; @@ -28,16 +29,18 @@ import java.util.ListIterator; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.stream.Collectors; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; +import reactor.core.publisher.Sinks; import reactor.util.annotation.Nullable; class RSocketPool extends ResolvingOperator - implements CoreSubscriber> { + implements CoreSubscriber>, Closeable { static final AtomicReferenceFieldUpdater ACTIVE_SOCKETS = AtomicReferenceFieldUpdater.newUpdater( @@ -49,6 +52,7 @@ class RSocketPool extends ResolvingOperator final DeferredResolutionRSocket deferredResolutionRSocket = new DeferredResolutionRSocket(this); final RSocketConnector connector; final LoadbalanceStrategy loadbalanceStrategy; + final Sinks.Empty onAllClosedSink = Sinks.unsafe().empty(); volatile PooledRSocket[] activeSockets; volatile Subscription s; @@ -64,6 +68,11 @@ public RSocketPool( targetPublisher.subscribe(this); } + @Override + public Mono onClose() { + return onAllClosedSink.asMono(); + } + @Override protected void doOnDispose() { Operators.terminate(S, this); @@ -72,6 +81,14 @@ protected void doOnDispose() { for (RSocket rSocket : activeSockets) { rSocket.dispose(); } + + if (activeSockets.length > 0) { + Mono.whenDelayError( + Arrays.stream(activeSockets).map(RSocket::onClose).collect(Collectors.toList())) + .subscribe(null, onAllClosedSink::tryEmitError, onAllClosedSink::tryEmitEmpty); + } else { + onAllClosedSink.tryEmitEmpty(); + } } @Override diff --git a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java index 9085f1d8f..56ddc2456 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -29,6 +29,7 @@ import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.subscriber.AssertSubscriber; import io.rsocket.util.ByteBufPayload; +import io.rsocket.util.RSocketProxy; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; @@ -457,6 +458,43 @@ public void shouldDisposeOriginalSource() { Assertions.assertThat(rule.socket.isDisposed()).isTrue(); } + @Test + public void shouldReceiveOnCloseNotificationOnDisposeOriginalSource() { + Sinks.Empty onCloseDelayer = Sinks.empty(); + ClientSocketRule rule = + new ClientSocketRule() { + @Override + protected RSocket newRSocket() { + return new RSocketProxy(super.newRSocket()) { + @Override + public Mono onClose() { + return super.onClose().and(onCloseDelayer.asMono()); + } + }; + } + }; + rule.init(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + rule.client.source().subscribe(assertSubscriber); + rule.delayer.run(); + assertSubscriber.assertTerminated().assertValueCount(1); + + rule.client.dispose(); + + Assertions.assertThat(rule.client.isDisposed()).isTrue(); + + AssertSubscriber onCloseSubscriber = AssertSubscriber.create(); + + rule.client.onClose().subscribe(onCloseSubscriber); + onCloseSubscriber.assertNotTerminated(); + + onCloseDelayer.tryEmitEmpty(); + + onCloseSubscriber.assertTerminated().assertComplete(); + + Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + } + @Test public void shouldDisposeOriginalSourceIfRacing() { for (int i = 0; i < RaceTestConstants.REPEATS; i++) { @@ -485,7 +523,7 @@ public void shouldDisposeOriginalSourceIfRacing() { } } - public static class ClientSocketRule extends AbstractSocketRule { + public static class ClientSocketRule extends AbstractSocketRule { protected RSocketClient client; protected Runnable delayer; @@ -498,14 +536,16 @@ protected void doInit() { producer = Sinks.one(); client = new DefaultRSocketClient( - producer - .asMono() - .doOnCancel(() -> socket.dispose()) - .doOnDiscard(Disposable.class, Disposable::dispose)); + Mono.defer( + () -> + producer + .asMono() + .doOnCancel(() -> socket.dispose()) + .doOnDiscard(Disposable.class, Disposable::dispose))); } @Override - protected RSocketRequester newRSocket() { + protected RSocket newRSocket() { return new RSocketRequester( connection, PayloadDecoder.ZERO_COPY, diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java index 5780737cc..fcd3ae4a9 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java @@ -19,6 +19,7 @@ import io.rsocket.RSocket; import io.rsocket.RaceTestConstants; import io.rsocket.core.RSocketConnector; +import io.rsocket.internal.subscriber.AssertSubscriber; import io.rsocket.plugins.RSocketInterceptor; import io.rsocket.test.util.TestClientTransport; import io.rsocket.transport.ClientTransport; @@ -319,6 +320,85 @@ public Flux requestChannel(Publisher source) { Assertions.assertThat(counter.get()).isEqualTo(3); } + @Test + public void shouldNotifyOnCloseWhenAllTheActiveSubscribersAreClosed() { + final AtomicInteger counter = new AtomicInteger(); + final ClientTransport mockTransport = Mockito.mock(ClientTransport.class); + final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + + Sinks.Empty onCloseSocket1 = Sinks.empty(); + Sinks.Empty onCloseSocket2 = Sinks.empty(); + + RSocket socket1 = + new RSocket() { + @Override + public Mono onClose() { + return onCloseSocket1.asMono(); + } + + @Override + public Mono fireAndForget(Payload payload) { + return Mono.empty(); + } + }; + RSocket socket2 = + new RSocket() { + @Override + public Mono onClose() { + return onCloseSocket2.asMono(); + } + + @Override + public Mono fireAndForget(Payload payload) { + return Mono.empty(); + } + }; + + Mockito.when(rSocketConnectorMock.connect(Mockito.any(ClientTransport.class))) + .then(im -> Mono.just(socket1)) + .then(im -> Mono.just(socket2)) + .then(im -> Mono.never().doOnCancel(() -> counter.incrementAndGet())); + + final TestPublisher> source = TestPublisher.create(); + final RSocketPool rSocketPool = + new RSocketPool(rSocketConnectorMock, source, new RoundRobinLoadbalanceStrategy()); + + source.next( + Arrays.asList( + LoadbalanceTarget.from("1", mockTransport), + LoadbalanceTarget.from("2", mockTransport), + LoadbalanceTarget.from("3", mockTransport))); + + StepVerifier.create(rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE)) + .expectSubscription() + .expectComplete() + .verify(Duration.ofSeconds(2)); + + StepVerifier.create(rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE)) + .expectSubscription() + .expectComplete() + .verify(Duration.ofSeconds(2)); + + rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); + + rSocketPool.dispose(); + + AssertSubscriber onCloseSubscriber = + rSocketPool.onClose().subscribeWith(AssertSubscriber.create()); + + onCloseSubscriber.assertNotTerminated(); + + onCloseSocket1.tryEmitEmpty(); + + onCloseSubscriber.assertNotTerminated(); + + onCloseSocket2.tryEmitEmpty(); + + onCloseSubscriber.assertTerminated().assertComplete(); + + Assertions.assertThat(counter.get()).isOne(); + } + static class TestRSocket extends RSocketProxy { final Sinks.Empty sink = Sinks.empty(); From 01f54580b657e373398c06908fcfef0de3d762e0 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 9 Sep 2022 13:07:33 +0300 Subject: [PATCH 082/116] introduces `.connect` method Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Co-authored-by: Rossen Stoyanchev Signed-off-by: Oleh Dokuka --- .../rsocket/core/ReconnectMonoStressTest.java | 5 + .../java/io/rsocket/core/RSocketClient.java | 11 ++ .../io/rsocket/core/RSocketClientAdapter.java | 5 + .../io/rsocket/core/ResolvingOperator.java | 24 ++++ .../loadbalance/LoadbalanceRSocketClient.java | 5 + .../loadbalance/ResolvingOperator.java | 24 ++++ .../core/DefaultRSocketClientTests.java | 114 ++++++++++++++++++ 7 files changed, 188 insertions(+) diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java index 1d3b72170..ef79d344d 100644 --- a/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java @@ -543,6 +543,11 @@ void subscribe() { reconnectMono.subscribe(stressSubscriber); } + @Actor + void connect() { + reconnectMono.resolvingInner.connect(); + } + @Arbiter public void arbiter(IIIIII_Result r) { r.r1 = stressSubscription.subscribes; diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java index b21a06b8b..32e3c229d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java @@ -77,6 +77,17 @@ */ public interface RSocketClient extends Closeable { + /** + * Connect to the remote rsocket endpoint, if not yet connected. This method is a shortcut for + * {@code RSocketClient#source().subscribe()}. + * + * @return {@code true} if an attempt to connect was triggered or if already connected, or {@code + * false} if the client is terminated. + */ + default boolean connect() { + throw new NotImplementedException(); + } + default Mono onClose() { return Mono.error(new NotImplementedException()); } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java index 1537da3f8..ae8b7da97 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java @@ -41,6 +41,11 @@ public RSocket rsocket() { return rsocket; } + @Override + public boolean connect() { + throw new UnsupportedOperationException("Connect does not apply to a server side RSocket"); + } + @Override public Mono source() { return Mono.just(rsocket); diff --git a/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java b/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java index 85c4a17a7..50bef5b70 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java @@ -331,6 +331,30 @@ protected void doOnDispose() { // no ops } + public final boolean connect() { + for (; ; ) { + final BiConsumer[] a = this.subscribers; + + if (a == TERMINATED) { + return false; + } + + if (a == READY) { + return true; + } + + if (a != EMPTY_UNSUBSCRIBED) { + // do nothing if already started + return true; + } + + if (SUBSCRIBERS.compareAndSet(this, a, EMPTY_SUBSCRIBED)) { + this.doSubscribe(); + return true; + } + } + } + final int add(BiConsumer ps) { for (; ; ) { BiConsumer[] a = this.subscribers; diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java index 0f70df06a..21ea3d836 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java @@ -45,6 +45,11 @@ public Mono onClose() { return rSocketPool.onClose(); } + @Override + public boolean connect() { + return rSocketPool.connect(); + } + /** Return {@code Mono} that selects an RSocket from the underlying pool. */ @Override public Mono source() { diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/ResolvingOperator.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/ResolvingOperator.java index a25bcc584..52f16e166 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/ResolvingOperator.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/ResolvingOperator.java @@ -327,6 +327,30 @@ protected void doOnDispose() { // no ops } + public final boolean connect() { + for (; ; ) { + final BiConsumer[] a = this.subscribers; + + if (a == TERMINATED) { + return false; + } + + if (a == READY) { + return true; + } + + if (a != EMPTY_UNSUBSCRIBED) { + // do nothing if already started + return true; + } + + if (SUBSCRIBERS.compareAndSet(this, a, EMPTY_SUBSCRIBED)) { + this.doSubscribe(); + return true; + } + } + } + final int add(BiConsumer ps) { for (; ; ) { BiConsumer[] a = this.subscribers; diff --git a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java index 56ddc2456..3c95fd65c 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -28,6 +28,7 @@ import io.rsocket.frame.PayloadFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.subscriber.AssertSubscriber; +import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.ByteBufPayload; import io.rsocket.util.RSocketProxy; import java.time.Duration; @@ -495,6 +496,88 @@ public Mono onClose() { Assertions.assertThat(rule.socket.isDisposed()).isTrue(); } + @Test + public void shouldResolveOnStartSource() { + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + Assertions.assertThat(rule.client.connect()).isTrue(); + rule.client.source().subscribe(assertSubscriber); + rule.delayer.run(); + assertSubscriber.assertTerminated().assertValueCount(1); + + rule.client.dispose(); + + Assertions.assertThat(rule.client.isDisposed()).isTrue(); + + AssertSubscriber assertSubscriber1 = AssertSubscriber.create(); + + rule.client.onClose().subscribe(assertSubscriber1); + + assertSubscriber1.assertTerminated().assertComplete(); + + Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + } + + @Test + public void shouldNotStartIfAlreadyDisposed() { + Assertions.assertThat(rule.client.connect()).isTrue(); + Assertions.assertThat(rule.client.connect()).isTrue(); + rule.delayer.run(); + + rule.client.dispose(); + + Assertions.assertThat(rule.client.connect()).isFalse(); + + Assertions.assertThat(rule.client.isDisposed()).isTrue(); + + AssertSubscriber assertSubscriber1 = AssertSubscriber.create(); + + rule.client.onClose().subscribe(assertSubscriber1); + + assertSubscriber1.assertTerminated().assertComplete(); + + Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + } + + @Test + public void shouldBeRestartedIfSourceWasClosed() { + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + AssertSubscriber terminateSubscriber = AssertSubscriber.create(); + + Assertions.assertThat(rule.client.connect()).isTrue(); + rule.client.source().subscribe(assertSubscriber); + rule.client.onClose().subscribe(terminateSubscriber); + + rule.delayer.run(); + + assertSubscriber.assertTerminated().assertValueCount(1); + + rule.socket.dispose(); + + terminateSubscriber.assertNotTerminated(); + Assertions.assertThat(rule.client.isDisposed()).isFalse(); + + rule.connection = new TestDuplexConnection(rule.allocator); + rule.socket = rule.newRSocket(); + rule.producer = Sinks.one(); + + AssertSubscriber assertSubscriber2 = AssertSubscriber.create(); + + Assertions.assertThat(rule.client.connect()).isTrue(); + rule.client.source().subscribe(assertSubscriber2); + + rule.delayer.run(); + + assertSubscriber2.assertTerminated().assertValueCount(1); + + rule.client.dispose(); + + terminateSubscriber.assertTerminated().assertComplete(); + + Assertions.assertThat(rule.client.connect()).isFalse(); + + Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + } + @Test public void shouldDisposeOriginalSourceIfRacing() { for (int i = 0; i < RaceTestConstants.REPEATS; i++) { @@ -523,6 +606,37 @@ public void shouldDisposeOriginalSourceIfRacing() { } } + @Test + public void shouldStartOriginalSourceOnceIfRacing() { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + ClientSocketRule rule = new ClientSocketRule(); + + rule.init(); + + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + + RaceTestUtils.race( + () -> rule.client.source().subscribe(assertSubscriber), () -> rule.client.connect()); + + Assertions.assertThat(rule.producer.currentSubscriberCount()).isOne(); + + rule.delayer.run(); + + assertSubscriber.assertTerminated(); + + rule.client.dispose(); + + Assertions.assertThat(rule.client.isDisposed()).isTrue(); + Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + AssertSubscriber assertSubscriber1 = AssertSubscriber.create(); + + rule.client.onClose().subscribe(assertSubscriber1); + + assertSubscriber1.assertTerminated().assertComplete(); + } + } + public static class ClientSocketRule extends AbstractSocketRule { protected RSocketClient client; From 980468854d1475bfab5da6ce7aa7d5e8b147f1af Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 14 Sep 2022 13:33:55 +0300 Subject: [PATCH 083/116] updates JMH lib version Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- benchmarks/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index 0b8bc601b..74e571d1f 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -14,8 +14,8 @@ dependencies { compileOnly "io.rsocket:rsocket-transport-local:${perfBaselineVersion}" compileOnly "io.rsocket:rsocket-transport-netty:${perfBaselineVersion}" - implementation "org.openjdk.jmh:jmh-core:1.21" - annotationProcessor "org.openjdk.jmh:jmh-generator-annprocess:1.21" + implementation "org.openjdk.jmh:jmh-core:1.35" + annotationProcessor "org.openjdk.jmh:jmh-generator-annprocess:1.35" current project(':rsocket-core') current project(':rsocket-transport-local') From 000f6da716fca29ac830d0b004082edd9d80a0b2 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 14 Sep 2022 13:34:28 +0300 Subject: [PATCH 084/116] improves `BaseDuplexConnection` and related subclasses Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .../internal/BaseDuplexConnection.java | 2 +- .../transport/netty/TcpDuplexConnection.java | 20 ++++++++----------- .../netty/WebsocketDuplexConnection.java | 20 ++++++++----------- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java index fc679c259..09026356f 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java @@ -39,7 +39,7 @@ public void sendFrame(int streamId, ByteBuf frame) { protected abstract void doOnClose(); @Override - public final Mono onClose() { + public Mono onClose() { return onClose.asMono(); } 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 85874f44d..901d1ba9a 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 @@ -26,6 +26,7 @@ import java.net.SocketAddress; import java.util.Objects; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.netty.Connection; /** An implementation of {@link DuplexConnection} that connects via TCP. */ @@ -67,24 +68,19 @@ protected void doOnClose() { connection.dispose(); } + @Override + public Mono onClose() { + return super.onClose().and(connection.onDispose()); + } + @Override public void sendErrorAndClose(RSocketErrorException e) { final ByteBuf errorFrame = ErrorFrameCodec.encode(alloc(), 0, e); connection .outbound() .sendObject(FrameLengthCodec.encode(alloc(), errorFrame.readableBytes(), errorFrame)) - .then() - .subscribe( - null, - t -> onClose.tryEmitError(t), - () -> { - final Throwable cause = e.getCause(); - if (cause == null) { - onClose.tryEmitEmpty(); - } else { - onClose.tryEmitError(cause); - } - }); + .subscribe(connection.disposeSubscriber()); + sender.onComplete(); } @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 140cfc59f..542ff3599 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 @@ -25,6 +25,7 @@ import java.net.SocketAddress; import java.util.Objects; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.netty.Connection; /** @@ -72,6 +73,11 @@ protected void doOnClose() { connection.dispose(); } + @Override + public Mono onClose() { + return super.onClose().and(connection.onDispose()); + } + @Override public Flux receive() { return connection.inbound().receive(); @@ -83,17 +89,7 @@ public void sendErrorAndClose(RSocketErrorException e) { connection .outbound() .sendObject(new BinaryWebSocketFrame(errorFrame)) - .then() - .subscribe( - null, - t -> onClose.tryEmitError(t), - () -> { - final Throwable cause = e.getCause(); - if (cause == null) { - onClose.tryEmitEmpty(); - } else { - onClose.tryEmitError(cause); - } - }); + .subscribe(connection.disposeSubscriber()); + sender.onComplete(); } } From 32da1318870753f331cc0e1750381d1db4172986 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 25 Oct 2022 18:43:50 +0200 Subject: [PATCH 085/116] adds support for Micrometer Observations (#1075) --- build.gradle | 7 +- rsocket-examples/build.gradle | 6 + .../ObservationIntegrationTest.java | 246 ++++++++++++++++++ rsocket-micrometer/build.gradle | 1 + .../micrometer/observation/ByteBufGetter.java | 36 +++ .../micrometer/observation/ByteBufSetter.java | 33 +++ .../observation/CompositeMetadataUtils.java | 40 +++ .../DefaultRSocketObservationConvention.java | 49 ++++ ...RSocketRequesterObservationConvention.java | 62 +++++ ...RSocketResponderObservationConvention.java | 61 +++++ .../ObservationRequesterRSocketProxy.java | 224 ++++++++++++++++ .../ObservationResponderRSocketProxy.java | 167 ++++++++++++ .../micrometer/observation/PayloadUtils.java | 73 ++++++ .../observation/RSocketContext.java | 76 ++++++ .../RSocketObservationDocumentation.java | 232 +++++++++++++++++ ...RSocketRequesterObservationConvention.java | 36 +++ ...ketRequesterTracingObservationHandler.java | 127 +++++++++ ...RSocketResponderObservationConvention.java | 36 +++ ...ketResponderTracingObservationHandler.java | 152 +++++++++++ .../netty/server/CloseableChannel.java | 4 +- 20 files changed, 1664 insertions(+), 4 deletions(-) create mode 100644 rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufGetter.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufSetter.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/CompositeMetadataUtils.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/PayloadUtils.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketContext.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketObservationDocumentation.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java diff --git a/build.gradle b/build.gradle index 8399c27e1..cdfdbaa2b 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,8 @@ subprojects { ext['slf4j.version'] = '1.7.36' ext['jmh.version'] = '1.35' ext['junit.version'] = '5.8.1' - ext['micrometer.version'] = '1.8.4' + ext['micrometer.version'] = '1.10.0-RC1' + ext['micrometer-tracing.version'] = '1.0.0-RC1' ext['assertj.version'] = '3.22.0' ext['netflix.limits.version'] = '0.3.6' ext['bouncycastle-bcpkix.version'] = '1.70' @@ -69,6 +70,8 @@ subprojects { 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']}" + mavenBom "io.micrometer:micrometer-bom:${ext['micrometer.version']}" + mavenBom "io.micrometer:micrometer-tracing-bom:${ext['micrometer-tracing.version']}" } dependencies { @@ -76,7 +79,6 @@ subprojects { dependency "ch.qos.logback:logback-classic:${ext['logback.version']}" dependency "io.netty:netty-tcnative-boringssl-static:${ext['netty-boringssl.version']}" dependency "org.bouncycastle:bcpkix-jdk15on:${ext['bouncycastle-bcpkix.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']}" @@ -103,6 +105,7 @@ subprojects { content { includeGroup "io.projectreactor" includeGroup "io.projectreactor.netty" + includeGroup "io.micrometer" } } diff --git a/rsocket-examples/build.gradle b/rsocket-examples/build.gradle index d03524cd9..423acec79 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -25,6 +25,10 @@ dependencies { implementation project(':rsocket-transport-netty') implementation 'com.netflix.concurrency-limits:concurrency-limits-core' + implementation "io.micrometer:micrometer-core" + implementation "io.micrometer:micrometer-tracing" + implementation project(":rsocket-micrometer") + testImplementation 'org.awaitility:awaitility' runtimeOnly 'ch.qos.logback:logback-classic' @@ -33,6 +37,8 @@ dependencies { testImplementation 'org.mockito:mockito-core' testImplementation 'org.assertj:assertj-core' testImplementation 'io.projectreactor:reactor-test' + testImplementation "io.micrometer:micrometer-test" + testImplementation "io.micrometer:micrometer-tracing-integration-test" testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' } diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java new file mode 100644 index 000000000..870ecf0cd --- /dev/null +++ b/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java @@ -0,0 +1,246 @@ +/* + * 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.integration.observation; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.core.tck.MeterRegistryAssert; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.test.SampleTestRunner; +import io.micrometer.tracing.test.reporter.BuildingBlocks; +import io.micrometer.tracing.test.simple.SpansAssert; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.micrometer.observation.ByteBufGetter; +import io.rsocket.micrometer.observation.ByteBufSetter; +import io.rsocket.micrometer.observation.ObservationRequesterRSocketProxy; +import io.rsocket.micrometer.observation.ObservationResponderRSocketProxy; +import io.rsocket.micrometer.observation.RSocketRequesterTracingObservationHandler; +import io.rsocket.micrometer.observation.RSocketResponderTracingObservationHandler; +import io.rsocket.plugins.RSocketInterceptor; +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 java.util.Deque; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class ObservationIntegrationTest extends SampleTestRunner { + private static final MeterRegistry registry = new SimpleMeterRegistry(); + private static final ObservationRegistry observationRegistry = ObservationRegistry.create(); + + static { + observationRegistry + .observationConfig() + .observationHandler(new DefaultMeterObservationHandler(registry)); + } + + private final RSocketInterceptor requesterInterceptor; + private final RSocketInterceptor responderInterceptor; + + ObservationIntegrationTest() { + super(SampleRunnerConfig.builder().build()); + requesterInterceptor = + reactiveSocket -> new ObservationRequesterRSocketProxy(reactiveSocket, observationRegistry); + + responderInterceptor = + reactiveSocket -> new ObservationResponderRSocketProxy(reactiveSocket, observationRegistry); + } + + private CloseableChannel server; + private RSocket client; + private AtomicInteger counter; + + @Override + public BiConsumer>> + customizeObservationHandlers() { + return (buildingBlocks, observationHandlers) -> { + observationHandlers.addFirst( + new RSocketRequesterTracingObservationHandler( + buildingBlocks.getTracer(), + buildingBlocks.getPropagator(), + new ByteBufSetter(), + false)); + observationHandlers.addFirst( + new RSocketResponderTracingObservationHandler( + buildingBlocks.getTracer(), + buildingBlocks.getPropagator(), + new ByteBufGetter(), + false)); + }; + } + + @AfterEach + public void teardown() { + if (server != null) { + server.dispose(); + } + } + + private void testRequest() { + counter.set(0); + client.requestResponse(DefaultPayload.create("REQUEST", "META")).block(); + assertThat(counter).as("Server did not see the request.").hasValue(1); + } + + private void testStream() { + counter.set(0); + client.requestStream(DefaultPayload.create("start")).blockLast(); + + assertThat(counter).as("Server did not see the request.").hasValue(1); + } + + private void testRequestChannel() { + counter.set(0); + client.requestChannel(Mono.just(DefaultPayload.create("start"))).blockFirst(); + assertThat(counter).as("Server did not see the request.").hasValue(1); + } + + private void testFireAndForget() { + counter.set(0); + client.fireAndForget(DefaultPayload.create("start")).subscribe(); + Awaitility.await().atMost(Duration.ofSeconds(50)).until(() -> counter.get() == 1); + assertThat(counter).as("Server did not see the request.").hasValue(1); + } + + @Override + public SampleTestRunnerConsumer yourCode() { + return (bb, meterRegistry) -> { + counter = new AtomicInteger(); + server = + RSocketServer.create( + (setup, sendingSocket) -> { + sendingSocket.onClose().subscribe(); + + return Mono.just( + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + payload.release(); + counter.incrementAndGet(); + return Mono.just(DefaultPayload.create("RESPONSE", "METADATA")); + } + + @Override + public Flux requestStream(Payload payload) { + payload.release(); + counter.incrementAndGet(); + return Flux.range(1, 10_000) + .map(i -> DefaultPayload.create("data -> " + i)); + } + + @Override + public Flux requestChannel(Publisher payloads) { + counter.incrementAndGet(); + return Flux.from(payloads); + } + + @Override + public Mono fireAndForget(Payload payload) { + payload.release(); + counter.incrementAndGet(); + return Mono.empty(); + } + }); + }) + .interceptors(registry -> registry.forResponder(responderInterceptor)) + .bind(TcpServerTransport.create("localhost", 0)) + .block(); + + client = + RSocketConnector.create() + .interceptors(registry -> registry.forRequester(requesterInterceptor)) + .connect(TcpClientTransport.create(server.address())) + .block(); + + testRequest(); + + testStream(); + + testRequestChannel(); + + testFireAndForget(); + + // @formatter:off + SpansAssert.assertThat(bb.getFinishedSpans()) + .haveSameTraceId() + // "request_*" + "handle" x 4 + .hasNumberOfSpansEqualTo(8) + .hasNumberOfSpansWithNameEqualTo("handle", 4) + .forAllSpansWithNameEqualTo("handle", span -> span.hasTagWithKey("rsocket.request-type")) + .hasASpanWithNameIgnoreCase("request_stream") + .thenASpanWithNameEqualToIgnoreCase("request_stream") + .hasTag("rsocket.request-type", "REQUEST_STREAM") + .backToSpans() + .hasASpanWithNameIgnoreCase("request_channel") + .thenASpanWithNameEqualToIgnoreCase("request_channel") + .hasTag("rsocket.request-type", "REQUEST_CHANNEL") + .backToSpans() + .hasASpanWithNameIgnoreCase("request_fnf") + .thenASpanWithNameEqualToIgnoreCase("request_fnf") + .hasTag("rsocket.request-type", "REQUEST_FNF") + .backToSpans() + .hasASpanWithNameIgnoreCase("request_response") + .thenASpanWithNameEqualToIgnoreCase("request_response") + .hasTag("rsocket.request-type", "REQUEST_RESPONSE"); + + MeterRegistryAssert.assertThat(registry) + .hasTimerWithNameAndTags( + "rsocket.response", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_RESPONSE"))) + .hasTimerWithNameAndTags( + "rsocket.fnf", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_FNF"))) + .hasTimerWithNameAndTags( + "rsocket.request", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_RESPONSE"))) + .hasTimerWithNameAndTags( + "rsocket.channel", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_CHANNEL"))) + .hasTimerWithNameAndTags( + "rsocket.stream", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_STREAM"))); + // @formatter:on + }; + } + + @Override + protected MeterRegistry getMeterRegistry() { + return registry; + } + + @Override + protected ObservationRegistry getObservationRegistry() { + return observationRegistry; + } +} diff --git a/rsocket-micrometer/build.gradle b/rsocket-micrometer/build.gradle index 128aa1aa5..40114a73b 100644 --- a/rsocket-micrometer/build.gradle +++ b/rsocket-micrometer/build.gradle @@ -23,6 +23,7 @@ plugins { dependencies { api project(':rsocket-core') api 'io.micrometer:micrometer-core' + api 'io.micrometer:micrometer-tracing' implementation 'org.slf4j:slf4j-api' diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufGetter.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufGetter.java new file mode 100644 index 000000000..09c8ba316 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufGetter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.tracing.propagation.Propagator; +import io.netty.buffer.ByteBuf; +import io.netty.util.CharsetUtil; +import io.rsocket.metadata.CompositeMetadata; + +public class ByteBufGetter implements Propagator.Getter { + + @Override + public String get(ByteBuf carrier, String key) { + final CompositeMetadata compositeMetadata = new CompositeMetadata(carrier, false); + for (CompositeMetadata.Entry entry : compositeMetadata) { + if (key.equals(entry.getMimeType())) { + return entry.getContent().toString(CharsetUtil.UTF_8); + } + } + return null; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufSetter.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufSetter.java new file mode 100644 index 000000000..678bdb1ed --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufSetter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.tracing.propagation.Propagator; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.metadata.CompositeMetadataCodec; + +public class ByteBufSetter implements Propagator.Setter { + + @Override + public void set(CompositeByteBuf carrier, String key, String value) { + final ByteBufAllocator alloc = carrier.alloc(); + CompositeMetadataCodec.encodeAndAddMetadataWithCompression( + carrier, alloc, key, ByteBufUtil.writeUtf8(alloc, value)); + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/CompositeMetadataUtils.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/CompositeMetadataUtils.java new file mode 100644 index 000000000..357be8f15 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/CompositeMetadataUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.core.lang.Nullable; +import io.netty.buffer.ByteBuf; +import io.rsocket.metadata.CompositeMetadata; + +final class CompositeMetadataUtils { + + private CompositeMetadataUtils() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + @Nullable + static ByteBuf extract(ByteBuf metadata, String key) { + final CompositeMetadata compositeMetadata = new CompositeMetadata(metadata, false); + for (CompositeMetadata.Entry entry : compositeMetadata) { + final String entryKey = entry.getMimeType(); + if (key.equals(entryKey)) { + return entry.getContent(); + } + } + return null; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java new file mode 100644 index 000000000..2c10fc78d --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.rsocket.frame.FrameType; + +/** + * Default {@link RSocketRequesterObservationConvention} implementation. + * + * @author Marcin Grzejszczak + * @since 1.1.4 + */ +class DefaultRSocketObservationConvention { + + private final RSocketContext rSocketContext; + + public DefaultRSocketObservationConvention(RSocketContext rSocketContext) { + this.rSocketContext = rSocketContext; + } + + String getName() { + if (this.rSocketContext.frameType == FrameType.REQUEST_FNF) { + return "rsocket.fnf"; + } else if (this.rSocketContext.frameType == FrameType.REQUEST_STREAM) { + return "rsocket.stream"; + } else if (this.rSocketContext.frameType == FrameType.REQUEST_CHANNEL) { + return "rsocket.channel"; + } + return "%s"; + } + + protected RSocketContext getRSocketContext() { + return this.rSocketContext; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java new file mode 100644 index 000000000..73e04b749 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java @@ -0,0 +1,62 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.util.StringUtils; +import io.micrometer.observation.Observation; +import io.rsocket.frame.FrameType; + +/** + * Default {@link RSocketRequesterObservationConvention} implementation. + * + * @author Marcin Grzejszczak + * @since 1.1.4 + */ +public class DefaultRSocketRequesterObservationConvention + extends DefaultRSocketObservationConvention implements RSocketRequesterObservationConvention { + + public DefaultRSocketRequesterObservationConvention(RSocketContext rSocketContext) { + super(rSocketContext); + } + + @Override + public KeyValues getLowCardinalityKeyValues(RSocketContext context) { + KeyValues values = + KeyValues.of( + RSocketObservationDocumentation.ResponderTags.REQUEST_TYPE.withValue( + context.frameType.name())); + if (StringUtils.isNotBlank(context.route)) { + values = + values.and(RSocketObservationDocumentation.ResponderTags.ROUTE.withValue(context.route)); + } + return values; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext; + } + + @Override + public String getName() { + if (getRSocketContext().frameType == FrameType.REQUEST_RESPONSE) { + return "rsocket.request"; + } + return super.getName(); + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java new file mode 100644 index 000000000..5318c1b37 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.util.StringUtils; +import io.micrometer.observation.Observation; +import io.rsocket.frame.FrameType; + +/** + * Default {@link RSocketRequesterObservationConvention} implementation. + * + * @author Marcin Grzejszczak + * @since 1.1.4 + */ +public class DefaultRSocketResponderObservationConvention + extends DefaultRSocketObservationConvention implements RSocketResponderObservationConvention { + + public DefaultRSocketResponderObservationConvention(RSocketContext rSocketContext) { + super(rSocketContext); + } + + @Override + public KeyValues getLowCardinalityKeyValues(RSocketContext context) { + KeyValues tags = + KeyValues.of( + RSocketObservationDocumentation.ResponderTags.REQUEST_TYPE.withValue( + context.frameType.name())); + if (StringUtils.isNotBlank(context.route)) { + tags = tags.and(RSocketObservationDocumentation.ResponderTags.ROUTE.withValue(context.route)); + } + return tags; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext; + } + + @Override + public String getName() { + if (getRSocketContext().frameType == FrameType.REQUEST_RESPONSE) { + return "rsocket.response"; + } + return super.getName(); + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java new file mode 100644 index 000000000..5a89071b4 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java @@ -0,0 +1,224 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.common.util.StringUtils; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import io.netty.buffer.ByteBuf; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.RSocketProxy; +import java.util.Iterator; +import java.util.function.Function; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** + * Tracing representation of a {@link RSocketProxy} for the requester. + * + * @author Marcin Grzejszczak + * @author Oleh Dokuka + * @since 1.1.4 + */ +public class ObservationRequesterRSocketProxy extends RSocketProxy { + + private final ObservationRegistry observationRegistry; + + private RSocketRequesterObservationConvention observationConvention; + + public ObservationRequesterRSocketProxy(RSocket source, ObservationRegistry observationRegistry) { + super(source); + this.observationRegistry = observationRegistry; + } + + @Override + public Mono fireAndForget(Payload payload) { + return setObservation( + super::fireAndForget, + payload, + FrameType.REQUEST_FNF, + RSocketObservationDocumentation.RSOCKET_REQUESTER_FNF); + } + + @Override + public Mono requestResponse(Payload payload) { + return setObservation( + super::requestResponse, + payload, + FrameType.REQUEST_RESPONSE, + RSocketObservationDocumentation.RSOCKET_REQUESTER_REQUEST_RESPONSE); + } + + Mono setObservation( + Function> input, + Payload payload, + FrameType frameType, + ObservationDocumentation observation) { + return Mono.deferContextual( + contextView -> { + if (contextView.hasKey(Observation.class)) { + Observation parent = contextView.get(Observation.class); + try (Observation.Scope scope = parent.openScope()) { + return observe(input, payload, frameType, observation); + } + } + return observe(input, payload, frameType, observation); + }); + } + + private String route(Payload payload) { + if (payload.hasMetadata()) { + try { + ByteBuf extracted = + CompositeMetadataUtils.extract( + payload.sliceMetadata(), WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()); + final RoutingMetadata routingMetadata = new RoutingMetadata(extracted); + final Iterator iterator = routingMetadata.iterator(); + return iterator.next(); + } catch (Exception e) { + + } + } + return null; + } + + private Mono observe( + Function> input, + Payload payload, + FrameType frameType, + ObservationDocumentation obs) { + String route = route(payload); + RSocketContext rSocketContext = + new RSocketContext( + payload, payload.sliceMetadata(), frameType, route, RSocketContext.Side.REQUESTER); + Observation observation = + obs.start( + this.observationConvention, + new DefaultRSocketRequesterObservationConvention(rSocketContext), + () -> rSocketContext, + observationRegistry); + setContextualName(frameType, route, observation); + Payload newPayload = payload; + if (rSocketContext.modifiedPayload != null) { + newPayload = rSocketContext.modifiedPayload; + } + return input + .apply(newPayload) + .doOnError(observation::error) + .doFinally(signalType -> observation.stop()); + } + + private Observation observation(ContextView contextView) { + if (contextView.hasKey(Observation.class)) { + return contextView.get(Observation.class); + } + return null; + } + + @Override + public Flux requestStream(Payload payload) { + return Flux.deferContextual( + contextView -> + setObservation( + super::requestStream, + payload, + contextView, + FrameType.REQUEST_STREAM, + RSocketObservationDocumentation.RSOCKET_REQUESTER_REQUEST_STREAM)); + } + + @Override + public Flux requestChannel(Publisher inbound) { + return Flux.from(inbound) + .switchOnFirst( + (firstSignal, flux) -> { + final Payload firstPayload = firstSignal.get(); + if (firstPayload != null) { + return setObservation( + p -> super.requestChannel(flux.skip(1).startWith(p)), + firstPayload, + firstSignal.getContextView(), + FrameType.REQUEST_CHANNEL, + RSocketObservationDocumentation.RSOCKET_REQUESTER_REQUEST_CHANNEL); + } + return flux; + }); + } + + private Flux setObservation( + Function> input, + Payload payload, + ContextView contextView, + FrameType frameType, + ObservationDocumentation obs) { + Observation parentObservation = observation(contextView); + if (parentObservation == null) { + return observationFlux(input, payload, frameType, obs); + } + try (Observation.Scope scope = parentObservation.openScope()) { + return observationFlux(input, payload, frameType, obs); + } + } + + private Flux observationFlux( + Function> input, + Payload payload, + FrameType frameType, + ObservationDocumentation obs) { + return Flux.deferContextual( + contextView -> { + String route = route(payload); + RSocketContext rSocketContext = + new RSocketContext( + payload, + payload.sliceMetadata(), + frameType, + route, + RSocketContext.Side.REQUESTER); + Observation newObservation = + obs.start( + this.observationConvention, + new DefaultRSocketRequesterObservationConvention(rSocketContext), + () -> rSocketContext, + this.observationRegistry); + setContextualName(frameType, route, newObservation); + return input + .apply(rSocketContext.modifiedPayload) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + }); + } + + private void setContextualName(FrameType frameType, String route, Observation newObservation) { + if (StringUtils.isNotBlank(route)) { + newObservation.contextualName(frameType.name() + " " + route); + } else { + newObservation.contextualName(frameType.name()); + } + } + + public void setObservationConvention(RSocketRequesterObservationConvention convention) { + this.observationConvention = convention; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java new file mode 100644 index 000000000..47c05f76c --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java @@ -0,0 +1,167 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.common.util.StringUtils; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.netty.buffer.ByteBuf; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.RSocketProxy; +import java.util.Iterator; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Tracing representation of a {@link RSocketProxy} for the responder. + * + * @author Marcin Grzejszczak + * @author Oleh Dokuka + * @since 1.1.4 + */ +public class ObservationResponderRSocketProxy extends RSocketProxy { + + private final ObservationRegistry observationRegistry; + + private RSocketResponderObservationConvention observationConvention; + + public ObservationResponderRSocketProxy(RSocket source, ObservationRegistry observationRegistry) { + super(source); + this.observationRegistry = observationRegistry; + } + + @Override + public Mono fireAndForget(Payload payload) { + // called on Netty EventLoop + // there can't be observation in thread local here + ByteBuf sliceMetadata = payload.sliceMetadata(); + String route = route(payload, sliceMetadata); + RSocketContext rSocketContext = + new RSocketContext( + payload, + payload.sliceMetadata(), + FrameType.REQUEST_FNF, + route, + RSocketContext.Side.RESPONDER); + Observation newObservation = + startObservation(RSocketObservationDocumentation.RSOCKET_RESPONDER_FNF, rSocketContext); + return super.fireAndForget(rSocketContext.modifiedPayload) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + } + + private Observation startObservation( + RSocketObservationDocumentation observation, RSocketContext rSocketContext) { + return observation.start( + this.observationConvention, + new DefaultRSocketResponderObservationConvention(rSocketContext), + () -> rSocketContext, + this.observationRegistry); + } + + @Override + public Mono requestResponse(Payload payload) { + ByteBuf sliceMetadata = payload.sliceMetadata(); + String route = route(payload, sliceMetadata); + RSocketContext rSocketContext = + new RSocketContext( + payload, + payload.sliceMetadata(), + FrameType.REQUEST_RESPONSE, + route, + RSocketContext.Side.RESPONDER); + Observation newObservation = + startObservation( + RSocketObservationDocumentation.RSOCKET_RESPONDER_REQUEST_RESPONSE, rSocketContext); + return super.requestResponse(rSocketContext.modifiedPayload) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + } + + @Override + public Flux requestStream(Payload payload) { + ByteBuf sliceMetadata = payload.sliceMetadata(); + String route = route(payload, sliceMetadata); + RSocketContext rSocketContext = + new RSocketContext( + payload, sliceMetadata, FrameType.REQUEST_STREAM, route, RSocketContext.Side.RESPONDER); + Observation newObservation = + startObservation( + RSocketObservationDocumentation.RSOCKET_RESPONDER_REQUEST_STREAM, rSocketContext); + return super.requestStream(rSocketContext.modifiedPayload) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + .switchOnFirst( + (firstSignal, flux) -> { + final Payload firstPayload = firstSignal.get(); + if (firstPayload != null) { + ByteBuf sliceMetadata = firstPayload.sliceMetadata(); + String route = route(firstPayload, sliceMetadata); + RSocketContext rSocketContext = + new RSocketContext( + firstPayload, + firstPayload.sliceMetadata(), + FrameType.REQUEST_CHANNEL, + route, + RSocketContext.Side.RESPONDER); + Observation newObservation = + startObservation( + RSocketObservationDocumentation.RSOCKET_RESPONDER_REQUEST_CHANNEL, + rSocketContext); + if (StringUtils.isNotBlank(route)) { + newObservation.contextualName(rSocketContext.frameType.name() + " " + route); + } + return super.requestChannel(flux.skip(1).startWith(rSocketContext.modifiedPayload)) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + } + return flux; + }); + } + + private String route(Payload payload, ByteBuf headers) { + if (payload.hasMetadata()) { + try { + final ByteBuf extract = + CompositeMetadataUtils.extract( + headers, WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()); + if (extract != null) { + final RoutingMetadata routingMetadata = new RoutingMetadata(extract); + final Iterator iterator = routingMetadata.iterator(); + return iterator.next(); + } + } catch (Exception e) { + + } + } + return null; + } + + public void setObservationConvention(RSocketResponderObservationConvention convention) { + this.observationConvention = convention; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/PayloadUtils.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/PayloadUtils.java new file mode 100644 index 000000000..e5286a53f --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/PayloadUtils.java @@ -0,0 +1,73 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.Payload; +import io.rsocket.metadata.CompositeMetadata; +import io.rsocket.metadata.CompositeMetadata.Entry; +import io.rsocket.metadata.CompositeMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.ByteBufPayload; +import io.rsocket.util.DefaultPayload; +import java.util.HashSet; +import java.util.Set; + +final class PayloadUtils { + + private PayloadUtils() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + static CompositeByteBuf cleanTracingMetadata(Payload payload, Set fields) { + Set fieldsWithDefaultZipkin = new HashSet<>(fields); + fieldsWithDefaultZipkin.add(WellKnownMimeType.MESSAGE_RSOCKET_TRACING_ZIPKIN.getString()); + final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + if (payload.hasMetadata()) { + try { + final CompositeMetadata entries = new CompositeMetadata(payload.metadata(), false); + for (Entry entry : entries) { + if (!fieldsWithDefaultZipkin.contains(entry.getMimeType())) { + CompositeMetadataCodec.encodeAndAddMetadataWithCompression( + metadata, + ByteBufAllocator.DEFAULT, + entry.getMimeType(), + entry.getContent().retain()); + } + } + } catch (Exception e) { + + } + } + return metadata; + } + + static Payload payload(Payload payload, CompositeByteBuf metadata) { + final Payload newPayload; + try { + if (payload instanceof ByteBufPayload) { + newPayload = ByteBufPayload.create(payload.data().retain(), metadata); + } else { + newPayload = DefaultPayload.create(payload.data().retain(), metadata); + } + } finally { + payload.release(); + } + return newPayload; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketContext.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketContext.java new file mode 100644 index 000000000..8622cdfa5 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketContext.java @@ -0,0 +1,76 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.common.lang.Nullable; +import io.micrometer.observation.Observation; +import io.netty.buffer.ByteBuf; +import io.rsocket.Payload; +import io.rsocket.frame.FrameType; + +public class RSocketContext extends Observation.Context { + + final Payload payload; + + final ByteBuf metadata; + + final FrameType frameType; + + final String route; + + final Side side; + + Payload modifiedPayload; + + RSocketContext( + Payload payload, ByteBuf metadata, FrameType frameType, @Nullable String route, Side side) { + this.payload = payload; + this.metadata = metadata; + this.frameType = frameType; + this.route = route; + this.side = side; + } + + public enum Side { + REQUESTER, + RESPONDER + } + + public Payload getPayload() { + return payload; + } + + public ByteBuf getMetadata() { + return metadata; + } + + public FrameType getFrameType() { + return frameType; + } + + public String getRoute() { + return route; + } + + public Side getSide() { + return side; + } + + public Payload getModifiedPayload() { + return modifiedPayload; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketObservationDocumentation.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketObservationDocumentation.java new file mode 100644 index 000000000..1be6b4599 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketObservationDocumentation.java @@ -0,0 +1,232 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +enum RSocketObservationDocumentation implements ObservationDocumentation { + + /** Observation created on the RSocket responder side. */ + RSOCKET_RESPONDER { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + }, + + /** Observation created on the RSocket requester side for Fire and Forget frame type. */ + RSOCKET_REQUESTER_FNF { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketRequesterObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return RequesterTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket responder side for Fire and Forget frame type. */ + RSOCKET_RESPONDER_FNF { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ResponderTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket requester side for Request Response frame type. */ + RSOCKET_REQUESTER_REQUEST_RESPONSE { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketRequesterObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return RequesterTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket responder side for Request Response frame type. */ + RSOCKET_RESPONDER_REQUEST_RESPONSE { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ResponderTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket requester side for Request Stream frame type. */ + RSOCKET_REQUESTER_REQUEST_STREAM { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketRequesterObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return RequesterTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket responder side for Request Stream frame type. */ + RSOCKET_RESPONDER_REQUEST_STREAM { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ResponderTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket requester side for Request Channel frame type. */ + RSOCKET_REQUESTER_REQUEST_CHANNEL { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketRequesterObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return RequesterTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket responder side for Request Channel frame type. */ + RSOCKET_RESPONDER_REQUEST_CHANNEL { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ResponderTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }; + + enum RequesterTags implements KeyName { + + /** Name of the RSocket route. */ + ROUTE { + @Override + public String asString() { + return "rsocket.route"; + } + }, + + /** Name of the RSocket request type. */ + REQUEST_TYPE { + @Override + public String asString() { + return "rsocket.request-type"; + } + }, + + /** Name of the RSocket content type. */ + CONTENT_TYPE { + @Override + public String asString() { + return "rsocket.content-type"; + } + } + } + + enum ResponderTags implements KeyName { + + /** Name of the RSocket route. */ + ROUTE { + @Override + public String asString() { + return "rsocket.route"; + } + }, + + /** Name of the RSocket request type. */ + REQUEST_TYPE { + @Override + public String asString() { + return "rsocket.request-type"; + } + } + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java new file mode 100644 index 000000000..d795f81b5 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +/** + * {@link ObservationConvention} for RSocket requester {@link RSocketContext}. + * + * @author Marcin Grzejszczak + * @since 1.1.4 + */ +public interface RSocketRequesterObservationConvention + extends ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext + && ((RSocketContext) context).side == RSocketContext.Side.REQUESTER; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java new file mode 100644 index 000000000..2cb3450d2 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java @@ -0,0 +1,127 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.TracingObservationHandler; +import io.micrometer.tracing.internal.EncodingUtils; +import io.micrometer.tracing.propagation.Propagator; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.Payload; +import io.rsocket.metadata.TracingMetadataCodec; +import java.util.HashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RSocketRequesterTracingObservationHandler + implements TracingObservationHandler { + private static final Logger log = + LoggerFactory.getLogger(RSocketRequesterTracingObservationHandler.class); + + private final Propagator propagator; + + private final Propagator.Setter setter; + + private final Tracer tracer; + + private final boolean isZipkinPropagationEnabled; + + public RSocketRequesterTracingObservationHandler( + Tracer tracer, + Propagator propagator, + Propagator.Setter setter, + boolean isZipkinPropagationEnabled) { + this.tracer = tracer; + this.propagator = propagator; + this.setter = setter; + this.isZipkinPropagationEnabled = isZipkinPropagationEnabled; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext + && ((RSocketContext) context).side == RSocketContext.Side.REQUESTER; + } + + @Override + public Tracer getTracer() { + return this.tracer; + } + + @Override + public void onStart(RSocketContext context) { + Payload payload = context.payload; + Span.Builder spanBuilder = this.tracer.spanBuilder(); + Span span = spanBuilder.kind(Span.Kind.PRODUCER).start(); + log.debug("Extracted result from context or thread local {}", span); + // TODO: newmetadata returns an empty composite byte buf + final CompositeByteBuf newMetadata = + PayloadUtils.cleanTracingMetadata(payload, new HashSet<>(propagator.fields())); + TraceContext traceContext = span.context(); + if (this.isZipkinPropagationEnabled) { + injectDefaultZipkinRSocketHeaders(newMetadata, traceContext); + } + this.propagator.inject(traceContext, newMetadata, this.setter); + context.modifiedPayload = PayloadUtils.payload(payload, newMetadata); + getTracingContext(context).setSpan(span); + } + + @Override + public void onError(RSocketContext context) { + Throwable error = context.getError(); + if (error != null) { + getRequiredSpan(context).error(error); + } + } + + @Override + public void onStop(RSocketContext context) { + Span span = getRequiredSpan(context); + tagSpan(context, span); + span.name(context.getContextualName()).end(); + } + + private void injectDefaultZipkinRSocketHeaders( + CompositeByteBuf newMetadata, TraceContext traceContext) { + TracingMetadataCodec.Flags flags = + traceContext.sampled() == null + ? TracingMetadataCodec.Flags.UNDECIDED + : traceContext.sampled() + ? TracingMetadataCodec.Flags.SAMPLE + : TracingMetadataCodec.Flags.NOT_SAMPLE; + String traceId = traceContext.traceId(); + long[] traceIds = EncodingUtils.fromString(traceId); + long[] spanId = EncodingUtils.fromString(traceContext.spanId()); + long[] parentSpanId = EncodingUtils.fromString(traceContext.parentId()); + boolean isTraceId128Bit = traceIds.length == 2; + if (isTraceId128Bit) { + TracingMetadataCodec.encode128( + newMetadata.alloc(), + traceIds[0], + traceIds[1], + spanId[0], + EncodingUtils.fromString(traceContext.parentId())[0], + flags); + } else { + TracingMetadataCodec.encode64( + newMetadata.alloc(), traceIds[0], spanId[0], parentSpanId[0], flags); + } + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java new file mode 100644 index 000000000..a5d6808bd --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +/** + * {@link ObservationConvention} for RSocket responder {@link RSocketContext}. + * + * @author Marcin Grzejszczak + * @since 1.1.4 + */ +public interface RSocketResponderObservationConvention + extends ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext + && ((RSocketContext) context).side == RSocketContext.Side.RESPONDER; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java new file mode 100644 index 000000000..e3975b577 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java @@ -0,0 +1,152 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.micrometer.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.TracingObservationHandler; +import io.micrometer.tracing.internal.EncodingUtils; +import io.micrometer.tracing.propagation.Propagator; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.Payload; +import io.rsocket.frame.FrameType; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TracingMetadata; +import io.rsocket.metadata.TracingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import java.util.HashSet; +import java.util.Iterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RSocketResponderTracingObservationHandler + implements TracingObservationHandler { + + private static final Logger log = + LoggerFactory.getLogger(RSocketResponderTracingObservationHandler.class); + + private final Propagator propagator; + + private final Propagator.Getter getter; + + private final Tracer tracer; + + private final boolean isZipkinPropagationEnabled; + + public RSocketResponderTracingObservationHandler( + Tracer tracer, + Propagator propagator, + Propagator.Getter getter, + boolean isZipkinPropagationEnabled) { + this.tracer = tracer; + this.propagator = propagator; + this.getter = getter; + this.isZipkinPropagationEnabled = isZipkinPropagationEnabled; + } + + @Override + public void onStart(RSocketContext context) { + Span handle = consumerSpanBuilder(context.payload, context.metadata, context.frameType); + CompositeByteBuf bufs = + PayloadUtils.cleanTracingMetadata(context.payload, new HashSet<>(propagator.fields())); + context.modifiedPayload = PayloadUtils.payload(context.payload, bufs); + getTracingContext(context).setSpan(handle); + } + + @Override + public void onError(RSocketContext context) { + Throwable error = context.getError(); + if (error != null) { + getRequiredSpan(context).error(error); + } + } + + @Override + public void onStop(RSocketContext context) { + Span span = getRequiredSpan(context); + tagSpan(context, span); + span.end(); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext + && ((RSocketContext) context).side == RSocketContext.Side.RESPONDER; + } + + @Override + public Tracer getTracer() { + return this.tracer; + } + + private Span consumerSpanBuilder(Payload payload, ByteBuf headers, FrameType requestType) { + Span.Builder consumerSpanBuilder = consumerSpanBuilder(payload, headers); + log.debug("Extracted result from headers {}", consumerSpanBuilder); + String name = "handle"; + if (payload.hasMetadata()) { + try { + final ByteBuf extract = + CompositeMetadataUtils.extract( + headers, WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()); + if (extract != null) { + final RoutingMetadata routingMetadata = new RoutingMetadata(extract); + final Iterator iterator = routingMetadata.iterator(); + name = requestType.name() + " " + iterator.next(); + } + } catch (Exception e) { + + } + } + return consumerSpanBuilder.kind(Span.Kind.CONSUMER).name(name).start(); + } + + private Span.Builder consumerSpanBuilder(Payload payload, ByteBuf headers) { + if (this.isZipkinPropagationEnabled && payload.hasMetadata()) { + try { + ByteBuf extract = + CompositeMetadataUtils.extract( + headers, WellKnownMimeType.MESSAGE_RSOCKET_TRACING_ZIPKIN.getString()); + if (extract != null) { + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(extract); + Span.Builder builder = this.tracer.spanBuilder(); + String traceId = EncodingUtils.fromLong(tracingMetadata.traceId()); + long traceIdHigh = tracingMetadata.traceIdHigh(); + if (traceIdHigh != 0L) { + // ExtendedTraceId + traceId = EncodingUtils.fromLong(traceIdHigh) + traceId; + } + TraceContext.Builder parentBuilder = + this.tracer + .traceContextBuilder() + .sampled(tracingMetadata.isDebug() || tracingMetadata.isSampled()) + .traceId(traceId) + .spanId(EncodingUtils.fromLong(tracingMetadata.spanId())) + .parentId(EncodingUtils.fromLong(tracingMetadata.parentId())); + return builder.setParent(parentBuilder.build()); + } else { + return this.propagator.extract(headers, this.getter); + } + } catch (Exception e) { + + } + } + return this.propagator.extract(headers, this.getter); + } +} 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 3c8192eb3..7e98905ff 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 @@ -29,7 +29,7 @@ */ public final class CloseableChannel implements Closeable { - /** For 1.0 and 1.1 compatibility: remove when RSocket requires Reactor Netty 1.0+. */ + /** For forward compatibility: remove when RSocket compiles against Reactor 1.0. */ private static final Method channelAddressMethod; static { @@ -61,7 +61,7 @@ public final class CloseableChannel implements Closeable { public InetSocketAddress address() { try { return (InetSocketAddress) channel.address(); - } catch (NoSuchMethodError e) { + } catch (ClassCastException | NoSuchMethodError e) { try { return (InetSocketAddress) channelAddressMethod.invoke(this.channel); } catch (Exception ex) { From 4e1d8b58c93879a9d3e62f5d4ee29a38495d71b9 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 27 Oct 2022 13:48:46 +0200 Subject: [PATCH 086/116] allows continuation of observations (#1076) --- gradle.properties | 2 +- .../ObservationRequesterRSocketProxy.java | 102 ++++++++---------- .../ObservationResponderRSocketProxy.java | 30 ++++-- ...ketRequesterTracingObservationHandler.java | 4 + 4 files changed, 69 insertions(+), 69 deletions(-) diff --git a/gradle.properties b/gradle.properties index 7b5ac2349..237ba8625 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.1.3 +version=1.1.4-SNAPSHOT perfBaselineVersion=1.1.2 diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java index 5a89071b4..fb80ea317 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java @@ -32,6 +32,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.annotation.Nullable; import reactor.util.context.ContextView; /** @@ -43,13 +44,24 @@ */ public class ObservationRequesterRSocketProxy extends RSocketProxy { + /** Aligned with ObservationThreadLocalAccessor#KEY */ + private static final String MICROMETER_OBSERVATION_KEY = "micrometer.observation"; + private final ObservationRegistry observationRegistry; - private RSocketRequesterObservationConvention observationConvention; + @Nullable private final RSocketRequesterObservationConvention observationConvention; public ObservationRequesterRSocketProxy(RSocket source, ObservationRegistry observationRegistry) { + this(source, observationRegistry, null); + } + + public ObservationRequesterRSocketProxy( + RSocket source, + ObservationRegistry observationRegistry, + RSocketRequesterObservationConvention observationConvention) { super(source); this.observationRegistry = observationRegistry; + this.observationConvention = observationConvention; } @Override @@ -76,15 +88,7 @@ Mono setObservation( FrameType frameType, ObservationDocumentation observation) { return Mono.deferContextual( - contextView -> { - if (contextView.hasKey(Observation.class)) { - Observation parent = contextView.get(Observation.class); - try (Observation.Scope scope = parent.openScope()) { - return observe(input, payload, frameType, observation); - } - } - return observe(input, payload, frameType, observation); - }); + contextView -> observe(input, payload, frameType, observation, contextView)); } private String route(Payload payload) { @@ -107,18 +111,22 @@ private Mono observe( Function> input, Payload payload, FrameType frameType, - ObservationDocumentation obs) { + ObservationDocumentation obs, + ContextView contextView) { String route = route(payload); RSocketContext rSocketContext = new RSocketContext( payload, payload.sliceMetadata(), frameType, route, RSocketContext.Side.REQUESTER); + Observation parentObservation = contextView.getOrDefault(MICROMETER_OBSERVATION_KEY, null); Observation observation = - obs.start( - this.observationConvention, - new DefaultRSocketRequesterObservationConvention(rSocketContext), - () -> rSocketContext, - observationRegistry); + obs.observation( + this.observationConvention, + new DefaultRSocketRequesterObservationConvention(rSocketContext), + () -> rSocketContext, + observationRegistry) + .parentObservation(parentObservation); setContextualName(frameType, route, observation); + observation.start(); Payload newPayload = payload; if (rSocketContext.modifiedPayload != null) { newPayload = rSocketContext.modifiedPayload; @@ -126,26 +134,17 @@ private Mono observe( return input .apply(newPayload) .doOnError(observation::error) - .doFinally(signalType -> observation.stop()); - } - - private Observation observation(ContextView contextView) { - if (contextView.hasKey(Observation.class)) { - return contextView.get(Observation.class); - } - return null; + .doFinally(signalType -> observation.stop()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, observation)); } @Override public Flux requestStream(Payload payload) { - return Flux.deferContextual( - contextView -> - setObservation( - super::requestStream, - payload, - contextView, - FrameType.REQUEST_STREAM, - RSocketObservationDocumentation.RSOCKET_REQUESTER_REQUEST_STREAM)); + return observationFlux( + super::requestStream, + payload, + FrameType.REQUEST_STREAM, + RSocketObservationDocumentation.RSOCKET_REQUESTER_REQUEST_STREAM); } @Override @@ -155,10 +154,9 @@ public Flux requestChannel(Publisher inbound) { (firstSignal, flux) -> { final Payload firstPayload = firstSignal.get(); if (firstPayload != null) { - return setObservation( + return observationFlux( p -> super.requestChannel(flux.skip(1).startWith(p)), firstPayload, - firstSignal.getContextView(), FrameType.REQUEST_CHANNEL, RSocketObservationDocumentation.RSOCKET_REQUESTER_REQUEST_CHANNEL); } @@ -166,21 +164,6 @@ public Flux requestChannel(Publisher inbound) { }); } - private Flux setObservation( - Function> input, - Payload payload, - ContextView contextView, - FrameType frameType, - ObservationDocumentation obs) { - Observation parentObservation = observation(contextView); - if (parentObservation == null) { - return observationFlux(input, payload, frameType, obs); - } - try (Observation.Scope scope = parentObservation.openScope()) { - return observationFlux(input, payload, frameType, obs); - } - } - private Flux observationFlux( Function> input, Payload payload, @@ -196,17 +179,22 @@ private Flux observationFlux( frameType, route, RSocketContext.Side.REQUESTER); + Observation parentObservation = + contextView.getOrDefault(MICROMETER_OBSERVATION_KEY, null); Observation newObservation = - obs.start( - this.observationConvention, - new DefaultRSocketRequesterObservationConvention(rSocketContext), - () -> rSocketContext, - this.observationRegistry); + obs.observation( + this.observationConvention, + new DefaultRSocketRequesterObservationConvention(rSocketContext), + () -> rSocketContext, + this.observationRegistry) + .parentObservation(parentObservation); setContextualName(frameType, route, newObservation); + newObservation.start(); return input .apply(rSocketContext.modifiedPayload) .doOnError(newObservation::error) - .doFinally(signalType -> newObservation.stop()); + .doFinally(signalType -> newObservation.stop()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); }); } @@ -217,8 +205,4 @@ private void setContextualName(FrameType frameType, String route, Observation ne newObservation.contextualName(frameType.name()); } } - - public void setObservationConvention(RSocketRequesterObservationConvention convention) { - this.observationConvention = convention; - } } diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java index 47c05f76c..9ed27adf3 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java @@ -30,6 +30,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.annotation.Nullable; /** * Tracing representation of a {@link RSocketProxy} for the responder. @@ -39,14 +40,24 @@ * @since 1.1.4 */ public class ObservationResponderRSocketProxy extends RSocketProxy { + /** Aligned with ObservationThreadLocalAccessor#KEY */ + private static final String MICROMETER_OBSERVATION_KEY = "micrometer.observation"; private final ObservationRegistry observationRegistry; - private RSocketResponderObservationConvention observationConvention; + @Nullable private final RSocketResponderObservationConvention observationConvention; public ObservationResponderRSocketProxy(RSocket source, ObservationRegistry observationRegistry) { + this(source, observationRegistry, null); + } + + public ObservationResponderRSocketProxy( + RSocket source, + ObservationRegistry observationRegistry, + RSocketResponderObservationConvention observationConvention) { super(source); this.observationRegistry = observationRegistry; + this.observationConvention = observationConvention; } @Override @@ -66,7 +77,8 @@ public Mono fireAndForget(Payload payload) { startObservation(RSocketObservationDocumentation.RSOCKET_RESPONDER_FNF, rSocketContext); return super.fireAndForget(rSocketContext.modifiedPayload) .doOnError(newObservation::error) - .doFinally(signalType -> newObservation.stop()); + .doFinally(signalType -> newObservation.stop()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); } private Observation startObservation( @@ -94,7 +106,8 @@ public Mono requestResponse(Payload payload) { RSocketObservationDocumentation.RSOCKET_RESPONDER_REQUEST_RESPONSE, rSocketContext); return super.requestResponse(rSocketContext.modifiedPayload) .doOnError(newObservation::error) - .doFinally(signalType -> newObservation.stop()); + .doFinally(signalType -> newObservation.stop()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); } @Override @@ -109,7 +122,8 @@ public Flux requestStream(Payload payload) { RSocketObservationDocumentation.RSOCKET_RESPONDER_REQUEST_STREAM, rSocketContext); return super.requestStream(rSocketContext.modifiedPayload) .doOnError(newObservation::error) - .doFinally(signalType -> newObservation.stop()); + .doFinally(signalType -> newObservation.stop()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); } @Override @@ -137,7 +151,9 @@ public Flux requestChannel(Publisher payloads) { } return super.requestChannel(flux.skip(1).startWith(rSocketContext.modifiedPayload)) .doOnError(newObservation::error) - .doFinally(signalType -> newObservation.stop()); + .doFinally(signalType -> newObservation.stop()) + .contextWrite( + context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); } return flux; }); @@ -160,8 +176,4 @@ private String route(Payload payload, ByteBuf headers) { } return null; } - - public void setObservationConvention(RSocketResponderObservationConvention convention) { - this.observationConvention = convention; - } } diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java index 2cb3450d2..996267d4a 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java @@ -69,6 +69,10 @@ public Tracer getTracer() { public void onStart(RSocketContext context) { Payload payload = context.payload; Span.Builder spanBuilder = this.tracer.spanBuilder(); + Span parentSpan = getParentSpan(context); + if (parentSpan != null) { + spanBuilder.setParent(parentSpan.context()); + } Span span = spanBuilder.kind(Span.Kind.PRODUCER).start(); log.debug("Extracted result from context or thread local {}", span); // TODO: newmetadata returns an empty composite byte buf From 3d7a0e230c2336893ab5e364d47eacf7a6ab5e18 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Thu, 27 Oct 2022 14:49:40 +0300 Subject: [PATCH 087/116] removes snapshot from version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 237ba8625..985394954 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.1.4-SNAPSHOT +version=1.1.4 perfBaselineVersion=1.1.2 From b730d91417307971602899536b9a7918243320b3 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Thu, 27 Oct 2022 14:49:57 +0300 Subject: [PATCH 088/116] updates baseline version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 985394954..7f8f4ca23 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,4 +12,4 @@ # limitations under the License. # version=1.1.4 -perfBaselineVersion=1.1.2 +perfBaselineVersion=1.1.3 From cdecc516eaf9e5d933f4b3dfb2e4a7026406a76c Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 2 Nov 2022 09:18:27 +0200 Subject: [PATCH 089/116] tries publishing snapshot to sonotype Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- .github/workflows/gradle-main.yml | 6 +++++- gradle/publications.gradle | 10 ---------- gradle/sonotype.gradle | 4 +++- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/gradle-main.yml b/.github/workflows/gradle-main.yml index 469ccb103..33bca8e72 100644 --- a/.github/workflows/gradle-main.yml +++ b/.github/workflows/gradle-main.yml @@ -142,10 +142,14 @@ jobs: run: chmod +x gradlew - name: Publish Packages to Artifactory if: ${{ matrix.jdk == '1.8' }} - run: ./gradlew -PversionSuffix="-SNAPSHOT" -PbuildNumber="${buildNumber}" publishMavenPublicationToGitHubPackagesRepository --no-daemon --stacktrace + run: ./gradlew -PversionSuffix="-SNAPSHOT" -PbuildNumber="${buildNumber}" publishMavenPublicationToSonatypeRepository --no-daemon --stacktrace env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} buildNumber: ${{ github.run_number }} + ORG_GRADLE_PROJECT_signingKey: ${{secrets.signingKey}} + ORG_GRADLE_PROJECT_signingPassword: ${{secrets.signingPassword}} + ORG_GRADLE_PROJECT_sonatypeUsername: ${{secrets.sonatypeUsername}} + ORG_GRADLE_PROJECT_sonatypePassword: ${{secrets.sonatypePassword}} - name: Aggregate test reports with ciMate if: always() continue-on-error: true diff --git a/gradle/publications.gradle b/gradle/publications.gradle index 97704e701..9e8dd6d88 100644 --- a/gradle/publications.gradle +++ b/gradle/publications.gradle @@ -21,16 +21,6 @@ subprojects { } } developers { - 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' diff --git a/gradle/sonotype.gradle b/gradle/sonotype.gradle index 1effd76b0..f339079b0 100644 --- a/gradle/sonotype.gradle +++ b/gradle/sonotype.gradle @@ -20,7 +20,9 @@ subprojects { repositories { maven { name = "sonatype" - url = "https://oss.sonatype.org/service/local/staging/deploy/maven2" + url = project.version.contains("-SNAPSHOT") + ? "https://oss.sonatype.org/content/repositories/snapshots/" + : "https://oss.sonatype.org/service/local/staging/deploy/maven2" credentials { username project.findProperty("sonatypeUsername") password project.findProperty("sonatypePassword") From 40ce6c35965a03f32e79351cfc24c433f1f10049 Mon Sep 17 00:00:00 2001 From: Alex079 <21042652+Alex079@users.noreply.github.com> Date: Wed, 5 Apr 2023 08:31:11 +0200 Subject: [PATCH 090/116] ensures LoadbalancedRSocket select new rsocket upon re-subscription RC Co-authored-by: alex079 <> --- rsocket-core/build.gradle | 2 +- .../loadbalance/LoadbalanceRSocketClient.java | 6 +- .../LoadbalanceRSocketClientTest.java | 91 +++++++++++++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index cd8595216..6f2056da0 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -34,7 +34,7 @@ dependencies { testImplementation 'org.assertj:assertj-core' testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-params' - testImplementation 'org.mockito:mockito-core' + testImplementation 'org.mockito:mockito-junit-jupiter' testImplementation 'org.awaitility:awaitility' testRuntimeOnly 'ch.qos.logback:logback-classic' diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java index 21ea3d836..d59cbb86e 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java @@ -27,8 +27,8 @@ import reactor.util.annotation.Nullable; /** - * An implementation of {@link RSocketClient backed by a pool of {@code RSocket} instances and using a {@link - * LoadbalanceStrategy} to select the {@code RSocket} to use for a given request. + * An implementation of {@link RSocketClient} backed by a pool of {@code RSocket} instances and + * using a {@link LoadbalanceStrategy} to select the {@code RSocket} to use for a given request. * * @since 1.1 */ @@ -73,7 +73,7 @@ public Flux requestStream(Mono payloadMono) { @Override public Flux requestChannel(Publisher payloads) { - return rSocketPool.select().requestChannel(payloads); + return source().flatMapMany(rSocket -> rSocket.requestChannel(payloads)); } @Override diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java new file mode 100644 index 000000000..c838d704c --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java @@ -0,0 +1,91 @@ +package io.rsocket.loadbalance; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketClient; +import io.rsocket.core.RSocketConnector; +import io.rsocket.transport.ClientTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +@ExtendWith(MockitoExtension.class) +class LoadbalanceRSocketClientTest { + + @Mock private ClientTransport clientTransport; + @Mock private RSocketConnector rSocketConnector; + + public static final Duration SHORT_DURATION = Duration.ofMillis(25); + public static final Duration LONG_DURATION = Duration.ofMillis(75); + + private static final Publisher SOURCE = + Flux.interval(SHORT_DURATION).map(String::valueOf).map(DefaultPayload::create); + + private static final Mono PROGRESSING_HANDLER = + Mono.just( + new RSocket() { + private final AtomicInteger i = new AtomicInteger(); + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + .delayElements(SHORT_DURATION) + .map(Payload::getDataUtf8) + .map(DefaultPayload::create) + .take(i.incrementAndGet()); + } + }); + + @Test + void testChannelReconnection() { + when(rSocketConnector.connect(clientTransport)).thenReturn(PROGRESSING_HANDLER); + + RSocketClient client = + LoadbalanceRSocketClient.create( + rSocketConnector, + Mono.just(singletonList(LoadbalanceTarget.from("key", clientTransport)))); + + Publisher result = + client + .requestChannel(SOURCE) + .repeatWhen(longFlux -> longFlux.delayElements(LONG_DURATION).take(5)) + .map(Payload::getDataUtf8) + .log(); + + StepVerifier.create(result) + .expectSubscription() + .assertNext(s -> assertThat(s).isEqualTo("0")) + .assertNext(s -> assertThat(s).isEqualTo("0")) + .assertNext(s -> assertThat(s).isEqualTo("1")) + .assertNext(s -> assertThat(s).isEqualTo("0")) + .assertNext(s -> assertThat(s).isEqualTo("1")) + .assertNext(s -> assertThat(s).isEqualTo("2")) + .assertNext(s -> assertThat(s).isEqualTo("0")) + .assertNext(s -> assertThat(s).isEqualTo("1")) + .assertNext(s -> assertThat(s).isEqualTo("2")) + .assertNext(s -> assertThat(s).isEqualTo("3")) + .assertNext(s -> assertThat(s).isEqualTo("0")) + .assertNext(s -> assertThat(s).isEqualTo("1")) + .assertNext(s -> assertThat(s).isEqualTo("2")) + .assertNext(s -> assertThat(s).isEqualTo("3")) + .assertNext(s -> assertThat(s).isEqualTo("4")) + .verifyComplete(); + + verify(rSocketConnector).connect(clientTransport); + verifyNoMoreInteractions(rSocketConnector, clientTransport); + } +} From 00d8311a7436fd2421dbcefb13f744bc4973184e Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Wed, 5 Apr 2023 10:05:10 +0300 Subject: [PATCH 091/116] updates libs versions and test run config Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- build.gradle | 14 +- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 263 ++++++++++++++--------- gradlew.bat | 14 +- rsocket-transport-netty/build.gradle | 5 + 6 files changed, 179 insertions(+), 119 deletions(-) diff --git a/build.gradle b/build.gradle index cdfdbaa2b..079a0e1d9 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ plugins { id 'com.github.sherter.google-java-format' version '0.9' apply false id 'me.champeau.jmh' version '0.6.7' apply false - id 'io.spring.dependency-management' version '1.0.13.RELEASE' apply false + id 'io.spring.dependency-management' version '1.0.15.RELEASE' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false id 'io.github.reyerizo.gradle.jcstress' version '0.8.13' apply false id 'com.github.vlsi.gradle-extensions' version '1.76' apply false @@ -33,17 +33,17 @@ subprojects { apply plugin: 'com.github.sherter.google-java-format' apply plugin: 'com.github.vlsi.gradle-extensions' - ext['reactor-bom.version'] = '2020.0.23' + ext['reactor-bom.version'] = '2020.0.31-SNAPSHOT' ext['logback.version'] = '1.2.10' - ext['netty-bom.version'] = '4.1.81.Final' - ext['netty-boringssl.version'] = '2.0.54.Final' + ext['netty-bom.version'] = '4.1.90.Final' + ext['netty-boringssl.version'] = '2.0.59.Final' ext['hdrhistogram.version'] = '2.1.12' ext['mockito.version'] = '4.4.0' ext['slf4j.version'] = '1.7.36' ext['jmh.version'] = '1.35' ext['junit.version'] = '5.8.1' - ext['micrometer.version'] = '1.10.0-RC1' - ext['micrometer-tracing.version'] = '1.0.0-RC1' + ext['micrometer.version'] = '1.10.0' + ext['micrometer-tracing.version'] = '1.0.0' ext['assertj.version'] = '3.22.0' ext['netflix.limits.version'] = '0.3.6' ext['bouncycastle-bcpkix.version'] = '1.70' @@ -174,8 +174,6 @@ subprojects { } } - forkEvery = 1 - if (isCiServer) { def stdout = new LinkedList() beforeTest { TestDescriptor td -> diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 21931 zcmaI6V~n8R6E)b==Cp0wc2C>3ZQD=Vwry+L)3)8ywrx-EZ{KV-`6rwGaFa@IRdPR6 z)u~hG4$gort%Eht{y(MI%kt z0Y0nYm>z`rdM7Lh=##-Ps^6h>FU7m~cgyxqs;Nqi&~ytk^e7KkJL>mWt4%qL*DKv= zcgsip(fRo@w)aGHJ&cRiJs;2cc4v+b>Y#M1j_&4}9i`o^*Uzg;mkN44%!|HxGTNmY za%+!%)BkmU@yFRSA8-3+6za3Rpa>0d>aP z|6x$gEo6tjC%O4IHwK@zhTuzcDM38z%iFcrUhI%h?s07}F{H1l!3u%>r`EgBk|m$r z87XPla{FK=fulv&qhyZ!oAD=l1}cy0X;ZOYTNqV6ux_FyBqy_7sRMe%ATeaSNf3#n zOHbG+%dn12N=ywJWtQcx6Vgpi+L_Aqs+4YL0kAFnwH`6{_7&pk8r>@_Sny}{j|w^r zLwLjOoTacOZKW)xkrBEW;+RmJLgpQK^{Q}vgg3n+^)Vw+pd)tvl37o*JRsA1Kbtr& zZNxVRV*JxYrwfU#Eet%gT$cq^7wurj4!-w)gR+f|=z6GTNnLF}F% zyYZeGV{!;%ZnkOP%w9!_VmGqu&WcTF*+vHiL}YHYZUe^Y0{djWLG^Go2y*z_pek+h zHj7WjmG0S6)jN(4zViLQbm-Ap2>C=?GRqH?R0!u95VvshKy^ew)53}k#lg#Y2yl7= z9Z^hYIZKXs3L3Yx2)!c? z;Kx4g%hVUnY!fQi3^`@vHe?08(_)T6K)gL-8ySjtjFyR1&(8SX3+N<&Mq8sLxve~z zzAV>jq2O*jsJ1)7Jh{io`FJPg@INV_KcD>*0$9G~#NO;Zs0ssiX)cDYrr>NMg|ueU zfPDk!onCalx;;Tp;eLRfhYXEb1XXOHJi=Hm#W4zEmHU^dH4Ei4`GGr`xhV#r~yJKHLGIJQyU&h%j=sVb-S?Wx&QV9@(T$Y)QhJt|4A~U}c zcsipTok4DLxZY?S?pG@X8?#Ckt%hhQ1&vrL320UYq)O%UJCrVJv!fbvGdr`yl$m&x zS5(FPkgt?3(L*qab)6Sg=}c%%Y%)(%!F*F-G6WkAyTZ$e!jKnM7X{96lH!+Zr%Gfd zM(2EUxW0s_M%j|w@E{uY3MxRqqR3)CbX6%kIhGph!o-r&l93|=XRTYv+VqLZTkF-i z?fE=YV<+!qSV+KfdFjsVP^5?Eu0prF$I^oyAKFP<9;h#ke&W<_dyrcR8uFiq!x zuhJ99bAm~;x|HpTHl66_p*LNw9Qi3V$0SxTI3TJAeP#c{s6Nb{Mm=_45nKr550Q#fz5ZEAv3 z&}MY$SXbrSQo^%cWPCD?rZ{p@@<*u|3m=;L&#_yl7Vk063P=Z6w*+mu+Pn@-mE%zg z*494lJ#6X(B_T0_GG_X=_5=SB$MfqaW?waGXzxGQbFnJ4S^*~w^C?BdgJ+-}404_s z)3Wn{!Zfk1(~redky}&R+amHQ1;KF3%5HVz9e(^EOE=b`}a?DLEs3Sax>ZOkn5mBnnu@!WcUnC|gK1(OfE7 zsX#cWxT>bc58uUVCq}{>jyg5GLQ7Nd?m_(#Hwoh!(X&#FN6Ums z+X!9VKu|p&$PWHUVcZyZlZ(LQ$U0+)dM%22Jz$<=k}+dKOCVkyyd4pZ^mEUh(l`B0 zpGQ_y25>@_cx4a9At)&sq$s8015AA~>R zUU$W#q`Km>izXR~7{ccVrRaUbl7iw9))M>FlT{V=qXl~^w!|8Q4LU_qH$|rCr}AjM z6hhys6DdDXoI^jz06n4I=OXKkt(ls9_d&!CJ9)bUGiD6Ow3^nurrxGSLzsX8KQh0%pBpSH#o z13n-moFP;!N$rQ-Nmiv>O6(@FNamVg3GzYWmDy1(i4m0}BAsaMHv3IaiR>4iA;ao} zK9abGwb(uK%%foHY(9A=>qBL^Jf12)tAiZ!gJR>0Rr~S#_-Z12NH&0B#6gQBl zWQ;zxGLAIqD0!7n6U^faRR%Ou&|QPA<)E1Jf8~WVuZ)XoSRudGC>@D#)|#tm%e`^A zD|^v{R?0es6ZS$t+@F|HQHP#ygZW;&fj(N?02&8@Ad5sH-I%`x&V0)`?5dc z$Lf$17$pl=q%9=1=ezsFkQM!G2A9o#PEQ^ubCt-5tnSz@2?M(c9_qUD+7LRJ26h&O zDbX@|*wXEoN!X)mI~9Pn?!tn^nz|4aL2wU|&*siR=lIPWU*fNkYW17WB#g9!iNn zYOH@~;oBN9K5KCW6{|kjxAOKdMs4i?Wpm&uT zUeI-Jk&(sHChg*t(I|;1$f7jtDPb%s1~8H>9bE3;Q^nn$O31%{k&)IMbz#sd8Cz1r zJ`urAk}O!Y;U`%q)0cH{@J-xYs>B9rwpK7<)& zA>_DT9h=CRaxm?#(~p;~{;rj4vF~%g;^?d?c7waRU|MiUl>f8QFDT^pV>GcJ#&tel zmau7PXprj6y(4DX(MtH-)jA2XzO7x_BINY6e)0OR@QK9V?9-+$7J2`dZ1yFyH?17QneiwTs5?R_8i%vW~j=NRA|~l z8#tikYP7IcHabK&IMU>3qSZ6x9S9o?UF~Z^-(do;OX)qQ$%~iBq^AMNXyD5wKl5&GaljASzVc#d5k zH|hy+XO5cGPNcz*)gCfW5o5F|G}EU;QRK<%Y(#KwLJ|*S#ekc^<~ZDkCNgwKgTBY= ziow^LRQcL{88KBgo1Pw;PfcZ!R#-@fr?eMn$n|@5gxO))jZeSl+y~u2wHl%e2U;VP zK>v9->T0=a!zaW5#lElaJ_J~CzuM&+JX!*Nfak$AIiwNuou@|Hxb(XZr>-vq-CDc` ziO|wR)DPuqU2oh2e$04u>uO=w%ud0pIflJc@ao&8PD^{sRRsYqP3-Ux(<3gJC6#PVyV9(iQ_TQ!$e{hBmZO2(UQ!NxhwND4s;Ow|; z3-R$W;tCcAsNqqne}Ua-W{A%Zz~lferyX9)eKDan8SG4y{5K1Y*T1s&BDCF3Pgxh) zIUCZ4T2)A9a6M-SKHBZ~z;ropiAA0P)m+h=T{-$qG;*HYeko4rVON}>+!idY} zZrJjxxKf2mK5t@oPIB$!iB}s(?G^5mBVz($^;oa1I)x)Td-8I!TLly4_gw%OC#RyK zalPpfGkYha{D-|YYjjUr6`r!T?I`oOnTn;%XX|C5ul{pFtEtKw4KHM4GPTyztB?6*e#|DZjfe=Sum9vhKmO z$Zxmjc4~UFEs}yELZ4V~I3@Mc7BN|vpMyA$6lhvXtv+g)@DX}9nZc&|0mg@MaXm`!i_F2yX`JC@XG6LSZ&?M$YY5bV&)MojT z#knO+ciCJ-N0cu*shmA0+mLjnW+e*qfBakQvp}q%q`>gqsJEa6bR#?WasO%C)5YXW@Q{@!t7wW# z;0zvdiYtIe;8o*w7jSX;5r-U1f*GfDuO(2R zyLyRLsXP27^)WCI(P^a*3m9?BVMS64pc07M?apF!Js_cQ)r~4Z>Mx0#g!FbC76K)t zb;v($uR6dHN$<5+OZEy2EV@W_F;hsf&D^*ZEhYK0S<}qR4Tg|fTi7?6?S7;z57DqjGnsM|B?}GQBIoCMW z7;?d5??`t*A!6WjoNk?_mqaiMtA5sSX@8EFPdliC*X9&Xylp?`$h9#-OO+2+)lb|| zR>aONPcokH1$^~6y1s<8#sq!O=6qIBRGYRm09r~Vt!I_TW!BteYe6OZ zWCoC38)tV!!WkK2|wwdL1&H`i=xHN(_uu}LKRS@<(G zTd8F``wfkv0N$&;k)9`N9wo<_k#wmB?9$^$NVBpeqfx^4o`83?7GIq`vJ|o9xv~;v zulzdp0$Wz>)Ewd*iw?A(Ojg(roGxfEz7brudm#=-P=|Ru_1vx7TShCRESpT8ft|fM z&IZZzDiKEWp73Xo#PA3PhkmT8V%~nM3esoNpEj=$0Kdv$udywmW;Z$q|2=LeibNS9 zNh2Sh@+hs&=^usu9&bTONeG{)9;&_@w0+d~0KQU(Io6zELe1g)_TXN_eFxQBg#_6! zP<=7RZHj87LWe#4B&@Xbz6%@$@$dtga7L2FPa;m_n_IC3l-iGwPs1!746PLaeG|XSa2z)5oyChBbAXH(` z#ymUnCbE)px)k!1G9OLY7P?Z`!jRIrITY@Gp#pjspEFz6=d+evYSyV9cgu@^FFll6 zO`%dJ**Dp~cYZH8kwsndIEy1!iS-GT{QV3?HAb5gntpJ{{0V~#%01OxmT*qCvfCE9!iY`VAQPoJSa zxc-_-U5a*#O5Hlg&~Oar(r`b%4Uzggy!k0~TeYIhlfs{Q^$iAl5Cqx-aQv=681LtF zeB(0o>9PP9wV$4+2m%Uw55q5@^K{75%JXy&bJ^XSgUj8*Z0xYBRk|mI%eprtclAL9 z|G}E~saucYQ7VD{FlMA!HH6vk0ZiKN5fP0AD4P1=bVlUqQX0<4dJ#!$^;ed{v!fy_ z_FQKC=;gO%A^-7-Q6RTC-GDjDxD{9;Hu6Sr& z;c6VJ1j=5TN64w9G&f3K^_o~}o~nCT$rv%iF{V1I3Z*e+Wu63%Bvm)L4Q2$S=B^o9(5o=31ZCmFI26hH_lnT%Sij zZxhvc1kSK2Q!_)=MZbNl6DD@zQE`_^ZNzjNDNv}l{#Gef_il-QZ4*Ecs@ z)Es=MTB>Won(zlq=IUz8ySo0=BJy6I!?^>$Umjns&SBl%Aw{k-vC*`m@=jwjLvj+w};ZAuW=)mtkL)thl>Bur^tS>&^p| zLa=P6iy0#~hgSaf4lB-!Z9&(`%(1&`AXbeXin)F~wI^LGzlp;cn7{kQ->Ie`KJ=G@ zXF3u3r~8a-Yhcs^#50ezgowq#0jDviI|k)CMX-*8ScLW&Nk8@tAi z$rNWPlV~K$Wl6dSL*NBKYr7UjL`Yy#FD-{h8Xqm|iBlf4oK)i7aT<+W$P|*0XOcWg zg}JjQ*Y~X&A&M|s1N0vrmaj!8;(q*5gvDXu;CFE5K_lF>$?!{5BF*D)nFyW@bYhrr z?8|G(l+0%8E{r$sBtw~mpfLx68$YGUOA)cZ#!t~c+=_O~&^XZLX}cBnzF-N*m?bhW z6r84_Dn|s%1CV&ISf9Wkc*;XFXgurH6vQCQNsPplMin@d0s<_UI3YblR)ZRe(Rl6J z@>o`C?Bfw8Ogn2jCF|(bIcdWX7PV6@S*8-Xbi0Y-8Li;O8g+`ZaUOL-SuwMRX=%~pG&K}Nt^i-;;w$XXxT9f~ik@na#9S**V?%q1XKkR~1TAH`Gn)sW z8T!|PCry4k12-3mJtzO6;Z7pI+YWRKL1 zvn6Jr_zD>-IKpZDXyz?h>~kiiqa>poo`)02#(dW@!g)6hyHj*W+@p37|6qp$1R?%M z+m-X#{*e)`ysA9rjpSqenZ31Of5-FFFD7-BEZ#UnqS=6l(gyC4UxX`$@)u8kcB&MY zpIRB34Y8pjz$E_1bJ+gz5&oJ%URolAX?PBkNk|>AA zUpx(ej2n5m$4p#l?kH6=mn6-}4@}s9Zo>};duh{;=2RG0g`5(wIICnhk z>e`Em6)}esmor3=VM%xM0V6v{7Gf@VkyK12gT{Mh0f5yw+PP_h<9)E!0drt8Y7sZJ z{8!FtZ1k}go8}#;EvE>JxO?_eJ?1cs&yn2BHjx{2#+{I`LRn0}-(-Jr!BKL>eVGHy zH?+k)y9@8G;4KY^ca?o6d_TWzFqYp?ur5ACalDp7@%=N@CPAy`l%4uhXDCmkVoRuwW`eiU1-T9#$;JW!%sJ!iAd(r;~|&v;7N- zIt(-u{j#%&g6AwRP<&LR)ppGcu;$w7r6rE ze{o51d)#@ZoaH)N`(1|}_};kb(nj<0QF-7B7CDn*Zrb!!T%xyeVH+t4!?}nChz!o& zmfyr$chSoyIE}{oh6|bk;7X1`Rip^mfh1N%wI4n!j{E97Mdh8bU}e52wxfF76i}fr zahs_V2zs2@eeKrA1M(2lJ#D-w``*4%PmiUG)M7^t?}9$Mkr!1anwmyh$Zk>g{=-um z`I!{yH7U6ABvunQiG0+9Ee<#l+1Jey@pX!K`%*&Cui(+3I}TzV2`_pHyi@*=?tlw z_LI#vTmc&RDc+Lf-dqy-5I$%_JKcQ2Xgv)>E}+IgKv+MBz$=0ia#Lm{G@jzrnQN$^ zwYb&7-l=T!@GEKtq=Tdsd=-h?xCJV%t z?O6BZ3ykmCuL+_kCEQ%10ClmS--QwOWe*i`@W!2ie23*ar%3N@C`vGXIT&+xkCB_N zOe6VIxB%>d!bz-P@SO$Rh`^ny*bb$B^}SEm*Kn|k|D8MJ_g2z3!NOc`dQZf&Ou;1) zC-)tFedST-JF2R45T41QuQz(+!!@>h2UJe}PG@t9y(7nd8569|o?dHf4rOH?i#uR|Kz ztxD3B2t!Acp?rVky9Ez-ObfEF%3L z6q0(u>#9?VA)H;aCPuCHgb?!jqvhwglc6%nIj;-ES`w=&RcP$&+6UC%mCnwR#Pk(= z~5t&g-t+t)q!vByWOS{)4rfRPN zT`p<|CY=TwwAR^6EbRQsA$TXVaD+m`jGe!pqtX~~-NR8h({?ypXX%}+H@7_M%UVbZ zw>p8*PA!bSE^l(u=HKn|j9JO4x}Txvuc?1hPaSvAQd`5*=GFF|6)7>AaCyyxvJ5Q2 zwwc@wsnVXS>ZUwX=6u$`cadRTa&_JRC9C$H#p;^5$^d zmP;PcAhBJ2(`H?wm}%Qyjnfa~cui&QJXclmaw3jG+GiAef~OOR-Y$CyRPpUVdG^b< zn5>5gfI{*d$R%$$6=sT&>(7@DA?tYfWE|K*mWgnonT(v_JFEJw>4vc%&@d|Z>6 zU4)DHCboPb@iyZc#pVe;JRY*goSU4JK$e^^M&ic}KGnja+k-p%cm#76f@)puY|jJq zf1!0GK&K1sR|}Ou$9RnK22M|)RjE{n6t1Fq>lJdMd8-t*nS#Qi@*>Zpf_%&B(seLY zWCh$yD+#2ez~nRp8&G(`dcp%P@XG1IdbZb@VRMrT@rAIrbrbCDp^ko*%<+~6 zi#-bxmiuS=lf7M>z3d{<-n2)$K~&2O-SAyaJ)q?fDVxe5JfB|F2?jTvUDSulW3Ru1 zSg{bb5)+;KYFHFofH1452#Rmi{{6+1F%=LEL8OSaNi{=oxf`nf01^)(F!$>3W5tsH@u^~lV*;DZZoaakHO8(jIX({XKa@e>O3D(aiFK}K~J@kimbXW zzqy;AYVRH3%Ngi4w8DP7>s%t2#? z=@*SL*KE?Ni^FNW=jz6EjZ*_#>@+MCpK2tFPZ(Uin1$YOJ!!GlhS7Zx`-(x=KA`hZ2JoaSfvBcq7e&*PV;54ELwPxW6i#?a<}0rI&P|c_6#> z0J?DEi=U5t%FA$li_wym(CRhzkJ58P$XAm+Ji%Y z{siP1^4i9G@?Z_CuZRPtann_&5CL9Zk_G`?)Z~ zx|D-tw5#T51HE$G(wE1=V07Z0r!)iRpO)-?N@L&nw#7_WXY`v(}29T$ahFy zmvAXi1~lStMASz}dKF29ZYH&}-54Jf0jap@bG_rwJ4(4ju`^PHsQ&`zxGQ`)f84{} z{lUi5=aEM2k^$hFtBA*8lhM|Yl1ofblS4U1!N19YresAM7fl2IyXVr}B2$(K z0isiAW63z%2ZlZ+WH2nmm<@*Qhp@0r=H<_9DRYaJH7(Gmf_3e9?^W6-fyOB5#oHu`VzCQl>-(0PI^F1;JxV?^&v<&PM z_YcB(Hh4+i?*e0%BGLm!*uP?AxJX3AqcA1jE}n_>#~z|RPloxrL&8n?Hi=2&(kD&_ zCq3I;kgo?O-!AO2>-%Uk57k)oV|{`=5t4g2B32t~Rwq5dw#RrKs`~zTvZD5craROM zfjd+Sp*frwkwkoWe&DlgM|zB7nbYojaw6U&-fk0ZV**1T!LLF{gemheh_ff&NEHNG6?re5 zE!hQ@uBFx@mg8-)y!;i98(+$~&Ff(>?hF|xN%NA7FSSiQ13&Mi=f#LyvtF7TIB3n1 zv~>6E%LaJwr6L7YYsvsoy*7UlfQCWwikelG72}!`lvN!5hlQv@ofd6FXG$6a>sduu zziYOeibpH#rW!Aykr9$~ZBvhai;7Ea-3IOMO+yjd6ZuwWktowc>UYxH|Sadmx51HAxeMv0Tnm|}m(gh)Mbln2b{zSkuAS`w0sLO^8WQbtLhgVN51E6Z8T8!qu1`a*xHepGf@YOSQskKtF|4^{lc z6g!(T)awGGrcRXSu`l(BI7|J6rVcA}7SL&TR%1=6Am#Yu7)>RZeC1mr3Uu31Iam&p z=%89YJ}6Ea%TW#p{8QBiFsr20dg*>NcL1h_D(tj1#a@(Mr=Lxp))U%-s(yMUBS_)F z8%m&f*Jz6Bl@2lg;Lq#<9BfYnBlRmw56NCNY)@D-Y)_nnq^D><=UqjRgOT_^8@eyl z4my>Cd_`;VuFtCgb}9tOS)Ea+V8X2kgrIR9;Q=Lzf7PzVYe$gFYiN+cJ~Kr80iXfv zKm85_V;PmHu(E}(!2oDZlLFE~d3_G#pYr`TcTf<(P(IoxHbA_O(nluhrb$hzZK5qN zH_@%9u%!57nF~X%NQ>xid8O2(EomiS7XBU9OY4ck3QB8HyqeB}&tG>G;#@Npnu~Y1 z{D4kt#W)hvck~b>zPlbxH*dQDu+~PeyBl#UE@p0>IoiHsoGJ8Z+b5+mPp_3Wt?J`Twp^J(kgtWEUg zU@Q=~P`|zTCj_uVq4H*)TlS2IM_n>I%EJB2vTfzy;hkW&UDj`>1WIcnm*zw@MG(o^UXKjFoziK zr-;AV*z+u&+-kfggV-^JjjdqtLrTEw;Rha?lqCznp^7#YsWPjjEAs!;ll4?T|K-^l z`5lTF(z)NJv{HMK&sJbtA=zsgJt`q-8S>r1=gR$WBhu zSeij(|GTkhp^d^jC)To#fvquam7-^h@|Ez^kgw;M(Wxjj;ISk6K&q(PFGmu3SecSV zB=IQYOypf`9?L!3675Cyd+Y_9CBJ5P#}URR%c`$*$Ox%$$Pv}@TO{*+BK{`(d@8(` zN?8pDO@>}l_~jh{P)*+Q;eZVxEcZ7a-qN*T96 z!m9z0%&h2mz`Pt`(YK|gikrNd5v=Gki#!;w-WS?UL2+>xD)NNj?4Y+Ff2PSFEaZ(? z(PaxIlpRqj7(q;@mm-tAr-J3poqrI$#Sh+W2-|$KsXx^Ld3}GoDQadi=Z+EISc`_( z7-kdH4Ss>kS?NlNS}l2oYc#$le6!^piHzMyb$Jun+_9~+bd=9xSkfY<=8v$0qW&E) zScUc0hui=aleU(Uq7t(AUNJ=r1zoJb<@&di8FP5gcD`t7==Z1)9-A4=qF}e^gl9|3 zh=Na%m={V1bbHsfJ({J_L%=@#U1=06x$VUJRl#Qy9=|^?1Y>lvnNs6gTY7V}*q(Ra z=-;Q8!xxMY{OPuaW}gv=1SP6Vb~@U5Q z;IyB4U^zFW2DKf#d@fuf@|^jyto#NfZ$O|CG}xFF2pi(K#N2SI<_ZWV`5DVrIWW@D zPKfM;p>zk$UpZ?e%J)N$FH-4FAtv&HNl%tANOoO4F>EVj7d7OOUscaPX)EB*BRY}P zht!V10DB=%@R#--7v!0$q3)*4@89{J@sU1`aB3xp3X#^EQD9_8csP3FNA0ogXhknG zKb6T;fpx1uUxE)ZaPmj~*x^*pB)zxPk^ zTgB5@e~tJujkqFg@vmo-=@SjXuaV#!EF1}4Z~WqGKFC4-Xq6D})9S2e?Y*z3d3(Xb z?;roGbj=?ro%{UvvkV&&1mp+(|B#W_a26!_gt8x44f20oWbD~A_Lo$q?N+Ybz8w%Ezi;q(|TVDAOdAVy}4=@(+56vo@-nf@RhY9|MGO?2iIM7??M+w$9rO5?RMHP zI<0argWj#ZgHBbW`8;mrZ!t#xAS@-7G?{=M0hg*%4s6(gM(D{UuW>wamY(luLJeDo zU(2+@KpA`;&v*@5H2mA;nB#bqFP-}jau`iQFeXp7^#V1Rbu8Q^Ac$hauCj+Glf_CE zhkh2L$@V1xoor}`U6(U)fE<+~i2`0Wt38&NX9WvG+|_g+(thGL!l?7&YWljcAsc{a z{T{85t1^z;t^ohziCj_&mQ`AckAs?$%sU5DYKwp~4RS!a&rA1$@NQAsDa*`o(*R#L zw+XMlS7nUVG;S1vhbuhxj10<&yk;o*w~$Wy04+6tj6V0*TQreK|bEGaG()-~G@|3~0(kcH7Qcnm#8(deZ%}_8U|zSD*?YIXkpwZ#Q#anpCFxE0cP=v%qp`Uf~n73$FQKgwQa*=GnBJ3P^3Aem{`Gx8XD_h8>w@4P^*&|AR<((EzK7IVjsVh%5PwfFm?1tMq(bB3@cs{+? zSqepoQ@XrPyUszw%*nmif~e~{1*sB{>wXI_I9d`fgSyZWE_itoG9%Rr$2H6=R-)&B zo!X;-#ba;)=X#D&>;51CduH&dWF=5`7s?~{Mv}{TymjvyR`aiYB;E28CO4^xxIcZO zZc=oA(#>1KcAkEk*Ee)T!`c@;c^*q<-JDE$$L@E2xxR^dk$^EpvU(DAL#GiSx0O~l zZBcJ;yVhOlw462_i<>pOt=Z;9ucEZraV+0VVLZ}lt$iuVwW2o3RwuylX#A|sn$+~^ z%bv`La&z8}&_q>uNU zjYqgq2X-ALZ!6@Bx6c5JosAop6)U|*s$urxU_tI)o$5f#;GL#jhsh)0*e&i#Qf4`u zYe6Gut(;H+HcB~QNA0zf6u}hhF*ZuqWjeNe1GmpGP%?+6~^Q1(=M@HD6>0so@gf(D+tkS*u;+>=dbj#)G!&_r>+B3HLDnAwwa(Omv5X8dt$X-sX`r#?UAwKgGyENr$TS|v0ntr z3%R(pPfD*B9G#(Ip;;){#XObR=9p$G-LTP$H!p3 z>WQi?!Saw=)9jC<;^NbZ<`EA@o^t0k6Gu~pO}bMp+EV_hWcl>0dDXgHX#R-|m5juG z8dt76{{TU+$zueJj+Nr@$BJkV=rJ$KlRLhClFs22BY==wts(sB%cF|VrZ172!qTl)*mCILG&5ScqC!MqP^ z&5_tYxv&i&w$KeKQAB_nlXgDk7*bZE#XaJW5~)WT4SQf!YBr@KJ-h9k%Uz0^7;I#? ze&dd<2V2@W;P>NBjI-KqFn1;`=o0#=TIe1RT1-hoF(+r1wf1l_YjN%@mzvrA{*8Tk z?|mZEER+>Gs7r^zu!-s(2Dhrx0S~0@bmWh|Wh?u7lvB{jP62Q>j z2ujDr;JM1(u%32C~8>M4#zo4fPDe+_fW1mOIddls+ zT_8Ab1IaHPkNs%#9{WpSE*a7*^`{ISA^pn_;ak%(l1w0*1H2e3rK&zJ!aJ;btU$?kEyN z!Smm-V8MjP5^MsNx5b}Wi=Cu0|EVRY!fdoYb7q)SKr|uesr-9U|IYCZ)+e~?#Fh?F zY75&|O+^sn(P@X^p2cK83RBv+ph??z)k$If4AC{6tKKl(Wc+I*=2;RMc@w?0>m+q# zX;q8_r=?2{Hx@nT;{A$=^KWv5N%0nD2%evF8rb|fo9IdDN#R_9s$#z*iZP|A50h88 zEld_nLt0$0P&yC8AO63Y5fX)jyou646nT!ZeI7K%6n#Q)#f@>>HygWdGu%#3V8n{2XQ~Pn0mR&4E#9lH6HPtYid>W!TC0ZM5g~HZQM_` zKgPGLpEdnEspThzLf%@w!VYkw3;uNAc@pSO?Xb4sova4vZ&zFMT+$S?P2@5F{67L; z1l1mgTg2CJ$Ztsy!R0?Jrm*c~4i+=JgbxZKa|)$zYtV&Fsm1+_5#rrN3U^KU7HP3H zAPD|SY17`{B#H+HSf4WfQX`R9z-fC z?$6TVd{68eL>skuI_txu7mxG;%&%>qRU^HuuP>ia!QW%Rz#~I>58EsIzvlk>2V5#E zy-BLz?R`#!f6-X?^#5$c2TiOggTE;nKW-qugALeU^Np{ui(?y&M47qhIq^>9Bk{K} znm;ECJkE9?dj~zhV#5_cWnJQ%2!!8qGcn4=|8(5`6+CBc)u5QvPPKXmK71ul_Q~67 zrV3j+c#(IGe8uAkwU!H3{#&V#!l$WhvB){mN-d{{q}-Ur#x8VGPhGh~lscxH#i#fi zYrUObq*@hY9O-JxQoH;qf+mo!U@^yms{_w8D37Mys4VyriF8Gg_Anu^frgcL*XWnfEUweL~u5bn8kfVq?*vLX#2Z32+U1d*oBG zmoS&GjCn__|E7M~UiAoBzo_W0@x0&gIk5N&v|I#|k6O~%Sa7x(pvP}G{%E$~hi$^x zko{{M2&4SWo>_?%6+Fa)O4_{X;1TwRjCOY5vF)hhNP;nX_MPb*CDO@^p)&;z`GjC7 zQr^vO+>^{qXaA-cXgz$7UcM0h1nv0LKG3z20|g(AK$fqYS!$mvU;81N>w5ShfyazI zwUpiH3D0glv}bOeIXN7CA686SjZbrOD~LNEb7#o<`=J}C!;{hq@;>w`Qr zhttcG#!rW$*#B;w!i~IjKO=$f*jP^Z@O_HjRQ@WgrVMltXm=%==#H0#o3X6j^fQB{ zcw+Zajmzq2g2Q|66s|Me*CVx=5KvuKqP(}0#k`kwi8~)#hbN~Lkugr>9j2#WGcZ4r zU8=HNLE8y!u$TUv%t`DTd4dOyN##hiXM*1CS?56!8KIXy|MB&lYCyvTBewCd)? z^{eDgIg&4c1DI-JV=*IqJT20I5N9JuE5aY*I zSL94+g|7B7rlIsFe&j}t(iis1=}@E#gxAIrmL422+7g4lEZGu9FE|r6oWd_lK%^uu zgi>8$KrPQ3rH2pei%u^ZdCy$%tfbIDYfS-lr8x7i?j1<%=wL|#=k8T`kz$W)vWP%T zJlrb)X(eqV)`vM(UsXd;uy~>STKi}8y&fJ|pvevVo>^<4ZOmfbw^%GuJM-JYJXDn(akqmnI zI?^-evB0ojdl21Nyt(Kn`g#VWT0Kug(@+rA_T=O7l$g~I5BX#V*$hXK5nFfsY(3^k z4867`wv00Lz?ePly|(ejQ8!(Geq6Sz#1?yq7f)~rJb6_-I$3syzR6|`S+$+awBDiJ z68N$>I|&>_B|e1E5XG*m8r*Jc4&O;pM*kFs(%!mz`>t9|wh=v=v@F|becUK2;xKOG z0KHwK(M+H2+3@Z|NOabNnfWhtBBC1S3L{%hi;T8-pJHmt%n}qRDgvBzq;vYHShA5H z$oza4h0JvL+M4g?c8v}B%gE>0(~&8@tsQk3S9n-M_5M5AtQ6%?dCQP|js$!ad-1Lt zBe7U4(g&2Y=Q2WOzCF2j`f|1IC~4jcCyPsgr~$b_!q{cGFSKUOx;tXy(obLXDYp2^ zKGB^ZXOqSl+3W9ovcorkL)ZZY@=CQtDg5*E`3LgB;qoIFcIPu*>E?$WW6;dw^0ey$ z8TrOX4RW4XaVir15600Nvi7?#!YMW$mw@rMl)TSa(}Vb)ty>_?KZX?#A=7u9p3(kT zQn+M(FKZo^6BI-qOPjG{+6`FAVfqChS$V&h*1V39iJ`@ZvHRH5#i?J=B&h=7OP_KJ zY#COQCaX|o)aiDVD0MBBnzp8W?jv1L0GsvXQlTLTWrhq4b(LK(=SK9W(k|7=bA&PS zby!}GF4aBd^HsU3%*}iT2aB>wdR?aWN93Q)%^2`+l3&}vdGD%*u~xA7~wf zbFM2|GE2Rp;;ENeT75jfD^G&FXDG zA80I6%noM1g-b__+@-d{g@^oPt_|jL2&1@s^s3SgJXxPOAMt4?{bPFM00w;1MzNc` z?XLH{;0SrWI;oA8AL98jeN7#+hdqhE^aAu&uyMrE4+%WqlS+PsvYbv1=SO(I?7w~vsvxY)uO!%%mh zl)CJZmHh3nCAVUU%Kf5^EX;MvfzPRytsARpo2E}B(aj9)N2lisg4RbL$W2`kC?T&L zxj3g!NANFu5ggg6dit7$z~Gb1x!#uYz5?Nj8g?Cz6-}DIW*MlgRa?Fc{h^wl*O~2& z_fT5tl}oXUscMRtUNq#Kl9uCJHCX&!T9xiR=hga>!`i*3Q=9i&3CRHlXIVPvYHnwM zxeVvnN)qY~F)JYO1juO=3?WnWBe` zX%bGAnNPD;9I?jS63A}cMaSxq`}s+rIb z^FHcuNqkbh_S!;l=8!1H<$Qz+IW5GmsU=9Xt;Lc)Hm#YYC(?qG+aC<^tJF1~glZ*tQ#|r>l?#sHM;S!tGTyMR(s>Nj#UrXT(>ieW`m*&%*j{lx8@O1pKTd4Uf_+-z7>?UQ45?w@Lpi*~H zMp|L=Jp(spWIh*01@&_TaNX$26t>GquTA?&hIHNXqs~YXUST!lh2CpC%z_uJm{xw} zNp-q5ApoCud?vc@_&T@ROWxtZRd87zGkR3r7Jksr#1HokCa zvnr+uohrZJB4rOMSJr%MJqdX?AG%$4eVb>@HSuV%JJ%0GzFw5kb^A4occ`^>Xp%&Z zX-hpf&#LMNy+X+4Ku84>El2Hsu5D=2t?hafAGM@I>x*HReU@0&tC#!5!)lYTD+x@i zE$3#Yw1T8`yL{E>j}pZ`%<^MJ-$WTA_&o}g4%1$s zP_M3q#IAZ(sAWBfVv*V5VG^KhgTxR9ZsAVvUM}H;Ot5OQYKpGEcso(EqOdOu_R8q=02=iqdua^e7BsZZ7$3 zn0WcxTEgj~n~ICV{=-h3XJ4t>6ke@+(yrvR#+aw~e2n;85^1Awv7(qlzO5ve+LO&o zA&NFB$YdgSpN+xPKs_4I5N0TBZ#thD>bQ4MX1$beiH~j3|6;*LAPwtHmG9n2ZY_;b ztL6RVjp~vqJ+|kik%4RRevZWC`)HdnQ#Rz--<*1ZwkkkgBJ|pc&7n%}kbNy@00%lZt$rc@(#)b=QgS z?U+1i5Xyj!&v6XHh$vWMCw`fG;K__`&a&hPW%Cl1@@pPf96a7TYlB{!wf=`6B*Gh=*gzDFFtfdtf4WLP>oJX5p_eQf*?0(n;>0rKRn#jA1NPT5> zw(Ch}efF_Vsx!LN*Nfm)R-885lIF>3Dpl1luU&4&D&%ihW6_&T?XoTxYkAGMqQ>AA z%%f)2aYtXP^tsMx@^GNvf-h52=)8ng&*6X=C?gt8E5@G&7`oWFCTdgAE@#|`F0ZOy zxirfmw^2&xuG4bqTNQ@t;MDqc6V|*NX(BGZ=@OstuaXor(e-srr4OIkn!W#==VSxg zM~Rwtj7GbokS?#AMUFLA>f545ePkpxa$q~2afpihmhdb!&AteJ>^XxT`;Jbw7h(v8 zr=|AhA_EdSvOL2_8Tt1$mZ-Tt;Y-LZ-;zqR7*?3jRXsupI|N}k5=@%L6`RCwMRjVt zy8?Q{s%RK94{L8(0m(PBsb|aF$n3iz^kPkrZNH`xI>p1R`=bW?Vp@$x^k#N!_C>uJ z9vY4xvDg?HNk?Wd;WbXa5t}`QqB-l+PZ40EKk8nT!^s@hqlRmd)_;_Swf}LQ6?cD) zyC&>zAO-ArJ-rtrV-&ah1UoUK%7jTyZmTA-4>-ies$bk?`6)3ay<39CIifUPzb(Hc zV~T$DkRb82y?^{#jPcXjt8(50YUGs5zx3|Bshu%9 zy?IN-{DiMI7=vEUoc@^|HoG04uwl@}lIz@1i1W~4A#;iY-$Ln!rM90UOi%C9L6foO}LNLbz9U+ z-$y5XOq@7PF?aKd)Fb8TIyM!ApCd=64lFN(AXA^Xhauc)xCsKSJCBfD?)6hEz9Jw ziuf8c%$@U9+i-;76S>V@^z*!u=usm|h1)`-p*z<%{7bJon|hOt8K_2NIWU9_F4t`d z>do-au?Y=m4j9*=?Rbtwlc+(8u#teEN_z7`5`1#TRr>S@eIVlBFoyXs8 za9rA?@4V4Ux5xr}t28hjG_#k!4fa4@dPUy-%8 zWiHbW-K$AkK8L7{p?0DDUX3#@AM>yF41KG~(&0?QLEZ882t2Y_^UjhKyhAh6Arqos zXr<6`!FSV7JwkL4IQnT|PA`w&F@B!|!;qzApXUlNHe%|`y=l|rPuX;f^&XQhNl3*v zR^U7zhwqPbwl)HX9863P9x|eoRS>E4`6Q&Y%>v4rtO2^SS7#ezJo~9Oxot@G#b$( zO7ZR)wco%$_0EIGXlx608?o&z-n9*)X91h_Jn~?j059P z5S;x)Vw_L_dBcdI5P+VL0bSua32_tWLC5^RLka+VmjOUb!a?goC(=}N13Ln~_w$^vj{b2$-2abD`VaZ&vH>ZG zmFyn^(|?nX{{g#Q1x%wHU&lhZm)`tP?9a|8JCSsZBkd=`}GMGUP$JAOcf1RKyy z4f$n<{Y8!W&!)uZf`}3S-^O1M8F|=W=!d_IH-i5d;e%qZe|pMENCf^eI)wc;QUiAf z&w$#K|D+>>8lc*glk}q?Kaep30UU>*AlyTu#0@+g5+>FW6Tj_`PaHbKe6U}o#1=$$ zRWz}AUGT3>Ze*UAeiip*4s*i(UH3yQulO?xBFLWpF@75sg8fy5@yGV-AUXh{Bfql$0Ui%=p8x;= delta 20228 zcmV)9K*hh*+5^MR1F$OrldI+sv!!iA2nD4BB1t=w?R8Fn-BTM?6#w09HVexJQb;J2 zwt%)Z1WKw_w4|+VfoNzbl~53^I+x@&Y`g5@W&`~j`ruPv`l3%xUwr9|2-TVPO=tQS z`1PVQI-}!*ALGZ2Gmht8!bhio(=nNxd-t4s&hK|V_U6GqAKwG;4Bj#k#|!mn!3ik_ zrO22hPS)Xnl!?=Lu>lF3k(#q6&S9tl!x%A;HSm&&C|-`7nSuJ4$YE59^9IHYTre=s z5OKV6S@;YcdCxDW%RVnTBE97Eg$3cK^U9cEs4EFalzAW+j&65w*jsWPkC!g`UfCCw zO5Uyn!d0$&7ksg3d)3Ou8Q~X&8!)gO;h(f!J2=gMa6Y*UfyaXEnPLbJc_rf7l($`R zp*lY+{7F9Rkfu5B6}dCTeOo@)l;L2`t}t{Ciz~e91Up4$uyQV~Lk_Q01Ua1Ajn|?7 zh(@JJlxns@z=LXKXpXyOQDSIG=CATao_0l$zBG}`jE>5j3|=b901S-}n;D`-&!wP2 zUby9dV2&y~%3!Vsml30cP`ozA7it+NBv*I66}&78UY1jWdU6e`wOI9ivOL-|>AVJS zd+FTx$n~OF2yD+K7Hw47V%4E3dBjb{rFJ(2UcjAonz2oa>ngM0Rmmr7OP0~~IQG5tB7)^aJ|vBTnEa#V$ph32lSjAf7@}u^U7WSwm{qtIqY&UWXQswUCzHzlQ^ob3$K*Nwi~!@1h}u=~P0eD&9uZp#BM>Gwu2c z8t>mx-~&X^s-^J+>PY@fMg4^u^DB=xke(NrbKnJm~`@K z)tKx?dRdheQ#+ZIrjn|IHgL{efYm^G(G5|{Yl5%P%>!;7Qo+B>V*vx?>q zHd-H1(f;0{)yHdSaXhEcLd0BpK948WKQCQA$Ww;qzfem91PTBE2nYZG06_rJx=!pY z0{{TP1^@sw0F$BU9g|$>8Gn^k-BS`#6#rdB7uQ9JP*d|GH3L*o`_eR1G0H+EP}A&X zg&o|&U0RmZf2e2c0iB%bp{6f;=nrb9>D(0=L`RK>d(J)Qch3Etv*%t8{(k%fUHT13R$(*^aXr`KwP2FISW;9JPLTNdhRR}W>(T!9vWys0265KT8Ohz$+)B2{C z*5zdP$poVeO)15UQh)fSZX`>5s;)6~d3}*r@>@BmDQ56=5M^*?c-|v7FT#pR%UUWJ zHw{%w5y(LxQ%~q=hH4AHm{o|sGj7U>*RyiQs#U-=L#Ox5A_hl!2W?ve4DIIt8N|4r zGZIQz<$ZJ>xdNP@ga$N9c!);=9!r?P6A4cd5il!Z4)Y9+<$qO7<zVyx zaM3Kpls7pgOYnv53~yT5-dj2n$I^EnLsIj*FM?yJjK=1dR~ULOnzw`{eU-&ngiNKZ z$U-Qobk9)3$A7#yf}SJ%@gc3^Ez#(U^?OgcPev35f={=pADW0t32HlQDjUVKsoUl@ z)p?=ZlyvwM-~~fnY;Vnm^2KTNZ7r;ReF~iPdQ>W#4lLO8KZ&@dKc@#e-*It zdjy6nvlX7Hm;hL9D;9-6X--}j~&BVY%46YL69u9Lk=-;Ux z;fbbyP)h>@3IG5I2mk;8K>#EG$$sMx003AZ001EXlcDG%f2~>xcpJxcevbsPOK^EX z5+&$_WgQd`(2{kMmTZxtBuKnOkd!G|l2^czgau;Z#X=OFIF9YeiR~zMY$tJ?rf!?2 zZksrfoCuUf+e(kft=%^15%);j^hl4iO;6XolCb{_79c=Ew9~KpgxQ%lZ{BFl_{?3a(OokD-_p*s2p4=thZd+5vbk7D|t zMDx!o{fmcQq<>ZD-^BB6(fqq;-Vx1zc<4*?pC0-zfBJ9H{7*Sl|3IZ5dgwqdD=Vrf0R9D#O-KUw@nbM2YU|p^d9XwHPqQ3 z3ikGZt?M5BtlkpS?%&_pe<~C_h7ku# ziRy`|s;|HIK!0Z_bgJVZWS5GF!Mcv#o}SK*0cbci5bW;k9UM5-9qj4~hB`5`FNDP# ze`}b0{hfRF6=h&@$IQ`Dv5ys9rZw6!YUz=f(K2D_iG*Rbbje9rs$krsj~h%L^o9&8 z88zcfHHmrtXf7t_M(%@T_ifR5)ZW9?UcZ0^^Sw8pvT2CP)nP_pWOY|GZuF$aPaD>N zemZ6d|C?bwHl$loF?NV9dn}3|uUg1tf0$@4XxWdm-S@hUm0>eJ5*)YL?_?gYo=HC8>`XgH~*g|GL@~ z-mmZhg%2tmRQQm>hjYwPrqy!-f3s<>^H&uRLX&Y@KUZLLN{FdL^xE}gG(0ymHWdy0 zd?$$%@PumLagjPeGe9ayGqcu^;(^}6^jb4C3#%=9+HeZfASdJGON&8 znzuqCe3zU+$hw$n0T1C+Ot+1}oF~>6k5=KfrRU-j8`T7aPM8*U<1G*;%YkWeeNhP> zK^rpS5pi@>WCjkv*3M4lXl^r^f#PyAnNQqng;6w~keRZ=hA37*L>7qxLXJj{&;`*v zq0tBFL5&`wltvFzim7b@e-vByE@vPla<@hwqVpPkjGjOQ#%wzgNC@N-n^(9;<6gRo zVipt0*%_w5LVD+)tU^_v!bddj=a9w&JgD&yAJyntdP<{9^peJR@-Xl-TeR&GdW=YZ zX#+d*AuWGO$Ui2U;~L+^Cp5ZDX^q~XH{n-daI*}g#wYm{PRs>tf7keK)-^sYnlNK% z@QB8vJf?6|<9qmw#xY^{=NaZyL0sf2Ar6pm|ba)N154tRQV>5a2ItHD2^C;fQ~ z1H$Zt!uM)yaZ+QO5mr+8ti}_Z(D zPc-jC@%u+~I5X1ff44I-HGV(6DQvnQm7ZTk8h-#2{D5daD7^BZ=shHwhchca1m7-z zf`FA-Bl~eGKw;kGqW#hkzis*xx|KBiLML6P*O|&>{%L%kA7MIwbZ>u8u;+k(Fe!F) zaA2U%FEQ0$2&#VbtYP`}IGmj{!Z?!sv$!dgWX~->7Wogze{}FiP#RYBbV~39{CzP4 zh$@yPqj04^l~WiBphkr{(~92bK)5?&ghnsZRgFK)AJO+>V}n@KyK;ji2O?!+(PV`-)BtUS|e6A)l$Ol!y0!~rv3B>6KM ze?kC(1n9t72d*_|#x+vML(d0mY^ z$)5t86+xQdzT9nZ)j}Y;8TX-EvL)z19!{cScOGoN_;n#4jN*Ch`E}h@5dMKN%bdtu zOP3TqbeRtSzul_EWhOrxCqWmmioyUdmfDl@EML$~Qp)W9=e*E)l7{UZgNRt(wV;4c z%BU1-e{~DQjH_$1hyLsp+C6?I619@@B7Y2JWt-A}InL}|5`|Ge|LX3mFMeYcb5+=G zJU?*D=g2I$D0{K1e&gO0?)$Tj+F0biSNtud7Rw!Z&Ow6Pd3{jYAtmdP9K8x&Daf6r zd2T7ZT8n!m#3HtK_DukO3PQFe-y6#6kGG3qe@#KU$*D@|+IPbug4^(Nk2T?#nH29~VlocV&F|?(?;QMXbNHPL`9l1vojXh#7EY!d ze@sZnl_T(>@R%XMQbGTqnY1&#J^;AW(?ve0=p9KJ;+POszTeVE$K?$>@t%@*J|*~n zTPCb_qk!~Sa!x*E-E=HttVBK$)O^25Vq2y*3ZT(9pUrtyfs;h8IO5j7OCYlfgk!U> zS$7m!b9~;Kd@1u@+?L&F4$g?i&zfftf4^NtoN;{NG|Ii|35T^$+T#0LU9laC&j>5) zI~GbokrlJ=aqbb*8rSVPRu$R&4U@Z#Zlcw6jF?O+Cm-3ALjNogmCyt&r*kx!8{dcV z`|`%`$N2ud@dq$|pkVA3uVd(Y#T%J?KI}a4QiZ1nypPa_(S8J@K`J8`p5+aVf85kO zMSMw$c~ml%plul^|QWD^<}pT1?yFydAWj zc1oMJW+dlq+K{tpgWPV3>^&rHe-b@moeNaFS~}LGfQpir1;atKoT_s-~%O zn5U@f3RMf6N~KLzQcfGy&~92mw@Vwe%zDR$C-H-Z8sX-T(^JruadW9$S>2STnl#lO zZ4i6*&Tcj%xE;>!K!2YU?9VL8ZLXT0re~zGYWf6y5-UF?l`)+{|Jkgvf6}w$mY;N6 zxrbZJ8n4izG%ap*Pt%g&X{sBB;-yoxtjFh0ldsj)(CBkb(Q^2HMXTa-c~|N&)YpvDAg(yOZulm|0c)p6>f1fd2q}0NT-`|6J|t#8HBks@JAgD9L^Ovmdlb|=X&5zsHyx)i-9;mG z0(E~9wR{R`dNF?*f8|{CO66vW0$@K8-aBwBJw9(Pxlh3F!CDiwb>7p)V_RQdrC` zK+X)+FUZA`>*l%{7_SuN1NhBgj|G$DOtC^+BMM!d0k+f>V{ra}1}FVU1tIE zJOs+av=-PVe*#Icv;i-sA5eiXVMgL@x`B^PKZu+ zb_4xpPmMh}5ZjXju{|_}1S!GlopePa^po-gDft0ae;~4pbN<`}rkCkz#-AL6Qa5HU zz-@hLI?~4uRO}YG%wIOVjbzGM~#=hRI{Y zrH$UZ(sTk0$G=7=FJk50Vx?ZV(&yr0+^sGduG0cN5w8*iy^oF{`29G9AHXx?qXu|} zP);h!e_g0KS5dpje+h0P>eFboNIWJQ-=d9l>vl$mISo~}9exU)9em$2d6~sTJ zCTZ_UOuj*HI(B{=N_Pk9-3UWJ9zxM;nCOWmueg4b|J zT$6h{m@&xNn;QqhZ^+1K2*hv7y?Jp={FdCC56AyE&HbuE~%)* z`{ew8{VG0y530yuSj7k)Qt>cG6?{m+WfkjjMa5mX>c@xWhL8C1Q3W59pC6ZtpHT5h ze5wkc#%B~fA~`=RAxZumJ}-wasQ4njq~go?ih{4I*p9CW%<9YL_EsHf@W=4X#!XStb|ln30kc0mU*+yDdiEiXq)f z8T?q7uV*wKYiczU2|d{_jot0=5U4V0CQlPcZdhBqq32x6HWIsYqVfP*$F>neF^B9J z{YhT)q#hb=|>C#RYYaf;EG!wM5B5n>0NM+}GMIquWa$ilB z(tg&6rfrk_i@f*`6mm(ox1Ws~t~m<6&fw_%{l#t&xG7v1kiwaat?Ej0m0nQ9-cTIQ z+N?tPGNy$~*!*#3rPM8#5lO>t+PAlhYl3p-7Z7{SC2jp|&K~lF@)B*A*&5e>Q>ixN z_%<`0>~FU$$Ns53wjMpXQy+42Ucom6R)r^yYKf{_Cbj8CAyj+Jv=uen^qyIAzE((q zOal*yHuFp}ZtDFS_M%6_6OqKyU<)_jy!xmWme;hjvKfn(){0Ki*@DmM>;-}1_@k7+9 zrv@2B4L`%r75qZOFYzl4F+4@X5Kd`0fu}0?wT9o|w*qrK%<7WmI3DNWb{Ec2<(1N* zzbo|M82@hF9&Aaaj0CgBl6=3H!yg3dJ(#z$R;6rCq`#POu0emqp9Hjj{5+yb?#>nC zS;1d4{1t!G@OK&9f8d&if8rX;!=20vYmq=z!IppF-*Vq$3jU+var{@IAR)vQ zMU-j6C(0F3p$SF!nNK%3LG;vkPV7x5?O4LdEfQZ;YC@G-_>NO~O;ia@U~{XUOqzD6 z-=L8RhAyQh-sRr6#+%mX<|CktTZ=1;F_3$Yl@huiCJPcGg1T{>?Y?*o6oZ`SjJz&`R?PB&= zyC`j1Z+0all7ntrPM&3Kpc8jbSfp9SdeXwxi?n@hZAPy9FLbnjC zDQTgTYUhOp7xkEb(qMSs(`t)llXm!qz+MG)tSfo07L-p%fMPgS(DXxIY2zuvt=XPy zUM1I&bBpIyrq~6I9+1Uts*@QMmshhorsl+VrqaZ6Qd+lo9Nm~#=H^VgvGgvyB`ajv zrOP{(W*I|qUEUY06#3VOCly^U%=*b~rB`aksZOnRO{dX+Hp?{YMVslq02YoZpJGU@ zn0>CPm}kRS>Ao(9>mK=DaqmTZ>Xe|4uM%(e_10MVh!n|PC36=|w|GZ36c+Oc3z%&> zMZJhqUOQ*!JF9olGSA3+qvIVJzMkly;auB|Q)xX;2hGUmcl+6fhJ$3_(NE|M-0dFT zKjg8;D{?b`JoZXW+>VuG=E=tF95$*_Hefh)ztE&H3-g%?9Vn$zY1_=czMM>zq~g9) zQeWv8_MNdF;vC)e@VeQU!sU?%+y$K&|*pHhb z|Ca>tA&3ZeLSPqXQ&7cucivp%e0ScwhVwmn^J(yZ^P4wyj=iKb@mKJ-ym1&)E;%gw zI952s5cYG_Tm~G#6Zl(+J{%+$H;a3yR26AgM^F}7Is)HL4&}Q>QPDRHrP&wsW#B&$ z^p#&mWnWpKs;AEv(0VeMnnCqAxki$wN%DbF)N*H_xja}d_tph{jTuaDt{B0LW+kYQ zS}}@$nPi!j!R!ozL9Wbc_6PmTM=)1T<~3I?8^Qc$HK;a@VnJW9aukAN;HE%m7&nh% zVPDWcj9Z4WXcUVHv?PQ2akIB0z_FfQ4%5&ERAVV-VHxIQIo4nWImD3`!ktd+M)^ECOm|iygCqQ!LJ60MbQoon z^B{B_Bi9}z5k)^;ew17Wjx!tvoj!m;D3o;vua1Wq$Me+U1Wpp|0`-d{!EhuUIRYlX z`S!?0IZCW4(lR=76yd(cK*KN^N3fJW%#xPok;WZTO~rt1s6z*q(0pmsOcx3km4Neq zb<{CRl`p=mz_r=5s$%?>x&JN}CD)F;-&}tfXq})o|ZwoZ+mFJI~@AweO@=XYnL{&10&$ zt54=%Eqr?wta}WV3hoMZDHN*8M`zaHJ|_==1&x8K47S~i>2BmX>Byi{sy%`_F6qXy zyioU3tbsYqwQ+YYaB|QUS_UzPV)&xXidml(Q$339M6aQ!VeBZ5&dEHu>MWdKDod`X z{|}RcoA?AdY!u>?f1EFWSb2ODcNPEsvd1iw0n$)H7igPWY;!N+DulyALNRR;AR&YV zq@C;zn<29^>+CFndQfexN4@KndY@QDrPypj(Z>5gYrSvLR;@=p>mT-0MSX8(H`(1R zDKVeV{?2#(-uu4y`%TXM=b?uItinI$VFN5~lH9zI8=IRHH;#;d7NjK{krBd(grhQK zqZrQ96n<_;MNyi7(nUe3*(^Kchl!K1rnyb`Zsl2^-k4ekly zwM_cD5MIx+-XP7v3#nkgZ7I zJ>0xk!uvvae+VCc2;qYvd`LzUKFk{*VQD8Md{o9d+%MA!KPKVh5>86^gh0g+)mULz zQPmjGlQ-#xCa|F6uzEy|=vIX18wJXlCZ?yHHr*Cjl$+W5VA|0wv)4AJm`u%y^mexs z(`8H+wai0$JZ-B?Cs5mA+3`r+R%3=18L`!5QnMp{Uf-I3PfGmZVl_QO>Z-NtdeRAj zN>7=gn(;^v5twme2s%T0YQ;){<)yT=n<+;%45r(po4T__;I5k42n(H1YL+|eB_C?0 z)wO#C{H<1uyuPqQH?^*GVon^u~eHiVj7kjBgO&3T1q{nwH0GcajayAc0@A>k97D7 zPde=zkq)9I!BvH>JC@A3ueykKQ=vPy5byjRM~x1DcdAL3MZ%{foRaVWSvzHVO2TP@ z%X7|jBf4|&uoh+A^Lq5SsXA$!)NP$fkY@m8M>K8Qn(0JZ$%(A4ggtVPmA0dr=cHV0 znwX40v)zmuR*In1sX0SdOv0xXJcuy`+i{bEP1vkp3pdZhjS9A6n}SxfDcFGw$;wxy zU>v)D1eO#-bX!_CVw$aB0%sIFgtHXaCTm#1XL!B?pH=WMCKY^+o6qyw7w|;|U&5Ca zd<9>X@HGWr$2kSxz&9m4qTpM2RKd3~Dd9T`zKib(1e%hn?I`#@en3X$06@B{S>X>Q z{7Au%nd>L`sf3>?_&I)|;5>e*;8%D|!Q=RUwSwQU{@)@_m}%1t&0%)JA9>uekCC7! z@H+{=SMUe?QNfe=lY&3vFGO4dn1rZSD{aK8P0OiHo44!9YD%DL$D&R&352>eHD#GC zB=xU+;J@MT3ZBBGz|v{&b*D{7PiRv@*;jOgo$Tc0vn4BOFUE|(m9v6I;QC8U(Ol4f zv!#p5c40bD(V1RocQh(omYwsGYf+w;mR?*bp*Cu3s^jLaz=o2Qwq%W*QJ;J@TqNhm zHD{N~r}pwdqIs8^(2BEg`Zi$MCY6!Kni6Gq#!?pM#29icZ%N?Vno?!IxPF)GskR)@ zTyv>z1@)9?=R&e`>tM<<(vG%Eb%w})F={lbrRbtsNmo^T&R0<3G3HR2vuc}JZNqG8 z3px2TIo?&wTRN7dd5dG2hwPqXDMzEL+^5-g{spm%PUg`0G&PZD^=j64^s+2U%Y2Bgdv>#nm}fp7Li$wUtE$Q0&lN#`9dXoPh%6rnygx7k1wq^Gv*piR)4P{eeHmOfiuLswRF0yV76dPP8;d4zd1u1}7LOuCUDc`6SVH|38H9;`X`d&w z@;*rZ6Y%>s)7(FSWnIgEM=?CB3CpKUXz_>rSy5sFS7u2ouOfoR46Y`k4641&Ygl~P ze+JL-A?)|0UE7zlcmgY0+}-C2v;@L|Gq_G*6q|W;y`bl1s3lmWq=uA)gLF*KnyjL5 za00b`C;mH`l^n>RE`xg3M?czZ$ZnK*Y8y}Bww6GV=m?4QEM(z-l`FleFFS26P?*QI ziY+3AtEULUft(#a@<%-dgb(M2WT|T{jjJZGhT^fd&z+n)i*@}xx?&tROhSl*A|aCW zG4FRVbvLeY|A$e7)r9Xggr~LWp%43gG#ezm#|hsfg!1Er?>hbrt9D2ng*eA}TUp#>U@f7xp zf8v0+fLmQmAA(H!mti;d5Q0xqPW6&@Kgq7gUK#~S(th-jZ2?Ag7W*~garD!!z=*h) za%(^F9vAoE09owA)1neDz(JC79u2PK0BHjhqWAW0BTuM*w9*Xu0`^G=N$7qC3+CTQ zb~!@A-~}v}5S0*n2CauBH2n(){*4GxK_fOl2|84# z0ssIR1^@s7EtAm57L&m08k0_?4S(H2Eq)-Pd`nv@TMC*G#TZG9CPgnWP4I45hO)5z z$Zo6tBX79SM558eOE3IU#xt`^TS^TyP0!5Po%cEK*_quxe}DV}P{JbvapYxKPEaVw z@Ia<3I*M{!HIP6_#~OoC_4vLkUN&liVYGb2-*d}pST7t`Jf;f=+;Q8U*nbwj&#SZ| z6RdD~y=v{WJf~izReHFJ!F*NsTikWG4uyTJ(z@`rT<-hAXV}bMROiYKuWAJ*tPdV< zHic(}l!aaz)zP*Z`&4AC?9|2Uc5P31Z~309Ts3U&R=DTLJiMsa&P?lm+qNlT*vOvm zaG2`xCr;gIJ!P2hgA8b@LVspkhYnR7rh?)472!Dtj@W02W^?ZtQadefA8+$!*p$Il zCkv~^B10j2Ww>NTJ{G%xk_2oF0q8#(XP`9++8i2m{sbk@+ERUWGG)@(X|z3C$g<*>R4x3x}qZ!6SytILxyzM+nc>3VSl$6CjXC7n^eIJ zy-^8z@gm4b2Q+!Y*Y|8po*oNPhVgTE1|K=$8&ALnXkbp|Kex*epiboI=h7 zGECwQpk@-z)J!%Tp?@De`LN7$8s)uI{wuWK(6vv{q9=4A+T(Sx$7?DC-=lvFk>oR$ z9>FwK4R}__i;?ZvNng+7J)9V3C5Oawn7<$PzyHjLZ_>yfFz~w5=7j zt*u&W(N?Wp=z^`NBxowy+FDzeYHe$+yWJPNecFD0rA`0mzM07+c?kIX_=Wr4yZ794 z&++2nm2ybq=^o;rJQ$=P z&yca1(##6-Y(7((aFFNl+#ns`v!t1)adD8Q@O+_Ppm9lnOM`S5muXxcr0HA{q`SFN zdKSuCmAoy|cyW-z918LhUK*r2UM8Q*rCA}(%JFoJ&kpb^jjLsNb&%f6Yozm>0DrHQ z=ea?C2iF9+Rz~VX`gKBBAEY8)AK>%kxk2NGAg$z$8lNAeRag$4jnZtArb+m0mZ@6; z{7&iFs&TW%+XB2jz&oU4XOL?7UDC7!>3QCz@otUZEw2{@>3n`qkT&v#8ebHo&BA>n z8v$;Wk2YymYTO>A?QCk?5#&zpl7A)=q@B`pagcU%D8PH<8I@*bkYgMVa3aXve91Vr zI4LUG0Zz&DQW2;}1=#_tPKjGLr+zYu;vWYQbrN!y4<>$=RgJ?b-VT6Iw)nKYA3p?`Jt>ua_* zZo6<@L-V$+4Yk|1HEeFWa7)d$4NL`%7aNxvRZ%0}S=DS?k$C57rU`Wk;TN}e7}1m& z;C)Q~Xri;zw3uczCalh?PRnSInpHiP(cNuYRgG#8GXw33o_I82v@^|iBWzfg9+y?R z4ZEubBF0*y!g;RSge|!=n13|g>}`vtl95Zz^^vGq)7EAtlbejVp=7Ia<4}LX31H`6 z6NyLcwM_3Rc?-SXT9cEDUAlwGTbF1znI<(x;$~AS)@oYY3=E0~5^Y9whhatJJKgEE zyCU%1OxKkiUqkv}n`Iidxh|5lnO3=Ku+w?Mp&gOVlx5hFM0|CrqDpKcu4v00 zXDU5qR?w&&%UhAwlzeZuqD&JV_Hom$+P<{`B!#&o&0WTlesJ<>|P~)r6 z-8j0NY1v7wJa5b_tgOk(>mpWGs9~LTwfL?`w|v8vz=_!{(~=rr4Yy#hEfs}%a|E7S zGLlQFTl6r*G?5Yj%?v#y{Od}@I*4hW}8@9nT4(uHXn5K=9s#ZxNj&8P%wmqAS zZiO?AuhICU8hu&gk1akz%i1t?|c))l7%4 z5-TarU0yO)v6Cu}$kt+x)8GW7%}yCn1(k8hM9OM2RX~h4d%Mjx+iX`OfvAH?s2X<1 zQ?BYhK?_JH?SCFgs?j4@q&dGQj~-T_P4U;)p*TLH6`6zF7^STmQ1Pb1ybTG(AEWYEEB*! zW4BwL@Br=_{TeP##rH;_@tLl1mg@nZ8Mm#ztP_-hF|`U=tX@VWt-%A?93jDyVX`@= zUoxYxihl^QigK9M$5Sygo7z1}EN{Ch`-`?WlPZhGuI@mRKcVp_0oArdcVAAXVp>?@ zn!(&q2voHRCCab*OMba#mX34M=%R~zI4L2i& z>nhngDZ^;FFj{l^jB@L!46hX@=XKI-li{^ecz;!%4zFFqlh2mP?>vRcr<-Z>dY2Bb zvPxE2ecDLK4X5#GR*M&%wz`-dY*rcGiHS@FzEH??dW;^|>38&dogSbEb$Xdz(dm2i zuufOdM|AoSeORY{8qnz)z77kYR@Ew#uGi@*x>~0zX`jY7==>?(uk)w*MvXrs9|v^4 ziGOd_`Lld8YRI=h`(k1CIh}9eTcJX(hKTp(4K=R)i8_q2i z!V8L%3&QOQGZ~I2>@X@;+la)&M!XMX7Js(agru{D;rjGm8@3bS4rKDM*^6yC+817& zrR!UWDq~o<&8-)sTj#s^9-WVHzs>`E2h#;76e7KL5=$h)v9~9I&PVxPjqlTWkiW&W z@#Gqd>kLbnW_1s{%mU~8`It_hr`vUYfFIQP+b}72?U1r3(x!5IIMLxYHQZsqxqt0* zisQxc7J%E8CT8@7yNpZCaI0y?!?qFYmLeeB6S2D%7RS};z>003wonuc=N7nO)MwS<;(uguJd;qvQeDcB)1CEYTe?oHR$c*{aE&VV#ti8E z9ljCu%`m>Urs8%aW@hUU3A%?+6%1$J8p|^JBn9jIU3yXH@A1Pre_!4nfdlCUiHTrq zB%Y3AVekV~0Vk@UMxZ-$D)6;+#S$oaJS&$k*ZGHtHE?-U=f@b~`-A{~s(*WB#}sne zyqz(ff5cA;qo<_#@d%}|m7mT}i$%O*Pl>XhWXMKVa611~$Y#HF5vTFbbbf|^uJf~! zJB!BVn6wGX>Jq7FyNVo?xro6`og3~RE_A~k39C9R`R5lJKd1Ba;utNFTo^~ir|}Cq zzsN6X{Ibrk@T)ril7EHa9)G7k)cM!^8=Zg4ze7ptS`q2=Xa2p;e-KChk^hvj@R+hq z=hr0l{aM^RbF>pSkErLS<)-1>A+i5o#2tUt=^yk@o&UyP0t!!@{FxS$Lil8MXS(oou7Tdx zol3zdvDJAftKRShOAvI~>y<0scA+-XYNxE6dj;t?(kHYU*Rz(w1$I|}5eGNBst&>l zxJF#`IOPHq91jJR27mE3Xt>zr`k?eA*E?RItX=T6yS7wrj8fh0hAp)hIvmLP+Mto$EPObh9wIisL(->yE$6DA`{A|s>Sbu8vb6mwb_4A!K8QBZ# zdf>@17R&nCE$nL(2^%3`bZJiCtB`&si`naB{Bd$=wcfdk$_E6; zs1Zr7%ha~8r+?87M4ff=dqXTS8N$>V@kAW8Y1ENsYKhB*iHe4#SX#u*b=2_fkk(^F zY}6gt3{-69Wb&e%1U2#&b(;I-gsgYQ@KE}eOL_wmwT zXPSRXORmmva&~ChSmZ8ndvo?jsGNb-Dw{PXdXUx)$$yzOa%o)G&=7%U@8*sZV7fuw zLM9yyxn9eKN^-q5@LRR~~(k3gpfK?*(!Jp`KUL zKJ~ncuEz5W&|X6yMf)*)T@DUjJm-}S(73We3bquC&!H%ibQWZoN3*FIZ}aI|jDM6lJn0kkNh0+oGO>CSsq)mD$mK!r zb#y&?M4F=%Bn{8C<^42i6Pn3QW%tlTyyRDVL*9NWsP@U@jA}pnCxrZiG^M31J4&HV zgEYORe1K*&c~*GyC)2kA)xJV+-mNsVGUV&0nJc`7-dl$LS`qSj3ZdkzgG0Zn?5EiW zNw4>LtF@5UPiJ{= zqwyi%wzKrt9Hj(HZXp(JKmRFCHdN>LN_&`#>5R_dc}0JM+Z3qZafu2(UHJ^C?DS% zh50zm*QoAInlQqZ{WOq<<`8)LM1TG=c+l5Wc`q$sIzUqjO1$?|X^W?#`6!hgrSdy5 zQi05KD~2jZ4|(pTg?R*s3Yw2n)%QWPXcUnQEWT68AikJSB5oUP+Iyh$eA(j+J)m1)BHDwh8w4~ZwDIvP_CRz-%F56kK zTvG~`H@A4vv7))fSJ~VG)QZB@zCl{q67mhu*$*7f;?L1}KbE_Z#v|yaz|bMONug#b zo@WCywZO==DrEim4$`-wpMMY3E9pCUlk`)UwL={}q*A23Nx0^zx9_U3KCF@`{|gLr zHT)huoOOJjis6lev0a}B{nkHzyv{IaTY=skyg}&Qqjs)ToCkW3uKzc<; zyO-AHkrR9`R*ZJ;*TMe~41Na`{RT|~fENS8s~}n}-Zub*8Rsshe18=N8Yq|1_vsNR z0Lik7ZcIBofSKavysUGbp8L@w3YU{-2uV!K1jaFqetWgH+T~Uhs|qs@cR%gH+q%zn|(_JO%6E^@4i%9IjvAJV56; zd3*!%IC4HHS?ZXBRZ1JqQFyuKN>-U1u?`{|u6)q#hpD5Mo^bz&tH zXzr)xoyd=3;%!X_X{N(=2VSvL9Hn>lQ;T%$5)Xy3S7?K@8yw$Va6v!4M_`CYKV8^Q z7afK+g$S^#XuEpefF$C;a2HKQdmpSl2>acSo%0wd9s~rxO@GyZSuJ3-6EM06SlI(_ zTt+{@{3pTbQ3U8?@X3?l_Beq21on*|VPAL(!2L01{zQ4S8*tr;-RKF7N$?ee{wb{1 zMYBM48P;4%Tj^;~d$Hd6^i%p7d~*$GpP`?lZ$CWy3_YvB{!kVJ52>(5K?@B1LV>*y zsCq67_Ie5ghJUbbAmd~F)oE0#(eoN@)R0igYwr(ft6W5tfb#4KZpVo$n$TSsL^kE)|+6GC%+e^ON6JexJ1(RgVZI( zLBv7w#ecHH?2#QNf|n=*b}=Vr;s>Zml&g@B1k~%NZiK?qgLDbv$Z8oeHbBV%vQaIC zywa5l`3LAyiK(80G{K3ko{;vy!J$vdqP@}?P;a3C^0AjLz|L<$I*V-e48kW;Ozy)j zv@dJGCV-9TFBttBO{V`ru6`ZH`3H>mPdbPGmw(`OAHF!n)~vc$-)Y>}}q*=5a=J&T+;h(To%g=;-uHc;^UOW(@!EUT z32#VNR=r>WZ;MT_Bp~v-7-|(FV@#zG5kUJK^ILKwPAdM$MPB&Lg{X%fF7e%GE04SE z(253pWgqdTD{uQY97Jn8_ju9c)lbTEl|TF~`|Ye>eHa3r9%1p9{U5k8rw4 z%7>IAEzQq%_O3k*x%0+3sWW0FFndLv(0NItE89N*f;HBn`*m+9N1A$B#w77b)DEQq z#=l-jn&q4d^g87q9U?4!*vTW$6@ACy%$br>jZ8xfBrmU06r!r?ZhRvKMOv_pLOadO z_n^1jCqCxi?+O%q-pmdZtVUngG=1>1I z98J}DeJzyEz@S1b3CL?Q!EGE}52NGv=B8~^(oH$4DUK4q+2}V~d?iVLjyq?i{xUO_vQ`Ha|Ls*U!VDyUyAvz^5|9{!h`CfY)#R zBLrrqG-6#!tWf%GDH9o~MqiI*Ufng`_1w*SE77j-R@N*tRoQ#e{1#4SCL={2d#3y| zB~S86pv;o6Rb`v3hqs7riCb}ghq4QW<>C3qE**B#n$n|l)&nuMp9rRh!V;8nwdlmq zuRoT2XJ*FusKbT-Erl>Yv8aO%tl8GJOSkwjXS*U8`LmyZT4i z!tRe%rAcVOk-fA4^+8qHBgV}Z_P}}6hUvT}k2B@#VVMGUXM>dIJ*v2)hM~t;dWS^f zYTl0KqJ87HYi`AaTQ-YP^cnBF+`fqBX)h5$-|Ic!Fwd%d#x@l&6c z$Jd)TI=U_9YST~>k#dCdkjRavH_9cnt9^B=tHP#-TIY$nomT7n zWM21SH~d7R2mS=n0Ht|L^t3}&FQezZq;duZBVIZ)ayBtIWKd$k9CuZ}GsFB<@2nLU zW`@wj`+P{jMNQr{t1bO9Dx}3+QPKRALd7t7p|echE5~h7!?(ig)gZ=vwPC~hE}=Pc zEVpx1(v~R4J*cMoz1;F%lh72RYUc_UjkL2ZoQthTB2%R-K%j;0=9ktBB-svTl3n}` z{j;d0v{!;oC%^WrG~ig(?c6Q}clYVG*^0%xEb4C&p7(tYu(BaY6+eDNDDobG#;S=J zzE^$Z;a^-?w6`dPs)1)i zXKGa%yvl0lV$~kh5jn(Fv|?Qrt#0zPuupP25HMY^?CV-@rX6sKhF^*%h@uo`6fL_t zmjL^ku*iGkj*g!+-u7{jg)Ekn@Z@Z(2p|DxVw3b2BQIz8`F7qs?hPScuN~`lAl#rG zHSdm2kQ!ed?~updv~ruh_`q(W+og|aKsS)Sff@Y9i@l(!-?>d!AxbgiZE{(%s7Frq z|B#7n-zLOOcHp+9v0|ULwK1_6luk%XayjO$p2GE(7Y{FWCZ780 z7nFOeY08*)bh|9E>BZ=%@5%31uD}-k*7D^inva)qHc9m1@%OmfuU?u&l4sS?!)#ltHs>y+k+FktWBH z23`q4ou+ZMI8OIK0RGE+e;uQV2GOxS((C&2w7{m6h@z;VJ;>_l_vSJ6`Zb=o zq@X?689ur;(Hf_f&%$*_QQL!BFA}Rs{9P}NoazTIKX_5~@(Nt3#<7;2j8~#bm3f?d z)#_L!UD)TQmY^V2<1JGu(#MlH)GSA3tru8 z&Be$X4o>&T zFz3PIp>{{q{WoR7$tKC?)@qZYrFao}#%YAQYPpwi0kakb#FE;jHnofu#pH55J#njd zYc;rza}rAzn|*GrqmzI|;p%G=(?aQ=UET7(sc8r@5dYeHXf5y~pQ}h$ia6pbEaTet z#iPCli?~;}qOKRjpmjl5)oG)r6V? zHNE&;K}nn7)JfJl5puyZJ93N2P|kVy9~*~oHlJ62(mQOTXXxYnw<1(JvtqbPI7fs_ z<|nSUxS?(IS?CMOk%P~%ap=Plyu#}4V3b*($QC4I7ZY+g#=3>%CKQJizarNe+>m8h z*Huj0jALg{bye9M-dL*Fkww%5BRMLNfw1XqvS*nvQa@~kIGYPdWuH! zKL`r)AjOw|3fyoioKcN#aqr`OUSJxQ*u1=~fH^o3FdHtFIS;hMi!V`^7_2;4C5siKj4rKx+(MrSy?^zaADG@E+v>8vVHd z%Dg;Oh!!AraDyrw_%Y4~(1oxuf!@>uADzTNEoUPe!T@Z~vmvM@v<^Wa!(cR^`7Z^Y z@zDW{RB&}c5pl2_&MyideT3zo;y4nBgT+05hvL{l$QW8GNM)IqG@v<1Y7duE1pr;J zhv`djXrTnvM9G2!lS^!f1KWXqH3s&}UFsije|L}|unzi~D^g7Y%lwG_Y*S0Hh)*mB z1cE%^cUFlC1%S}+X(7@Fpni2K^kx}_`0w>RNC+H&a<%?xi5@_bWuZ}4RF$QHY5&I{ zsBte11hNk1ohTph6SoB;Ed#{C;OiHGkA~S(LotaJS!k>)*r+^^w1R?WdIHn$Wq?sK z8?e0sho<^~DguyQWdqKQvmpMPWne=>zZwGvY9$zuf{6f2qbR6R2#^Pp1T@A_(2Nkk zcT5%98UoyB;RU3b;J_GYH3q6HHKv4qlR5%JGO{1Wjg;do~nGG}N0Q_?v&=Dda;i \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,101 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f93..53a6b238d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 0dd02b180..17756dbc6 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -52,4 +52,9 @@ jar { } } +test { + minHeapSize = "512m" + maxHeapSize = "4096m" +} + description = 'Reactor Netty RSocket transport implementations (TCP, Websocket)' From c65683ecc0df0c12b6c7c049c1e3cb3b705fc212 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 5 Apr 2023 20:47:53 +0300 Subject: [PATCH 092/116] ensures last frame is delivered in UP Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- .../UnboundedProcessorStressTest.java | 64 ++++++++ .../internal/BaseDuplexConnection.java | 4 +- .../rsocket/internal/UnboundedProcessor.java | 139 ++++++++++++++++-- .../local/LocalDuplexConnection.java | 7 +- .../transport/netty/TcpDuplexConnection.java | 21 +-- .../netty/WebsocketDuplexConnection.java | 21 +-- 6 files changed, 208 insertions(+), 48 deletions(-) diff --git a/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java index bdbdc7a3b..2f5e51f0e 100644 --- a/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java +++ b/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java @@ -895,6 +895,70 @@ public void arbiter(LLL_Result r) { } } + @JCStressTest + @Outcome( + id = { + "0, 1, 0, 5", + "1, 1, 0, 5", + "2, 1, 0, 5", + "3, 1, 0, 5", + "4, 1, 0, 5", + "5, 1, 0, 5", + }, + expect = Expect.ACCEPTABLE, + desc = "onComplete()") + @State + public static class Smoke33StressTest extends UnboundedProcessorStressTest { + + final StressSubscriber stressSubscriber = + new StressSubscriber<>(Long.MAX_VALUE, Fuseable.NONE); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + final ByteBuf byteBuf5 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(5); + + { + unboundedProcessor.subscribe(stressSubscriber); + } + + @Actor + public void next1() { + unboundedProcessor.tryEmitNormal(byteBuf1); + unboundedProcessor.tryEmitPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.tryEmitPrioritized(byteBuf3); + unboundedProcessor.tryEmitNormal(byteBuf4); + } + + @Actor + public void complete() { + unboundedProcessor.tryEmitFinal(byteBuf5); + } + + @Arbiter + public void arbiter(LLLL_Result r) { + r.r1 = stressSubscriber.onNextCalls; + r.r2 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + r.r4 = stressSubscriber.values.get(stressSubscriber.values.size() - 1).readByte(); + stressSubscriber.values.forEach(ByteBuf::release); + + r.r3 = + byteBuf1.refCnt() + + byteBuf2.refCnt() + + byteBuf3.refCnt() + + byteBuf4.refCnt() + + byteBuf5.refCnt(); + } + } + @JCStressTest @Outcome( id = { diff --git a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java index 09026356f..0296b0a07 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java @@ -30,9 +30,9 @@ public BaseDuplexConnection() {} @Override public void sendFrame(int streamId, ByteBuf frame) { if (streamId == 0) { - sender.onNextPrioritized(frame); + sender.tryEmitPrioritized(frame); } else { - sender.onNext(frame); + sender.tryEmitNormal(frame); } } 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 520ff318a..95bc210fe 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -91,6 +91,8 @@ public final class UnboundedProcessor extends Flux static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(UnboundedProcessor.class, "requested"); + ByteBuf last; + boolean outputFused; public UnboundedProcessor() { @@ -121,78 +123,127 @@ public Object scanUnsafe(Attr key) { return null; } - public void onNextPrioritized(ByteBuf t) { + public boolean tryEmitPrioritized(ByteBuf t) { if (this.done || this.cancelled) { release(t); - return; + return false; } if (!this.priorityQueue.offer(t)) { onError(Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext())); release(t); - return; + return false; } final long previousState = markValueAdded(this); if (isFinalized(previousState)) { this.clearSafely(); - return; + return false; } if (isSubscriberReady(previousState)) { if (this.outputFused) { // fast path for fusion this.actual.onNext(null); - return; + return true; } if (isWorkInProgress(previousState)) { - return; + return true; } if (hasRequest(previousState)) { drainRegular(previousState); } } + return true; } - @Override - public void onNext(ByteBuf t) { + public boolean tryEmitNormal(ByteBuf t) { if (this.done || this.cancelled) { release(t); - return; + return false; } if (!this.queue.offer(t)) { onError(Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext())); release(t); - return; + return false; } final long previousState = markValueAdded(this); if (isFinalized(previousState)) { this.clearSafely(); - return; + return false; } if (isSubscriberReady(previousState)) { if (this.outputFused) { // fast path for fusion this.actual.onNext(null); - return; + return true; } if (isWorkInProgress(previousState)) { - return; + return true; } if (hasRequest(previousState)) { drainRegular(previousState); } } + + return true; + } + + public boolean tryEmitFinal(ByteBuf t) { + if (this.done || this.cancelled) { + release(t); + return false; + } + + this.last = t; + this.done = true; + + final long previousState = markValueAddedAndTerminated(this); + if (isFinalized(previousState)) { + this.clearSafely(); + return false; + } + + if (isSubscriberReady(previousState)) { + if (this.outputFused) { + // fast path for fusion + this.actual.onNext(null); + this.actual.onComplete(); + return true; + } + + if (isWorkInProgress(previousState)) { + return true; + } + + if (hasRequest(previousState)) { + drainRegular(previousState); + } + } + + return true; + } + + @Deprecated + public void onNextPrioritized(ByteBuf t) { + tryEmitPrioritized(t); } @Override + @Deprecated + public void onNext(ByteBuf t) { + tryEmitNormal(t); + } + + @Override + @Deprecated public void onError(Throwable t) { if (this.done || this.cancelled) { Operators.onErrorDropped(t, currentContext()); @@ -235,6 +286,7 @@ public void onError(Throwable t) { } @Override + @Deprecated public void onComplete() { if (this.done || this.cancelled) { return; @@ -363,6 +415,11 @@ boolean checkTerminated(boolean done, boolean empty, CoreSubscriber queue = this.queue; final Queue priorityQueue = this.priorityQueue; + final ByteBuf last = this.last; + + if (last != null) { + release(last); + } + ByteBuf byteBuf; while ((byteBuf = queue.poll()) != null) { release(byteBuf); @@ -745,6 +826,36 @@ static long markValueAdded(UnboundedProcessor instance) { } } + /** + * Sets {@link #FLAG_HAS_VALUE} flag if it was not set before and if flags {@link + * #FLAG_FINALIZED}, {@link #FLAG_CANCELLED}, {@link #FLAG_DISPOSED} are unset. Also, this method + * increments number of work in progress (WIP) if {@link #FLAG_HAS_REQUEST} is set + * + * @return previous state + */ + static long markValueAddedAndTerminated(UnboundedProcessor instance) { + for (; ; ) { + final long state = instance.state; + + if (isFinalized(state)) { + return state; + } + + long nextState = state; + if (isWorkInProgress(state)) { + nextState = addWork(state); + } else if (isSubscriberReady(state) && !instance.outputFused) { + if (hasRequest(state)) { + nextState = addWork(state); + } + } + + if (STATE.compareAndSet(instance, state, nextState | FLAG_HAS_VALUE | FLAG_TERMINATED)) { + return state; + } + } + } + /** * Sets {@link #FLAG_TERMINATED} flag if it was not set before and if flags {@link * #FLAG_FINALIZED}, {@link #FLAG_CANCELLED}, {@link #FLAG_DISPOSED} are unset. Also, this method 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 5c395156c..08fd780dc 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 @@ -89,17 +89,16 @@ public Flux receive() { @Override public void sendFrame(int streamId, ByteBuf frame) { if (streamId == 0) { - out.onNextPrioritized(frame); + out.tryEmitPrioritized(frame); } else { - out.onNext(frame); + out.tryEmitNormal(frame); } } @Override public void sendErrorAndClose(RSocketErrorException e) { final ByteBuf errorFrame = ErrorFrameCodec.encode(allocator, 0, e); - out.onNext(errorFrame); - dispose(); + out.tryEmitFinal(errorFrame); } @Override 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 901d1ba9a..0445f5c02 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 @@ -43,14 +43,11 @@ public TcpDuplexConnection(Connection connection) { this.connection = Objects.requireNonNull(connection, "connection must not be null"); connection - .channel() - .closeFuture() - .addListener( - future -> { - if (!isDisposed()) dispose(); - }); - - connection.outbound().send(sender).then().subscribe(); + .outbound() + .send(sender.hide()) + .then() + .doFinally(__ -> connection.dispose()) + .subscribe(); } @Override @@ -70,17 +67,13 @@ protected void doOnClose() { @Override public Mono onClose() { - return super.onClose().and(connection.onDispose()); + return Mono.whenDelayError(super.onClose(), connection.onTerminate()); } @Override public void sendErrorAndClose(RSocketErrorException e) { final ByteBuf errorFrame = ErrorFrameCodec.encode(alloc(), 0, e); - connection - .outbound() - .sendObject(FrameLengthCodec.encode(alloc(), errorFrame.readableBytes(), errorFrame)) - .subscribe(connection.disposeSubscriber()); - sender.onComplete(); + sender.tryEmitFinal(FrameLengthCodec.encode(alloc(), errorFrame.readableBytes(), errorFrame)); } @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 542ff3599..9deef6030 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 @@ -48,14 +48,11 @@ public WebsocketDuplexConnection(Connection connection) { this.connection = Objects.requireNonNull(connection, "connection must not be null"); connection - .channel() - .closeFuture() - .addListener( - future -> { - if (!isDisposed()) dispose(); - }); - - connection.outbound().sendObject(sender.map(BinaryWebSocketFrame::new)).then().subscribe(); + .outbound() + .sendObject(sender.map(BinaryWebSocketFrame::new).hide()) + .then() + .doFinally(__ -> connection.dispose()) + .subscribe(); } @Override @@ -75,7 +72,7 @@ protected void doOnClose() { @Override public Mono onClose() { - return super.onClose().and(connection.onDispose()); + return Mono.whenDelayError(super.onClose(), connection.onTerminate()); } @Override @@ -86,10 +83,6 @@ public Flux receive() { @Override public void sendErrorAndClose(RSocketErrorException e) { final ByteBuf errorFrame = ErrorFrameCodec.encode(alloc(), 0, e); - connection - .outbound() - .sendObject(new BinaryWebSocketFrame(errorFrame)) - .subscribe(connection.disposeSubscriber()); - sender.onComplete(); + sender.tryEmitFinal(errorFrame); } } From 17f5d74c0603ca28b908bbf88e1d9db97538a12d Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 5 Apr 2023 20:52:29 +0300 Subject: [PATCH 093/116] fixes flaky tests Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- .../io/rsocket/core/AbstractSocketRule.java | 6 +- .../WeightedLoadbalanceStrategyTest.java | 33 +- .../resume/ClientRSocketSessionTest.java | 817 +++++++++--------- 3 files changed, 447 insertions(+), 409 deletions(-) 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 a3e5a62ff..310e15b3e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java +++ b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java @@ -44,12 +44,12 @@ public void init() { } protected void doInit() { - if (socket != null) { - socket.dispose(); - } if (connection != null) { connection.dispose(); } + if (socket != null) { + socket.dispose(); + } connection = new TestDuplexConnection(allocator); socket = newRSocket(); } diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategyTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategyTest.java index 6640aea4e..8cc254cbb 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategyTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategyTest.java @@ -80,8 +80,13 @@ public Mono fireAndForget(Payload payload) { LoadbalanceTarget.from("1", mockTransport), LoadbalanceTarget.from("2", mockTransport))); - Assertions.assertThat(counter1.get()).isCloseTo(1000, Offset.offset(1)); - Assertions.assertThat(counter2.get()).isCloseTo(0, Offset.offset(1)); + Assertions.assertThat(counter1.get()) + .describedAs("c1=" + counter1.get() + " c2=" + counter2.get()) + .isCloseTo( + RaceTestConstants.REPEATS, Offset.offset(Math.round(RaceTestConstants.REPEATS * 0.1f))); + Assertions.assertThat(counter2.get()) + .describedAs("c1=" + counter1.get() + " c2=" + counter2.get()) + .isCloseTo(0, Offset.offset(Math.round(RaceTestConstants.REPEATS * 0.1f))); } @Test @@ -165,8 +170,11 @@ public Mono fireAndForget(Payload payload) { } Assertions.assertThat(counter1.get()) - .isCloseTo(RaceTestConstants.REPEATS * 3, Offset.offset(100)); - Assertions.assertThat(counter2.get()).isCloseTo(0, Offset.offset(100)); + .isCloseTo( + RaceTestConstants.REPEATS * 3, + Offset.offset(Math.round(RaceTestConstants.REPEATS * 3 * 0.1f))); + Assertions.assertThat(counter2.get()) + .isCloseTo(0, Offset.offset(Math.round(RaceTestConstants.REPEATS * 3 * 0.1f))); rSocket2.updateAvailability(0.0); @@ -177,8 +185,13 @@ public Mono fireAndForget(Payload payload) { } Assertions.assertThat(counter1.get()) - .isCloseTo(RaceTestConstants.REPEATS * 3, Offset.offset(100)); - Assertions.assertThat(counter2.get()).isCloseTo(RaceTestConstants.REPEATS, Offset.offset(100)); + .isCloseTo( + RaceTestConstants.REPEATS * 3, + Offset.offset(Math.round(RaceTestConstants.REPEATS * 4 * 0.1f))); + Assertions.assertThat(counter2.get()) + .isCloseTo( + RaceTestConstants.REPEATS, + Offset.offset(Math.round(RaceTestConstants.REPEATS * 4 * 0.1f))); source.next( Arrays.asList( @@ -191,9 +204,13 @@ public Mono fireAndForget(Payload payload) { } Assertions.assertThat(counter1.get()) - .isCloseTo(RaceTestConstants.REPEATS * 3, Offset.offset(100)); + .isCloseTo( + RaceTestConstants.REPEATS * 3, + Offset.offset(Math.round(RaceTestConstants.REPEATS * 5 * 0.1f))); Assertions.assertThat(counter2.get()) - .isCloseTo(RaceTestConstants.REPEATS * 2, Offset.offset(100)); + .isCloseTo( + RaceTestConstants.REPEATS * 2, + Offset.offset(Math.round(RaceTestConstants.REPEATS * 5 * 0.1f))); } static class WeightedTestRSocket extends BaseWeightedStats implements RSocket { diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java index 34d8a7345..bdd46f8c6 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java @@ -29,418 +29,439 @@ public class ClientRSocketSessionTest { @Test void sessionTimeoutSmokeTest() { final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); - final TestClientTransport transport = new TestClientTransport(); - final InMemoryResumableFramesStore framesStore = - new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); - - transport.connect().subscribe(); - - final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection( - "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); - - resumableDuplexConnection.receive().subscribe(); - - final ClientRSocketSession session = - new ClientRSocketSession( - Unpooled.EMPTY_BUFFER, - resumableDuplexConnection, - transport.connect().delaySubscription(Duration.ofMillis(1)), - c -> { - AtomicBoolean firstHandled = new AtomicBoolean(); - return ((TestDuplexConnection) c) - .receive() - .next() - .doOnNext(__ -> firstHandled.set(true)) - .doOnCancel( - () -> { - if (firstHandled.compareAndSet(false, true)) { - c.dispose(); - } - }) - .map(b -> Tuples.of(b, c)); - }, - framesStore, - Duration.ofMinutes(1), - Retry.indefinitely(), - true); - - final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = - new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); - session.setKeepAliveSupport(keepAliveSupport); - - // connection is active. just advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - // deactivate connection - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time so new connection is received - virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); - // timeout should not terminate current connection - assertThat(transport.testConnection().isDisposed()).isFalse(); - - // send RESUME_OK frame - transport.testConnection().addToReceivedBuffer(ResumeOkFrameCodec.encode(transport.alloc(), 0)); - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be terminated - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME) - .matches(ReferenceCounted::release); - - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); - - // disconnects for the second time - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time so new connection is received - virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME) - .matches(ReferenceCounted::release); - - transport - .testConnection() - .addToReceivedBuffer( - ErrorFrameCodec.encode( - transport.alloc(), 0, new ConnectionCloseException("some message"))); - // connection should be closed because of the wrong first frame - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout is still in progress - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); - // should obtain new connection - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_OK frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME) - .matches(ReferenceCounted::release); - - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); - - assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); - assertThat(transport.testConnection().isDisposed()).isTrue(); - - assertThat(session.isDisposed()).isTrue(); - - resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); - transport.alloc().assertHasNoLeaks(); + try { + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ClientRSocketSession session = + new ClientRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.connect().delaySubscription(Duration.ofMillis(1)), + c -> { + AtomicBoolean firstHandled = new AtomicBoolean(); + return ((TestDuplexConnection) c) + .receive() + .next() + .doOnNext(__ -> firstHandled.set(true)) + .doOnCancel( + () -> { + if (firstHandled.compareAndSet(false, true)) { + c.dispose(); + } + }) + .map(b -> Tuples.of(b, c)); + }, + framesStore, + Duration.ofMinutes(1), + Retry.indefinitely(), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + // deactivate connection + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); + // timeout should not terminate current connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + + // send RESUME_OK frame + transport + .testConnection() + .addToReceivedBuffer(ResumeOkFrameCodec.encode(transport.alloc(), 0)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be terminated + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); + + // disconnects for the second time + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + transport + .testConnection() + .addToReceivedBuffer( + ErrorFrameCodec.encode( + transport.alloc(), 0, new ConnectionCloseException("some message"))); + // connection should be closed because of the wrong first frame + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout is still in progress + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); + // should obtain new connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_OK frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); + + assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); + assertThat(transport.testConnection().isDisposed()).isTrue(); + + assertThat(session.isDisposed()).isTrue(); + + resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } } @Test void sessionTerminationOnWrongFrameTest() { final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); - final TestClientTransport transport = new TestClientTransport(); - final InMemoryResumableFramesStore framesStore = - new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); - - transport.connect().subscribe(); - - final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection( - "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); - - resumableDuplexConnection.receive().subscribe(); - - final ClientRSocketSession session = - new ClientRSocketSession( - Unpooled.EMPTY_BUFFER, - resumableDuplexConnection, - transport.connect().delaySubscription(Duration.ofMillis(1)), - c -> { - AtomicBoolean firstHandled = new AtomicBoolean(); - return ((TestDuplexConnection) c) - .receive() - .next() - .doOnNext(__ -> firstHandled.set(true)) - .doOnCancel( - () -> { - if (firstHandled.compareAndSet(false, true)) { - c.dispose(); - } - }) - .map(b -> Tuples.of(b, c)); - }, - framesStore, - Duration.ofMinutes(1), - Retry.indefinitely(), - true); - - final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = - new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); - session.setKeepAliveSupport(keepAliveSupport); - - // connection is active. just advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - // deactivate connection - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time so new connection is received - virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME) - .matches(ReferenceCounted::release); - - // advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); - // timeout should not terminate current connection - assertThat(transport.testConnection().isDisposed()).isFalse(); - - // send RESUME_OK frame - transport.testConnection().addToReceivedBuffer(ResumeOkFrameCodec.encode(transport.alloc(), 0)); - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be terminated - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); - - // disconnects for the second time - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time so new connection is received - virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME) - .matches(ReferenceCounted::release); - - // Send KEEPALIVE frame as a first frame - transport - .testConnection() - .addToReceivedBuffer( - KeepAliveFrameCodec.encode(transport.alloc(), false, 0, Unpooled.EMPTY_BUFFER)); - - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); - - assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); - assertThat(transport.testConnection().isDisposed()).isTrue(); - assertThat(session.isDisposed()).isTrue(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.ERROR) - .matches(ReferenceCounted::release); - - resumableDuplexConnection - .onClose() - .as(StepVerifier::create) - .expectErrorMessage("RESUME_OK frame must be received before any others") - .verify(); - transport.alloc().assertHasNoLeaks(); + try { + + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ClientRSocketSession session = + new ClientRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.connect().delaySubscription(Duration.ofMillis(1)), + c -> { + AtomicBoolean firstHandled = new AtomicBoolean(); + return ((TestDuplexConnection) c) + .receive() + .next() + .doOnNext(__ -> firstHandled.set(true)) + .doOnCancel( + () -> { + if (firstHandled.compareAndSet(false, true)) { + c.dispose(); + } + }) + .map(b -> Tuples.of(b, c)); + }, + framesStore, + Duration.ofMinutes(1), + Retry.indefinitely(), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + // deactivate connection + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); + // timeout should not terminate current connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + + // send RESUME_OK frame + transport + .testConnection() + .addToReceivedBuffer(ResumeOkFrameCodec.encode(transport.alloc(), 0)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be terminated + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); + + // disconnects for the second time + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + // Send KEEPALIVE frame as a first frame + transport + .testConnection() + .addToReceivedBuffer( + KeepAliveFrameCodec.encode(transport.alloc(), false, 0, Unpooled.EMPTY_BUFFER)); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); + + assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); + assertThat(transport.testConnection().isDisposed()).isTrue(); + assertThat(session.isDisposed()).isTrue(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.ERROR) + .matches(ReferenceCounted::release); + + resumableDuplexConnection + .onClose() + .as(StepVerifier::create) + .expectErrorMessage("RESUME_OK frame must be received before any others") + .verify(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } } @Test void shouldErrorWithNoRetriesOnErrorFrameTest() { final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); - final TestClientTransport transport = new TestClientTransport(); - final InMemoryResumableFramesStore framesStore = - new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); - - transport.connect().subscribe(); - - final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection( - "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); - - resumableDuplexConnection.receive().subscribe(); - - final ClientRSocketSession session = - new ClientRSocketSession( - Unpooled.EMPTY_BUFFER, - resumableDuplexConnection, - transport.connect().delaySubscription(Duration.ofMillis(1)), - c -> { - AtomicBoolean firstHandled = new AtomicBoolean(); - return ((TestDuplexConnection) c) - .receive() - .next() - .doOnNext(__ -> firstHandled.set(true)) - .doOnCancel( - () -> { - if (firstHandled.compareAndSet(false, true)) { - c.dispose(); - } - }) - .map(b -> Tuples.of(b, c)); - }, - framesStore, - Duration.ofMinutes(1), - Retry.indefinitely(), - true); - - final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = - new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); - session.setKeepAliveSupport(keepAliveSupport); - - // connection is active. just advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - // deactivate connection - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time so new connection is received - virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME) - .matches(ReferenceCounted::release); - - // advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); - // timeout should not terminate current connection - assertThat(transport.testConnection().isDisposed()).isFalse(); - - // send REJECTED_RESUME_ERROR frame - transport - .testConnection() - .addToReceivedBuffer( - ErrorFrameCodec.encode( - transport.alloc(), 0, new RejectedResumeException("failed resumption"))); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // timeout should be terminated - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isTrue(); - - resumableDuplexConnection - .onClose() - .as(StepVerifier::create) - .expectError(RejectedResumeException.class) - .verify(); - transport.alloc().assertHasNoLeaks(); + try { + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ClientRSocketSession session = + new ClientRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.connect().delaySubscription(Duration.ofMillis(1)), + c -> { + AtomicBoolean firstHandled = new AtomicBoolean(); + return ((TestDuplexConnection) c) + .receive() + .next() + .doOnNext(__ -> firstHandled.set(true)) + .doOnCancel( + () -> { + if (firstHandled.compareAndSet(false, true)) { + c.dispose(); + } + }) + .map(b -> Tuples.of(b, c)); + }, + framesStore, + Duration.ofMinutes(1), + Retry.indefinitely(), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + // deactivate connection + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); + // timeout should not terminate current connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + + // send REJECTED_RESUME_ERROR frame + transport + .testConnection() + .addToReceivedBuffer( + ErrorFrameCodec.encode( + transport.alloc(), 0, new RejectedResumeException("failed resumption"))); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // timeout should be terminated + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isTrue(); + + resumableDuplexConnection + .onClose() + .as(StepVerifier::create) + .expectError(RejectedResumeException.class) + .verify(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } } @Test void shouldTerminateConnectionOnIllegalStateInKeepAliveFrame() { final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); - final TestClientTransport transport = new TestClientTransport(); - final InMemoryResumableFramesStore framesStore = - new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); - - transport.connect().subscribe(); - - final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection( - "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); - - resumableDuplexConnection.receive().subscribe(); - - final ClientRSocketSession session = - new ClientRSocketSession( - Unpooled.EMPTY_BUFFER, - resumableDuplexConnection, - transport.connect().delaySubscription(Duration.ofMillis(1)), - c -> { - AtomicBoolean firstHandled = new AtomicBoolean(); - return ((TestDuplexConnection) c) - .receive() - .next() - .doOnNext(__ -> firstHandled.set(true)) - .doOnCancel( - () -> { - if (firstHandled.compareAndSet(false, true)) { - c.dispose(); - } - }) - .map(b -> Tuples.of(b, c)); - }, - framesStore, - Duration.ofMinutes(1), - Retry.indefinitely(), - true); - - final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = - new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); - keepAliveSupport.resumeState(session); - session.setKeepAliveSupport(keepAliveSupport); - - // connection is active. just advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - final ByteBuf keepAliveFrame = - KeepAliveFrameCodec.encode(transport.alloc(), false, 1529, Unpooled.EMPTY_BUFFER); - keepAliveSupport.receive(keepAliveFrame); - keepAliveFrame.release(); - - assertThat(transport.testConnection().isDisposed()).isTrue(); - // timeout should be terminated - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isTrue(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.ERROR) - .matches(ReferenceCounted::release); - - resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); - - transport.alloc().assertHasNoLeaks(); + try { + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ClientRSocketSession session = + new ClientRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.connect().delaySubscription(Duration.ofMillis(1)), + c -> { + AtomicBoolean firstHandled = new AtomicBoolean(); + return ((TestDuplexConnection) c) + .receive() + .next() + .doOnNext(__ -> firstHandled.set(true)) + .doOnCancel( + () -> { + if (firstHandled.compareAndSet(false, true)) { + c.dispose(); + } + }) + .map(b -> Tuples.of(b, c)); + }, + framesStore, + Duration.ofMinutes(1), + Retry.indefinitely(), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + keepAliveSupport.resumeState(session); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + final ByteBuf keepAliveFrame = + KeepAliveFrameCodec.encode(transport.alloc(), false, 1529, Unpooled.EMPTY_BUFFER); + keepAliveSupport.receive(keepAliveFrame); + keepAliveFrame.release(); + + assertThat(transport.testConnection().isDisposed()).isTrue(); + // timeout should be terminated + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isTrue(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.ERROR) + .matches(ReferenceCounted::release); + + resumableDuplexConnection.onClose().as(StepVerifier::create).expectError().verify(); + + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } } } From 1936e8ccfebb25ff7b82470c6e186ff93fed7490 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 5 Apr 2023 20:57:45 +0300 Subject: [PATCH 094/116] fixes removal ordering Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- .../core/RequestStreamRequesterFlux.java | 10 ++++++---- .../RequestStreamResponderSubscriber.java | 20 +++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java index 55ec43feb..6182ca506 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java @@ -238,11 +238,11 @@ void sendFirstPayload(Payload payload, long initialRequestN) { return; } - sm.remove(streamId, this); - final ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, streamId); connection.sendFrame(streamId, cancelFrame); + sm.remove(streamId, this); + if (requestInterceptor != null) { requestInterceptor.onCancel(streamId, FrameType.REQUEST_STREAM); } @@ -276,12 +276,13 @@ public final void cancel() { if (isFirstFrameSent(previousState)) { final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); ReassemblyUtils.synchronizedRelease(this, previousState); this.connection.sendFrame(streamId, CancelFrameCodec.encode(this.allocator, streamId)); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onCancel(streamId, FrameType.REQUEST_STREAM); @@ -309,13 +310,14 @@ public final void handlePayload(Payload p) { } final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); final IllegalStateException cause = Exceptions.failWithOverflow( "The number of messages received exceeds the number requested"); this.connection.sendFrame(streamId, CancelFrameCodec.encode(this.allocator, streamId)); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, cause); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamResponderSubscriber.java b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamResponderSubscriber.java index 774fae9e5..48903ae38 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamResponderSubscriber.java @@ -144,6 +144,8 @@ public void onNext(Payload p) { final ByteBuf errorFrame = ErrorFrameCodec.encode(allocator, streamId, e); sender.sendFrame(streamId, errorFrame); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, e); @@ -162,6 +164,8 @@ public void onNext(Payload p) { new CanceledException("Failed to validate payload. Cause" + e.getMessage())); sender.sendFrame(streamId, errorFrame); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, e); @@ -176,6 +180,8 @@ public void onNext(Payload p) { return; } + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, t); @@ -195,8 +201,6 @@ boolean tryTerminateOnError() { return false; } - this.requesterResponderSupport.remove(this.streamId, this); - currentSubscription.cancel(); return true; @@ -222,11 +226,12 @@ public void onError(Throwable t) { } final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); final ByteBuf errorFrame = ErrorFrameCodec.encode(this.allocator, streamId, t); this.connection.sendFrame(streamId, errorFrame); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, t); @@ -246,11 +251,12 @@ public void onComplete() { } final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(this.allocator, streamId); this.connection.sendFrame(streamId, completeFrame); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, null); @@ -321,7 +327,6 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas S.lazySet(this, Operators.cancelledSubscription()); final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); this.frames = null; frames.release(); @@ -334,6 +339,8 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas new CanceledException("Failed to reassemble payload. Cause: " + e.getMessage())); this.connection.sendFrame(streamId, errorFrame); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, e); @@ -354,7 +361,6 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas this.done = true; final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); ReferenceCountUtil.safeRelease(frames); @@ -366,6 +372,8 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas new CanceledException("Failed to reassemble payload. Cause: " + t.getMessage())); this.connection.sendFrame(streamId, errorFrame); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, t); From 5ed16d9922f8a7a181c8609bd39c22bba9e57dd6 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 5 Apr 2023 21:21:34 +0300 Subject: [PATCH 095/116] fixes test expectation Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- .../test/java/io/rsocket/resume/ClientRSocketSessionTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java index bdd46f8c6..f34bb5d64 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java @@ -457,7 +457,7 @@ void shouldTerminateConnectionOnIllegalStateInKeepAliveFrame() { .typeOf(FrameType.ERROR) .matches(ReferenceCounted::release); - resumableDuplexConnection.onClose().as(StepVerifier::create).expectError().verify(); + resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); transport.alloc().assertHasNoLeaks(); } finally { From c633030b5428e38e8c465636fb5ab856ab8124c5 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Wed, 5 Apr 2023 21:22:03 +0300 Subject: [PATCH 096/116] improves leak tracker Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- .../buffer/LeaksTrackingByteBufAllocator.java | 47 +++++++++++------- rsocket-test/build.gradle | 2 + .../test/LeaksTrackingByteBufAllocator.java | 48 ++++++++++++------- 3 files changed, 64 insertions(+), 33 deletions(-) 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 04c9e4bff..1db708ab5 100644 --- a/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java +++ b/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java @@ -5,20 +5,25 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; +import io.netty.util.IllegalReferenceCountException; import io.netty.util.ResourceLeakDetector; import java.lang.reflect.Field; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import org.assertj.core.api.Assertions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Additional Utils which allows to decorate a ByteBufAllocator and track/assertOnLeaks all created * ByteBuffs */ public class LeaksTrackingByteBufAllocator implements ByteBufAllocator { + static final Logger LOGGER = LoggerFactory.getLogger(LeaksTrackingByteBufAllocator.class); /** * Allows to instrument any given the instance of ByteBufAllocator @@ -83,6 +88,7 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { return this; } + LOGGER.debug(tag + " await buffers to be released"); for (int i = 0; i < 100; i++) { System.gc(); parkNanos(1000); @@ -91,22 +97,31 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { } } - Assertions.assertThat(unreleased) - .allMatch( - bb -> { - final boolean checkResult = bb.refCnt() == 0; - - if (!checkResult) { - try { - System.out.println(tag + " " + resolveTrackingInfo(bb)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - return checkResult; - }, - tag); + Set collected = new HashSet<>(); + for (ByteBuf buf : unreleased) { + if (buf.refCnt() != 0) { + try { + collected.add(buf); + } catch (IllegalReferenceCountException ignored) { + // fine to ignore if throws because of refCnt + } + } + } + + Assertions.assertThat( + collected + .stream() + .filter(bb -> bb.refCnt() != 0) + .peek( + bb -> { + try { + LOGGER.debug(tag + " " + resolveTrackingInfo(bb)); + } catch (Exception e) { + e.printStackTrace(); + } + })) + .describedAs("[" + tag + "] all buffers expected to be released but got ") + .isEmpty(); } finally { tracker.clear(); } diff --git a/rsocket-test/build.gradle b/rsocket-test/build.gradle index d95e9bd41..bcdf88f28 100644 --- a/rsocket-test/build.gradle +++ b/rsocket-test/build.gradle @@ -28,6 +28,8 @@ dependencies { implementation 'io.projectreactor:reactor-test' implementation 'org.assertj:assertj-core' implementation 'org.mockito:mockito-core' + implementation 'org.awaitility:awaitility' + implementation 'org.slf4j:slf4j-api' } jar { diff --git a/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java b/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java index 139ae146b..46e807b09 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java +++ b/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java @@ -5,20 +5,25 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; +import io.netty.util.IllegalReferenceCountException; import io.netty.util.ResourceLeakDetector; import java.lang.reflect.Field; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import org.assertj.core.api.Assertions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Additional Utils which allows to decorate a ByteBufAllocator and track/assertOnLeaks all created * ByteBuffs */ public class LeaksTrackingByteBufAllocator implements ByteBufAllocator { + static final Logger LOGGER = LoggerFactory.getLogger(LeaksTrackingByteBufAllocator.class); /** * Allows to instrument any given the instance of ByteBufAllocator @@ -83,7 +88,7 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { return this; } - System.out.println(tag + " await buffers to be released"); + LOGGER.debug(tag + " await buffers to be released"); for (int i = 0; i < 100; i++) { System.gc(); parkNanos(1000); @@ -92,22 +97,31 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { } } - Assertions.assertThat(unreleased) - .allMatch( - bb -> { - final boolean checkResult = bb.refCnt() == 0; - - if (!checkResult) { - try { - System.out.println(tag + " " + resolveTrackingInfo(bb)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - return checkResult; - }, - tag); + Set collected = new HashSet<>(); + for (ByteBuf buf : unreleased) { + if (buf.refCnt() != 0) { + try { + collected.add(buf); + } catch (IllegalReferenceCountException ignored) { + // fine to ignore if throws because of refCnt + } + } + } + + Assertions.assertThat( + collected + .stream() + .filter(bb -> bb.refCnt() != 0) + .peek( + bb -> { + try { + LOGGER.debug(tag + " " + resolveTrackingInfo(bb)); + } catch (Exception e) { + e.printStackTrace(); + } + })) + .describedAs("[" + tag + "] all buffers expected to be released but got ") + .isEmpty(); } finally { tracker.clear(); } From e0f4bc327445c323665dbd1b27ddcc85ced3c189 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Wed, 5 Apr 2023 21:35:10 +0300 Subject: [PATCH 097/116] ensures local server awaits all connections are close before termination notification Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- .../transport/local/LocalServerTransport.java | 49 ++++++++++++++----- .../local/LocalClientTransportTest.java | 23 ++++++--- .../local/LocalServerTransportTest.java | 15 ++++-- 3 files changed, 64 insertions(+), 23 deletions(-) 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 7ea1f8cda..975cb6793 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 @@ -17,12 +17,15 @@ package io.rsocket.transport.local; import io.rsocket.Closeable; +import io.rsocket.DuplexConnection; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import java.util.Objects; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; import reactor.core.Scannable; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -34,7 +37,7 @@ */ public final class LocalServerTransport implements ServerTransport { - private static final ConcurrentMap registry = + private static final ConcurrentMap registry = new ConcurrentHashMap<>(); private final String name; @@ -72,7 +75,10 @@ public static LocalServerTransport createEphemeral() { */ public static void dispose(String name) { Objects.requireNonNull(name, "name must not be null"); - registry.remove(name); + ServerCloseableAcceptor sca = registry.remove(name); + if (sca != null) { + sca.dispose(); + } } /** @@ -107,34 +113,55 @@ public Mono start(ConnectionAcceptor acceptor) { Objects.requireNonNull(acceptor, "acceptor must not be null"); return Mono.create( sink -> { - ServerCloseable closeable = new ServerCloseable(name, acceptor); - if (registry.putIfAbsent(name, acceptor) != null) { - throw new IllegalStateException("name already registered: " + name); + ServerCloseableAcceptor closeable = new ServerCloseableAcceptor(name, acceptor); + if (registry.putIfAbsent(name, closeable) != null) { + sink.error(new IllegalStateException("name already registered: " + name)); } sink.success(closeable); }); } - static class ServerCloseable implements Closeable { + @SuppressWarnings({"ReactorTransformationOnMonoVoid", "CallingSubscribeInNonBlockingScope"}) + static class ServerCloseableAcceptor implements ConnectionAcceptor, Closeable { private final LocalSocketAddress address; private final ConnectionAcceptor acceptor; - private final Sinks.Empty onClose = Sinks.empty(); + private final Set activeConnections = ConcurrentHashMap.newKeySet(); + + private final Sinks.Empty onClose = Sinks.unsafe().empty(); - ServerCloseable(String name, ConnectionAcceptor acceptor) { + ServerCloseableAcceptor(String name, ConnectionAcceptor acceptor) { Objects.requireNonNull(name, "name must not be null"); this.address = new LocalSocketAddress(name); this.acceptor = acceptor; } + @Override + public Mono apply(DuplexConnection duplexConnection) { + activeConnections.add(duplexConnection); + duplexConnection + .onClose() + .doFinally(__ -> activeConnections.remove(duplexConnection)) + .subscribe(); + return acceptor.apply(duplexConnection); + } + @Override public void dispose() { - if (!registry.remove(address.getName(), acceptor)) { - throw new AssertionError(); + if (!registry.remove(address.getName(), this)) { + // already disposed + return; } - onClose.tryEmitEmpty(); + + Mono.whenDelayError( + activeConnections + .stream() + .peek(DuplexConnection::dispose) + .map(DuplexConnection::onClose) + .collect(Collectors.toList())) + .subscribe(null, onClose::tryEmitError, onClose::tryEmitEmpty); } @Override 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 ac4c13efe..095de3f0e 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 @@ -19,9 +19,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import io.rsocket.Closeable; +import java.time.Duration; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; import reactor.test.StepVerifier; final class LocalClientTransportTest { @@ -31,12 +32,20 @@ final class LocalClientTransportTest { void connect() { LocalServerTransport serverTransport = LocalServerTransport.createEphemeral(); - serverTransport - .start(duplexConnection -> Mono.empty()) - .flatMap(closeable -> LocalClientTransport.create(serverTransport.getName()).connect()) - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); + Closeable closeable = + serverTransport.start(duplexConnection -> duplexConnection.receive().then()).block(); + + try { + LocalClientTransport.create(serverTransport.getName()) + .connect() + .doOnNext(d -> d.receive().subscribe()) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } finally { + closeable.dispose(); + closeable.onClose().block(Duration.ofSeconds(5)); + } } @DisplayName("generates error if server not started") 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 ed906f65b..e4edafc39 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 @@ -96,11 +96,16 @@ void named() { @DisplayName("starts local server transport") @Test void start() { - LocalServerTransport.createEphemeral() - .start(duplexConnection -> Mono.empty()) - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); + LocalServerTransport ephemeral = LocalServerTransport.createEphemeral(); + try { + ephemeral + .start(duplexConnection -> Mono.empty()) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } finally { + LocalServerTransport.dispose(ephemeral.getName()); + } } @DisplayName("start throws NullPointerException with null acceptor") From 0ab392ac73eec9aa12d21d45e8f5033cd95ebd7a Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 6 Apr 2023 09:33:09 +0300 Subject: [PATCH 098/116] improves e2e tests for transports Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- .../java/io/rsocket/test/TransportTest.java | 247 +++++++++++++----- .../local/LocalResumableTransportTest.java | 29 +- ...sumableWithFragmentationTransportTest.java | 29 +- .../transport/local/LocalTransportTest.java | 21 +- .../LocalTransportWithFragmentationTest.java | 27 +- .../netty/TcpFragmentationTransportTest.java | 32 ++- .../netty/TcpResumableTransportTest.java | 34 ++- ...sumableWithFragmentationTransportTest.java | 34 ++- .../netty/TcpSecureTransportTest.java | 82 +++--- .../transport/netty/TcpTransportTest.java | 30 ++- .../WebsocketResumableTransportTest.java | 38 +-- ...sumableWithFragmentationTransportTest.java | 38 +-- .../netty/WebsocketSecureTransportTest.java | 73 +++--- .../netty/WebsocketTransportTest.java | 34 ++- 14 files changed, 491 insertions(+), 257 deletions(-) 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 570a7de2f..1fcca97db 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-2020 the original author or authors. + * Copyright 2015-2023 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. @@ -46,12 +46,14 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; import org.assertj.core.api.Assertions; import org.assertj.core.api.Assumptions; 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 org.reactivestreams.Subscription; @@ -61,9 +63,10 @@ import reactor.core.Exceptions; import reactor.core.Fuseable; 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.Sinks; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; @@ -80,7 +83,6 @@ public interface TransportTest { Payload LARGE_PAYLOAD = ByteBufPayload.create(LARGE_DATA, LARGE_DATA); static String read(String resourceName) { - try (BufferedReader br = new BufferedReader( new InputStreamReader( @@ -93,38 +95,55 @@ static String read(String resourceName) { } } + @BeforeEach + default void setup() { + Hooks.onOperatorDebug(); + } + @AfterEach default void close() { - getTransportPair().responder.awaitAllInteractionTermination(getTimeout()); - getTransportPair().dispose(); - getTransportPair().awaitClosed(); - RuntimeException throwable = - new RuntimeException() { - @Override - public synchronized Throwable fillInStackTrace() { - return this; - } - - @Override - public String getMessage() { - return Arrays.toString(getSuppressed()); - } - }; - try { - getTransportPair().byteBufAllocator2.assertHasNoLeaks(); - } catch (Throwable t) { - throwable = Exceptions.addSuppressed(throwable, t); - } - - try { - getTransportPair().byteBufAllocator1.assertHasNoLeaks(); - } catch (Throwable t) { - throwable = Exceptions.addSuppressed(throwable, t); - } - - if (throwable.getSuppressed().length > 0) { - throw throwable; + logger.debug("------------------Awaiting communication to finish------------------"); + getTransportPair().responder.awaitAllInteractionTermination(getTimeout()); + logger.debug("---------------------Disposing Client And Server--------------------"); + getTransportPair().dispose(); + getTransportPair().awaitClosed(getTimeout()); + logger.debug("------------------------Disposing Schedulers-------------------------"); + Schedulers.parallel().disposeGracefully().timeout(getTimeout(), Mono.empty()).block(); + Schedulers.boundedElastic().disposeGracefully().timeout(getTimeout(), Mono.empty()).block(); + Schedulers.single().disposeGracefully().timeout(getTimeout(), Mono.empty()).block(); + logger.debug("---------------------------Leaks Checking----------------------------"); + RuntimeException throwable = + new RuntimeException() { + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + + @Override + public String getMessage() { + return Arrays.toString(getSuppressed()); + } + }; + + try { + getTransportPair().byteBufAllocator2.assertHasNoLeaks(); + } catch (Throwable t) { + throwable = Exceptions.addSuppressed(throwable, t); + } + + try { + getTransportPair().byteBufAllocator1.assertHasNoLeaks(); + } catch (Throwable t) { + throwable = Exceptions.addSuppressed(throwable, t); + } + + if (throwable.getSuppressed().length > 0) { + throw throwable; + } + } finally { + Hooks.resetOnOperatorDebug(); + Schedulers.resetOnHandleError(); } } @@ -226,7 +245,7 @@ default void requestChannel1() { .requestChannel(Mono.just(createTestPayload(0))) .doOnNext(Payload::release) .as(StepVerifier::create) - .expectNextCount(1) + .thenConsumeWhile(new PayloadPredicate(1)) .expectComplete() .verify(getTimeout()); } @@ -241,7 +260,7 @@ default void requestChannel200_000() { .doOnNext(Payload::release) .limitRate(8) .as(StepVerifier::create) - .expectNextCount(200_000) + .thenConsumeWhile(new PayloadPredicate(200_000)) .expectComplete() .verify(getTimeout()); } @@ -255,7 +274,7 @@ default void largePayloadRequestChannel50() { .requestChannel(payloads) .doOnNext(Payload::release) .as(StepVerifier::create) - .expectNextCount(50) + .thenConsumeWhile(new PayloadPredicate(50)) .expectComplete() .verify(getTimeout()); } @@ -270,7 +289,7 @@ default void requestChannel20_000() { .doOnNext(this::assertChannelPayload) .doOnNext(Payload::release) .as(StepVerifier::create) - .expectNextCount(20_000) + .thenConsumeWhile(new PayloadPredicate(20_000)) .expectComplete() .verify(getTimeout()); } @@ -285,7 +304,7 @@ default void requestChannel2_000_000() { .doOnNext(Payload::release) .limitRate(8) .as(StepVerifier::create) - .expectNextCount(2_000_000) + .thenConsumeWhile(new PayloadPredicate(2_000_000)) .expectComplete() .verify(getTimeout()); } @@ -301,7 +320,7 @@ default void requestChannel3() { .requestChannel(payloads) .doOnNext(Payload::release) .as(publisher -> StepVerifier.create(publisher, 3)) - .expectNextCount(3) + .thenConsumeWhile(new PayloadPredicate(3)) .expectComplete() .verify(getTimeout()); @@ -322,9 +341,13 @@ default void requestChannel256() { }); final Scheduler scheduler = Schedulers.fromExecutorService(Executors.newFixedThreadPool(12)); - Flux.range(0, 1024) - .flatMap(v -> Mono.fromRunnable(() -> check(payloads)).subscribeOn(scheduler), 12) - .blockLast(); + try { + Flux.range(0, 1024) + .flatMap(v -> Mono.fromRunnable(() -> check(payloads)).subscribeOn(scheduler), 12) + .blockLast(); + } finally { + scheduler.disposeGracefully().block(); + } } default void check(Flux payloads) { @@ -333,7 +356,7 @@ default void check(Flux payloads) { .doOnNext(ReferenceCounted::release) .limitRate(8) .as(StepVerifier::create) - .expectNextCount(256) + .thenConsumeWhile(new PayloadPredicate(256)) .as("expected 256 items") .expectComplete() .verify(getTimeout()); @@ -465,6 +488,8 @@ class TransportPair implements Disposable { private static final String metadata = "metadata"; private final boolean withResumability; + private final boolean runClientWithAsyncInterceptors; + private final boolean runServerWithAsyncInterceptors; private final LeaksTrackingByteBufAllocator byteBufAllocator1 = LeaksTrackingByteBufAllocator.instrument( @@ -505,12 +530,15 @@ public TransportPair( BiFunction> serverTransportSupplier, boolean withRandomFragmentation, boolean withResumability) { + Schedulers.onHandleError((t, e) -> e.printStackTrace()); + Schedulers.resetFactory(); + this.withResumability = withResumability; T address = addressSupplier.get(); - final boolean runClientWithAsyncInterceptors = ThreadLocalRandom.current().nextBoolean(); - final boolean runServerWithAsyncInterceptors = ThreadLocalRandom.current().nextBoolean(); + this.runClientWithAsyncInterceptors = ThreadLocalRandom.current().nextBoolean(); + this.runServerWithAsyncInterceptors = ThreadLocalRandom.current().nextBoolean(); ByteBufAllocator allocatorToSupply1; ByteBufAllocator allocatorToSupply2; @@ -535,7 +563,7 @@ public TransportPair( registry .forConnection( (type, duplexConnection) -> - new AsyncDuplexConnection(duplexConnection)) + new AsyncDuplexConnection(duplexConnection, "server")) .forSocketAcceptor( delegate -> (connectionSetupPayload, sendingSocket) -> @@ -583,7 +611,7 @@ public TransportPair( registry .forConnection( (type, duplexConnection) -> - new AsyncDuplexConnection(duplexConnection)) + new AsyncDuplexConnection(duplexConnection, "client")) .forSocketAcceptor( delegate -> (connectionSetupPayload, sendingSocket) -> @@ -625,7 +653,7 @@ public TransportPair( @Override public void dispose() { - server.dispose(); + logger.info("terminating transport pair"); client.dispose(); } @@ -641,21 +669,46 @@ public String expectedPayloadMetadata() { return metadata; } - public void awaitClosed() { - server + public void awaitClosed(Duration timeout) { + logger.info("awaiting termination of transport pair"); + logger.info( + "wrappers combination: client{async=" + + runClientWithAsyncInterceptors + + "; resume=" + + withResumability + + "} server{async=" + + runServerWithAsyncInterceptors + + "; resume=" + + withResumability + + "}"); + client .onClose() - .onErrorResume(__ -> Mono.empty()) - .and(client.onClose().onErrorResume(__ -> Mono.empty())) - .block(Duration.ofMinutes(1)); + .doOnSubscribe(s -> logger.info("Client termination stage=onSubscribe(" + s + ")")) + .doOnEach(s -> logger.info("Client termination stage=" + s)) + .onErrorResume(t -> Mono.empty()) + .doOnTerminate(() -> logger.info("Client terminated. Terminating Server")) + .then(Mono.fromRunnable(server::dispose)) + .then( + server + .onClose() + .doOnSubscribe( + s -> logger.info("Server termination stage=onSubscribe(" + s + ")")) + .doOnEach(s -> logger.info("Server termination stage=" + s))) + .onErrorResume(t -> Mono.empty()) + .block(timeout); + + logger.info("TransportPair has been terminated"); } private static class AsyncDuplexConnection implements DuplexConnection { private final DuplexConnection duplexConnection; + private String tag; private final ByteBufReleaserOperator bufReleaserOperator; - public AsyncDuplexConnection(DuplexConnection duplexConnection) { + public AsyncDuplexConnection(DuplexConnection duplexConnection, String tag) { this.duplexConnection = duplexConnection; + this.tag = tag; this.bufReleaserOperator = new ByteBufReleaserOperator(); } @@ -673,9 +726,11 @@ public void sendErrorAndClose(RSocketErrorException e) { public Flux receive() { return duplexConnection .receive() + .doOnTerminate(() -> logger.info("[" + this + "] Receive is done before PO")) .subscribeOn(Schedulers.boundedElastic()) .doOnNext(ByteBuf::retain) .publishOn(Schedulers.boundedElastic(), Integer.MAX_VALUE) + .doOnTerminate(() -> logger.info("[" + this + "] Receive is done after PO")) .doOnDiscard(ReferenceCounted.class, ReferenceCountUtil::safeRelease) .transform( Operators.lift( @@ -697,13 +752,32 @@ public SocketAddress remoteAddress() { @Override public Mono onClose() { - return duplexConnection.onClose().and(bufReleaserOperator.onClose()); + return Mono.whenDelayError( + duplexConnection + .onClose() + .doOnTerminate(() -> logger.info("[" + this + "] Source Connection is done")), + bufReleaserOperator + .onClose() + .doOnTerminate(() -> logger.info("[" + this + "] BufferReleaser is done"))); } @Override public void dispose() { duplexConnection.dispose(); } + + @Override + public String toString() { + return "AsyncDuplexConnection{" + + "duplexConnection=" + + duplexConnection + + ", tag='" + + tag + + '\'' + + ", bufReleaserOperator=" + + bufReleaserOperator + + '}'; + } } private static class DisconnectingDuplexConnection implements DuplexConnection { @@ -727,7 +801,9 @@ public void dispose() { @Override public Mono onClose() { - return source.onClose(); + return source + .onClose() + .doOnTerminate(() -> logger.info("[" + this + "] Source Connection is done")); } @Override @@ -746,6 +822,8 @@ public void sendErrorAndClose(RSocketErrorException errorException) { public Flux receive() { return source .receive() + .doOnSubscribe( + __ -> logger.warn("Tag {}. Subscribing Connection[{}]", tag, source.hashCode())) .doOnNext( bb -> { if (!receivedFirst) { @@ -772,18 +850,31 @@ public ByteBufAllocator alloc() { public SocketAddress remoteAddress() { return source.remoteAddress(); } + + @Override + public String toString() { + return "DisconnectingDuplexConnection{" + + "tag='" + + tag + + '\'' + + ", source=" + + source + + ", disposables=" + + disposables + + '}'; + } } private static class ByteBufReleaserOperator implements CoreSubscriber, Subscription, Fuseable.QueueSubscription { CoreSubscriber actual; - final MonoProcessor closeableMono; + final Sinks.Empty closeableMonoSink; Subscription s; public ByteBufReleaserOperator() { - this.closeableMono = MonoProcessor.create(); + this.closeableMonoSink = Sinks.unsafe().empty(); } @Override @@ -804,19 +895,19 @@ public void onNext(ByteBuf buf) { } Mono onClose() { - return closeableMono; + return closeableMonoSink.asMono(); } @Override public void onError(Throwable t) { actual.onError(t); - closeableMono.onError(t); + closeableMonoSink.tryEmitError(t); } @Override public void onComplete() { actual.onComplete(); - closeableMono.onComplete(); + closeableMonoSink.tryEmitEmpty(); } @Override @@ -827,7 +918,7 @@ public void request(long n) { @Override public void cancel() { s.cancel(); - closeableMono.onComplete(); + closeableMonoSink.tryEmitEmpty(); } @Override @@ -854,6 +945,40 @@ public boolean isEmpty() { public void clear() { throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); } + + @Override + public String toString() { + return "ByteBufReleaserOperator{" + + "isActualPresent=" + + (actual != null) + + ", " + + "isSubscriptionPresent=" + + (s != null) + + '}'; + } + } + } + + class PayloadPredicate implements Predicate { + final int expectedCnt; + int cnt; + + public PayloadPredicate(int expectedCnt) { + this.expectedCnt = expectedCnt; + } + + @Override + public boolean test(Payload p) { + boolean shouldConsume = cnt++ < expectedCnt; + if (!shouldConsume) { + logger.info( + "Metadata: \n\r{}\n\rData:{}", + p.hasMetadata() + ? new ByteBufRepresentation().fallbackToStringOf(p.sliceMetadata()) + : "Empty", + new ByteBufRepresentation().fallbackToStringOf(p.sliceData())); + } + return shouldConsume; } } } diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java index 51c812cc3..28c1dacac 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 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,20 +19,31 @@ import io.rsocket.test.TransportTest; import java.time.Duration; import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; final class LocalResumableTransportTest implements TransportTest { - private final TransportPair transportPair = - new TransportPair<>( - () -> "test-" + UUID.randomUUID(), - (address, server, allocator) -> LocalClientTransport.create(address, allocator), - (address, allocator) -> LocalServerTransport.create(address), - false, - true); + private TransportPair transportPair; + + @BeforeEach + void createTestPair(TestInfo testInfo) { + transportPair = + new TransportPair<>( + () -> + "LocalResumableTransportTest-" + + testInfo.getDisplayName() + + "-" + + UUID.randomUUID(), + (address, server, allocator) -> LocalClientTransport.create(address, allocator), + (address, allocator) -> LocalServerTransport.create(address), + false, + true); + } @Override public Duration getTimeout() { - return Duration.ofSeconds(10); + return Duration.ofMinutes(1); } @Override diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableWithFragmentationTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableWithFragmentationTransportTest.java index 124cecec9..8ae16a0a5 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableWithFragmentationTransportTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableWithFragmentationTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 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,20 +19,31 @@ import io.rsocket.test.TransportTest; import java.time.Duration; import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; final class LocalResumableWithFragmentationTransportTest implements TransportTest { - private final TransportPair transportPair = - new TransportPair<>( - () -> "test-" + UUID.randomUUID(), - (address, server, allocator) -> LocalClientTransport.create(address, allocator), - (address, allocator) -> LocalServerTransport.create(address), - true, - true); + private TransportPair transportPair; + + @BeforeEach + void createTestPair(TestInfo testInfo) { + transportPair = + new TransportPair<>( + () -> + "LocalResumableWithFragmentationTransportTest-" + + testInfo.getDisplayName() + + "-" + + UUID.randomUUID(), + (address, server, allocator) -> LocalClientTransport.create(address, allocator), + (address, allocator) -> LocalServerTransport.create(address), + true, + true); + } @Override public Duration getTimeout() { - return Duration.ofSeconds(10); + return Duration.ofMinutes(1); } @Override diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportTest.java index e9c137255..87ad2105b 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 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,18 +19,25 @@ import io.rsocket.test.TransportTest; import java.time.Duration; import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; final class LocalTransportTest implements TransportTest { - private final TransportPair transportPair = - new TransportPair<>( - () -> "test-" + UUID.randomUUID(), - (address, server, allocator) -> LocalClientTransport.create(address, allocator), - (address, allocator) -> LocalServerTransport.create(address)); + private TransportPair transportPair; + + @BeforeEach + void createTestPair(TestInfo testInfo) { + transportPair = + new TransportPair<>( + () -> "LocalTransportTest-" + testInfo.getDisplayName() + "-" + UUID.randomUUID(), + (address, server, allocator) -> LocalClientTransport.create(address, allocator), + (address, allocator) -> LocalServerTransport.create(address)); + } @Override public Duration getTimeout() { - return Duration.ofSeconds(10); + return Duration.ofMinutes(1); } @Override diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportWithFragmentationTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportWithFragmentationTest.java index 4c2f47771..3ca5f5911 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportWithFragmentationTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportWithFragmentationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 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,19 +19,30 @@ import io.rsocket.test.TransportTest; import java.time.Duration; import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; final class LocalTransportWithFragmentationTest implements TransportTest { - private final TransportPair transportPair = - new TransportPair<>( - () -> "test-" + UUID.randomUUID(), - (address, server, allocator) -> LocalClientTransport.create(address, allocator), - (address, allocator) -> LocalServerTransport.create(address), - true); + private TransportPair transportPair; + + @BeforeEach + void createTestPair(TestInfo testInfo) { + transportPair = + new TransportPair<>( + () -> + "LocalTransportWithFragmentationTest-" + + testInfo.getDisplayName() + + "-" + + UUID.randomUUID(), + (address, server, allocator) -> LocalClientTransport.create(address, allocator), + (address, allocator) -> LocalServerTransport.create(address), + true); + } @Override public Duration getTimeout() { - return Duration.ofSeconds(10); + return Duration.ofMinutes(1); } @Override diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpFragmentationTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpFragmentationTransportTest.java index 299ea96c0..b17da654f 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpFragmentationTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpFragmentationTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 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,25 +22,31 @@ import io.rsocket.transport.netty.server.TcpServerTransport; import java.net.InetSocketAddress; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.netty.tcp.TcpClient; import reactor.netty.tcp.TcpServer; final class TcpFragmentationTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server, allocator) -> - TcpClientTransport.create( - TcpClient.create() - .remoteAddress(server::address) - .option(ChannelOption.ALLOCATOR, allocator)), - (address, allocator) -> - TcpServerTransport.create( + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + TcpClientTransport.create( + TcpClient.create() + .remoteAddress(server::address) + .option(ChannelOption.ALLOCATOR, allocator)), + (address, allocator) -> { + return TcpServerTransport.create( TcpServer.create() .bindAddress(() -> address) - .option(ChannelOption.ALLOCATOR, allocator)), - true); + .option(ChannelOption.ALLOCATOR, allocator)); + }, + true); + } @Override public Duration getTimeout() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableTransportTest.java index cf9e0540c..7be1c1c54 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 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,26 +22,32 @@ import io.rsocket.transport.netty.server.TcpServerTransport; import java.net.InetSocketAddress; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.netty.tcp.TcpClient; import reactor.netty.tcp.TcpServer; final class TcpResumableTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server, allocator) -> - TcpClientTransport.create( - TcpClient.create() - .remoteAddress(server::address) - .option(ChannelOption.ALLOCATOR, allocator)), - (address, allocator) -> - TcpServerTransport.create( + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + TcpClientTransport.create( + TcpClient.create() + .remoteAddress(server::address) + .option(ChannelOption.ALLOCATOR, allocator)), + (address, allocator) -> { + return TcpServerTransport.create( TcpServer.create() .bindAddress(() -> address) - .option(ChannelOption.ALLOCATOR, allocator)), - false, - true); + .option(ChannelOption.ALLOCATOR, allocator)); + }, + false, + true); + } @Override public Duration getTimeout() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableWithFragmentationTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableWithFragmentationTransportTest.java index 7d9d80542..39b3cec67 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableWithFragmentationTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableWithFragmentationTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 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,26 +22,32 @@ import io.rsocket.transport.netty.server.TcpServerTransport; import java.net.InetSocketAddress; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.netty.tcp.TcpClient; import reactor.netty.tcp.TcpServer; final class TcpResumableWithFragmentationTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server, allocator) -> - TcpClientTransport.create( - TcpClient.create() - .remoteAddress(server::address) - .option(ChannelOption.ALLOCATOR, allocator)), - (address, allocator) -> - TcpServerTransport.create( + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + TcpClientTransport.create( + TcpClient.create() + .remoteAddress(server::address) + .option(ChannelOption.ALLOCATOR, allocator)), + (address, allocator) -> { + return TcpServerTransport.create( TcpServer.create() .bindAddress(() -> address) - .option(ChannelOption.ALLOCATOR, allocator)), - true, - true); + .option(ChannelOption.ALLOCATOR, allocator)); + }, + true, + true); + } @Override public Duration getTimeout() { 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 85481924a..ee49b83cd 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 @@ -1,3 +1,19 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.rsocket.transport.netty; import io.netty.channel.ChannelOption; @@ -10,41 +26,47 @@ import java.net.InetSocketAddress; import java.security.cert.CertificateException; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; 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, allocator) -> - TcpClientTransport.create( - TcpClient.create() - .option(ChannelOption.ALLOCATOR, allocator) - .remoteAddress(server::address) - .secure( - ssl -> - ssl.sslContext( - SslContextBuilder.forClient() - .trustManager(InsecureTrustManagerFactory.INSTANCE)))), - (address, allocator) -> { - try { - SelfSignedCertificate ssc = new SelfSignedCertificate(); - TcpServer server = - TcpServer.create() - .option(ChannelOption.ALLOCATOR, allocator) - .bindAddress(() -> address) - .secure( - ssl -> - ssl.sslContext( - SslContextBuilder.forServer( - ssc.certificate(), ssc.privateKey()))); - return TcpServerTransport.create(server); - } catch (CertificateException e) { - throw Exceptions.propagate(e); - } - }); + private TransportPair transportPair; + + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> new InetSocketAddress("localhost", 0), + (address, server, allocator) -> + TcpClientTransport.create( + TcpClient.create() + .option(ChannelOption.ALLOCATOR, allocator) + .remoteAddress(server::address) + .secure( + ssl -> + ssl.sslContext( + SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE)))), + (address, allocator) -> { + try { + SelfSignedCertificate ssc = new SelfSignedCertificate(); + TcpServer server = + TcpServer.create() + .option(ChannelOption.ALLOCATOR, allocator) + .bindAddress(() -> 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() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpTransportTest.java index c474f9b0b..428681f3e 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 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,24 +22,30 @@ import io.rsocket.transport.netty.server.TcpServerTransport; import java.net.InetSocketAddress; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.netty.tcp.TcpClient; import reactor.netty.tcp.TcpServer; final class TcpTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server, allocator) -> - TcpClientTransport.create( - TcpClient.create() - .remoteAddress(server::address) - .option(ChannelOption.ALLOCATOR, allocator)), - (address, allocator) -> - TcpServerTransport.create( + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + TcpClientTransport.create( + TcpClient.create() + .remoteAddress(server::address) + .option(ChannelOption.ALLOCATOR, allocator)), + (address, allocator) -> { + return TcpServerTransport.create( TcpServer.create() .bindAddress(() -> address) - .option(ChannelOption.ALLOCATOR, allocator))); + .option(ChannelOption.ALLOCATOR, allocator)); + }); + } @Override public Duration getTimeout() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableTransportTest.java index 34dc99ae0..043f6bc64 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 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,29 +22,35 @@ import io.rsocket.transport.netty.server.WebsocketServerTransport; import java.net.InetSocketAddress; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.netty.http.client.HttpClient; import reactor.netty.http.server.HttpServer; final class WebsocketResumableTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server, allocator) -> - WebsocketClientTransport.create( - HttpClient.create() - .host(server.address().getHostName()) - .port(server.address().getPort()) - .option(ChannelOption.ALLOCATOR, allocator), - ""), - (address, allocator) -> - WebsocketServerTransport.create( + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + WebsocketClientTransport.create( + HttpClient.create() + .host(server.address().getHostName()) + .port(server.address().getPort()) + .option(ChannelOption.ALLOCATOR, allocator), + ""), + (address, allocator) -> { + return WebsocketServerTransport.create( HttpServer.create() .host(address.getHostName()) .port(address.getPort()) - .option(ChannelOption.ALLOCATOR, allocator)), - false, - true); + .option(ChannelOption.ALLOCATOR, allocator)); + }, + false, + true); + } @Override public Duration getTimeout() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableWithFragmentationTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableWithFragmentationTransportTest.java index 21c027e88..b1ca65fcc 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableWithFragmentationTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableWithFragmentationTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 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,29 +22,35 @@ import io.rsocket.transport.netty.server.WebsocketServerTransport; import java.net.InetSocketAddress; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.netty.http.client.HttpClient; import reactor.netty.http.server.HttpServer; final class WebsocketResumableWithFragmentationTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server, allocator) -> - WebsocketClientTransport.create( - HttpClient.create() - .host(server.address().getHostName()) - .port(server.address().getPort()) - .option(ChannelOption.ALLOCATOR, allocator), - ""), - (address, allocator) -> - WebsocketServerTransport.create( + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + WebsocketClientTransport.create( + HttpClient.create() + .host(server.address().getHostName()) + .port(server.address().getPort()) + .option(ChannelOption.ALLOCATOR, allocator), + ""), + (address, allocator) -> { + return WebsocketServerTransport.create( HttpServer.create() .host(address.getHostName()) .port(address.getPort()) - .option(ChannelOption.ALLOCATOR, allocator)), - true, - true); + .option(ChannelOption.ALLOCATOR, allocator)); + }, + true, + true); + } @Override public Duration getTimeout() { 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 9777c8bfa..81f7ffb95 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2023 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,45 +26,50 @@ import java.net.InetSocketAddress; import java.security.cert.CertificateException; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.core.Exceptions; import reactor.netty.http.client.HttpClient; import reactor.netty.http.server.HttpServer; final class WebsocketSecureTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> new InetSocketAddress("localhost", 0), - (address, server, allocator) -> - WebsocketClientTransport.create( - HttpClient.create() - .option(ChannelOption.ALLOCATOR, allocator) - .remoteAddress(server::address) - .secure( - ssl -> - ssl.sslContext( - SslContextBuilder.forClient() - .trustManager(InsecureTrustManagerFactory.INSTANCE))), - String.format( - "https://%s:%d/", - server.address().getHostName(), server.address().getPort())), - (address, allocator) -> { - try { - SelfSignedCertificate ssc = new SelfSignedCertificate(); - HttpServer server = - HttpServer.create() - .option(ChannelOption.ALLOCATOR, allocator) - .bindAddress(() -> address) - .secure( - ssl -> - ssl.sslContext( - SslContextBuilder.forServer( - ssc.certificate(), ssc.privateKey()))); - return WebsocketServerTransport.create(server); - } catch (CertificateException e) { - throw Exceptions.propagate(e); - } - }); + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> new InetSocketAddress("localhost", 0), + (address, server, allocator) -> + WebsocketClientTransport.create( + HttpClient.create() + .option(ChannelOption.ALLOCATOR, allocator) + .remoteAddress(server::address) + .secure( + ssl -> + ssl.sslContext( + SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE))), + String.format( + "https://%s:%d/", + server.address().getHostName(), server.address().getPort())), + (address, allocator) -> { + try { + SelfSignedCertificate ssc = new SelfSignedCertificate(); + HttpServer server = + HttpServer.create() + .option(ChannelOption.ALLOCATOR, allocator) + .bindAddress(() -> address) + .secure( + ssl -> + ssl.sslContext( + SslContextBuilder.forServer( + ssc.certificate(), ssc.privateKey()))); + return WebsocketServerTransport.create(server); + } catch (CertificateException e) { + throw Exceptions.propagate(e); + } + }); + } @Override public Duration getTimeout() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketTransportTest.java index 93d7bdb2f..cdd507456 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 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,27 +22,33 @@ import io.rsocket.transport.netty.server.WebsocketServerTransport; import java.net.InetSocketAddress; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.netty.http.client.HttpClient; import reactor.netty.http.server.HttpServer; final class WebsocketTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server, allocator) -> - WebsocketClientTransport.create( - HttpClient.create() - .host(server.address().getHostName()) - .port(server.address().getPort()) - .option(ChannelOption.ALLOCATOR, allocator), - ""), - (address, allocator) -> - WebsocketServerTransport.create( + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + WebsocketClientTransport.create( + HttpClient.create() + .host(server.address().getHostName()) + .port(server.address().getPort()) + .option(ChannelOption.ALLOCATOR, allocator), + ""), + (address, allocator) -> { + return WebsocketServerTransport.create( HttpServer.create() .host(address.getHostName()) .port(address.getPort()) - .option(ChannelOption.ALLOCATOR, allocator))); + .option(ChannelOption.ALLOCATOR, allocator)); + }); + } @Override public Duration getTimeout() { From 89593852bdb02cb766ee37ef7f99544fb44eb812 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 6 Apr 2023 10:07:00 +0300 Subject: [PATCH 099/116] ensures resumable connection awaits termination of all component Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- .../java/io/rsocket/core/ClientSetup.java | 23 +++- .../java/io/rsocket/core/ServerSetup.java | 1 + .../rsocket/resume/ClientRSocketSession.java | 116 ++++++++++------ .../resume/InMemoryResumableFramesStore.java | 20 ++- .../resume/ResumableDuplexConnection.java | 129 ++++++++++-------- .../rsocket/resume/ServerRSocketSession.java | 9 +- .../resume/ClientRSocketSessionTest.java | 2 +- .../resume/ServerRSocketSessionTest.java | 2 +- 8 files changed, 190 insertions(+), 112 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/ClientSetup.java b/rsocket-core/src/main/java/io/rsocket/core/ClientSetup.java index 725201fe7..3477b8d6d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ClientSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ClientSetup.java @@ -4,6 +4,7 @@ import io.netty.buffer.Unpooled; import io.rsocket.DuplexConnection; import java.nio.channels.ClosedChannelException; +import reactor.core.Disposable; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; @@ -25,8 +26,24 @@ class ResumableClientSetup extends ClientSetup { @Override Mono> init(DuplexConnection connection) { - return Mono.>create( - sink -> sink.onRequest(__ -> new SetupHandlingDuplexConnection(connection, sink))) - .or(connection.onClose().then(Mono.error(ClosedChannelException::new))); + return Mono.create( + sink -> { + sink.onRequest( + __ -> { + new SetupHandlingDuplexConnection(connection, sink); + }); + + Disposable subscribe = + connection + .onClose() + .doFinally(__ -> sink.error(new ClosedChannelException())) + .subscribe(); + sink.onCancel( + () -> { + subscribe.dispose(); + connection.dispose(); + connection.receive().subscribe(); + }); + }); } } 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 2d367bd73..ddad96047 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java @@ -60,6 +60,7 @@ void dispose() {} void sendError(DuplexConnection duplexConnection, RSocketErrorException exception) { duplexConnection.sendErrorAndClose(exception); + duplexConnection.receive().subscribe(); } static class DefaultServerSetup extends ServerSetup { 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 2f2f29001..d6a0c9292 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -29,7 +29,6 @@ import io.rsocket.frame.ResumeOkFrameCodec; import io.rsocket.keepalive.KeepAliveSupport; import java.time.Duration; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Function; import org.reactivestreams.Subscription; @@ -79,31 +78,49 @@ public ClientRSocketSession( this.resumeToken = resumeToken; this.session = resumeToken.toString(CharsetUtil.UTF_8); this.connectionFactory = - connectionFactory.flatMap( - dc -> { - final long impliedPosition = resumableFramesStore.frameImpliedPosition(); - final long position = resumableFramesStore.framePosition(); - dc.sendFrame( - 0, - ResumeFrameCodec.encode( - dc.alloc(), - resumeToken.retain(), - // server uses this to release its cache - impliedPosition, // observed on the client side - // server uses this to check whether there is no mismatch - position // sent from the client sent - )); - - if (logger.isDebugEnabled()) { - logger.debug( - "Side[client]|Session[{}]. ResumeFrame[impliedPosition[{}], position[{}]] has been sent.", - session, - impliedPosition, - position); - } - - return connectionTransformer.apply(dc); - }); + connectionFactory + .doOnDiscard( + DuplexConnection.class, + c -> { + final ConnectionErrorException connectionErrorException = + new ConnectionErrorException("resumption_server=[Session Expired]"); + c.sendErrorAndClose(connectionErrorException); + c.receive().subscribe(); + }) + .flatMap( + dc -> { + final long impliedPosition = resumableFramesStore.frameImpliedPosition(); + final long position = resumableFramesStore.framePosition(); + dc.sendFrame( + 0, + ResumeFrameCodec.encode( + dc.alloc(), + resumeToken.retain(), + // server uses this to release its cache + impliedPosition, // observed on the client side + // server uses this to check whether there is no mismatch + position // sent from the client sent + )); + + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. ResumeFrame[impliedPosition[{}], position[{}]] has been sent.", + session, + impliedPosition, + position); + } + + return connectionTransformer + .apply(dc) + .doOnDiscard( + Tuple2.class, + tuple2 -> { + if (logger.isDebugEnabled()) { + logger.debug("try to reestablish from discard"); + } + tryReestablishSession(tuple2); + }); + }); this.resumableFramesStore = resumableFramesStore; this.allocator = resumableDuplexConnection.alloc(); this.resumeSessionDuration = resumeSessionDuration; @@ -160,11 +177,20 @@ public void onImpliedPosition(long remoteImpliedPos) { @Override public void dispose() { - Operators.terminate(S, this); + if (logger.isDebugEnabled()) { + logger.debug("Side[client]|Session[{}]. Disposing", session); + } + + boolean result = Operators.terminate(S, this); + + if (logger.isDebugEnabled()) { + logger.debug("Side[client]|Session[{}]. Sessions[isDisposed={}]", session, result); + } reconnectDisposable.dispose(); resumableConnection.dispose(); - resumableFramesStore.dispose(); + // frame store is disposed by resumable connection + // resumableFramesStore.dispose(); if (resumeToken.refCnt() > 0) { resumeToken.release(); @@ -177,6 +203,9 @@ public boolean isDisposed() { } void tryReestablishSession(Tuple2 tuple2) { + if (logger.isDebugEnabled()) { + logger.debug("Active subscription is canceled {}", s == Operators.cancelledSubscription()); + } ByteBuf shouldBeResumeOKFrame = tuple2.getT1(); DuplexConnection nextDuplexConnection = tuple2.getT2(); @@ -189,9 +218,9 @@ void tryReestablishSession(Tuple2 tuple2) { } final ConnectionErrorException connectionErrorException = new ConnectionErrorException("RESUME_OK frame must be received before any others"); - resumableConnection.dispose(connectionErrorException); + resumableConnection.dispose(nextDuplexConnection, connectionErrorException); nextDuplexConnection.sendErrorAndClose(connectionErrorException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); throw connectionErrorException; // throw to retry connection again } @@ -227,10 +256,10 @@ void tryReestablishSession(Tuple2 tuple2) { } final ConnectionErrorException t = new ConnectionErrorException(e.getMessage(), e); - resumableConnection.dispose(t); + resumableConnection.dispose(nextDuplexConnection, t); nextDuplexConnection.sendErrorAndClose(t); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); return; } @@ -244,7 +273,7 @@ void tryReestablishSession(Tuple2 tuple2) { final ConnectionErrorException connectionErrorException = new ConnectionErrorException("resumption_server=[Session Expired]"); nextDuplexConnection.sendErrorAndClose(connectionErrorException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); return; } @@ -263,7 +292,7 @@ void tryReestablishSession(Tuple2 tuple2) { final ConnectionErrorException connectionErrorException = new ConnectionErrorException("resumption_server_pos=[Session Expired]"); nextDuplexConnection.sendErrorAndClose(connectionErrorException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); // no need to do anything since connection resumable connection is liklly to // be disposed } @@ -278,10 +307,10 @@ void tryReestablishSession(Tuple2 tuple2) { final ConnectionErrorException connectionErrorException = new ConnectionErrorException("resumption_server_pos=[" + remoteImpliedPos + "]"); - resumableConnection.dispose(connectionErrorException); + resumableConnection.dispose(nextDuplexConnection, connectionErrorException); nextDuplexConnection.sendErrorAndClose(connectionErrorException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); } } else if (frameType == FrameType.ERROR) { final RuntimeException exception = Exceptions.from(0, shouldBeResumeOKFrame); @@ -292,13 +321,14 @@ void tryReestablishSession(Tuple2 tuple2) { exception); } if (exception instanceof RejectedResumeException) { - resumableConnection.dispose(exception); + resumableConnection.dispose(nextDuplexConnection, exception); nextDuplexConnection.dispose(); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); return; } nextDuplexConnection.dispose(); + nextDuplexConnection.receive().subscribe(); throw exception; // assume retryable exception } else { if (logger.isDebugEnabled()) { @@ -309,10 +339,10 @@ void tryReestablishSession(Tuple2 tuple2) { final ConnectionErrorException connectionErrorException = new ConnectionErrorException("RESUME_OK frame must be received before any others"); - resumableConnection.dispose(connectionErrorException); + resumableConnection.dispose(nextDuplexConnection, connectionErrorException); nextDuplexConnection.sendErrorAndClose(connectionErrorException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); // no need to do anything since remote server rejected our connection completely } @@ -349,11 +379,7 @@ public void onError(Throwable t) { Operators.onErrorDropped(t, currentContext()); } - if (t instanceof TimeoutException) { - resumableConnection.dispose(); - } else { - resumableConnection.dispose(t); - } + resumableConnection.dispose(); } @Override 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 87d82048d..b71693f0d 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java @@ -168,9 +168,9 @@ void drain(long expectedState) { if (isConnected(expectedState)) { if (isTerminated(expectedState)) { - handleTerminal(this.terminal); + handleTerminated(qs, this.terminal); } else if (isDisposed()) { - handleTerminal(new CancellationException("Disposed")); + handleDisposed(); } else if (hasFrames(expectedState)) { handlePendingFrames(qs); } @@ -402,7 +402,17 @@ void handleFrame(ByteBuf frame) { handleConnectionFrame(frame); } - void handleTerminal(@Nullable Throwable t) { + void handleTerminated(Fuseable.QueueSubscription qs, @Nullable Throwable t) { + for (; ; ) { + final ByteBuf frame = qs.poll(); + final boolean empty = frame == null; + + if (empty) { + break; + } + + handleFrame(frame); + } if (t != null) { this.actual.onError(t); } else { @@ -410,6 +420,10 @@ void handleTerminal(@Nullable Throwable t) { } } + void handleDisposed() { + this.actual.onError(new CancellationException("Disposed")); + } + void handleConnectionFrame(ByteBuf frame) { this.actual.onNext(frame); } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java index f061857ff..9704d9aba 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -21,8 +21,8 @@ import io.netty.util.CharsetUtil; import io.rsocket.DuplexConnection; import io.rsocket.RSocketErrorException; -import io.rsocket.exceptions.ConnectionCloseException; import io.rsocket.exceptions.ConnectionErrorException; +import io.rsocket.frame.ErrorFrameCodec; import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.internal.UnboundedProcessor; import java.net.SocketAddress; @@ -50,8 +50,8 @@ public class ResumableDuplexConnection extends Flux final ResumableFramesStore resumableFramesStore; final UnboundedProcessor savableFramesSender; - final Disposable framesSaverDisposable; - final Sinks.Empty onClose; + final Sinks.Empty onQueueClose; + final Sinks.Empty onLastConnectionClose; final SocketAddress remoteAddress; final Sinks.Many onConnectionClosedSink; @@ -79,11 +79,13 @@ public ResumableDuplexConnection( this.session = session.toString(CharsetUtil.UTF_8); this.onConnectionClosedSink = Sinks.unsafe().many().unicast().onBackpressureBuffer(); this.resumableFramesStore = resumableFramesStore; - this.savableFramesSender = new UnboundedProcessor(); - this.framesSaverDisposable = resumableFramesStore.saveFrames(savableFramesSender).subscribe(); - this.onClose = Sinks.empty(); + this.onQueueClose = Sinks.unsafe().empty(); + this.onLastConnectionClose = Sinks.unsafe().empty(); + this.savableFramesSender = new UnboundedProcessor(onQueueClose::tryEmitEmpty); this.remoteAddress = initialConnection.remoteAddress(); + resumableFramesStore.saveFrames(savableFramesSender).subscribe(); + ACTIVE_CONNECTION.lazySet(this, initialConnection); } @@ -92,7 +94,10 @@ public boolean connect(DuplexConnection nextConnection) { if (activeConnection != DisposedConnection.INSTANCE && ACTIVE_CONNECTION.compareAndSet(this, activeConnection, nextConnection)) { - activeConnection.dispose(); + if (!activeConnection.isDisposed()) { + activeConnection.sendErrorAndClose( + new ConnectionErrorException("Connection unexpectedly replaced")); + } initConnection(nextConnection); @@ -120,10 +125,16 @@ void initConnection(DuplexConnection nextConnection) { .resumeStream() .subscribe( f -> nextConnection.sendFrame(FrameHeaderCodec.streamId(f), f), - t -> sendErrorAndClose(new ConnectionErrorException(t.getMessage())), - () -> - sendErrorAndClose( - new ConnectionCloseException("Connection Closed Unexpectedly"))); + t -> { + dispose(nextConnection, t); + nextConnection.sendErrorAndClose(new ConnectionErrorException(t.getMessage(), t)); + }, + () -> { + final ConnectionErrorException e = + new ConnectionErrorException("Connection Closed Unexpectedly"); + dispose(nextConnection, e); + nextConnection.sendErrorAndClose(e); + }); nextConnection.receive().subscribe(frameReceivingSubscriber); nextConnection .onClose() @@ -153,7 +164,7 @@ void initConnection(DuplexConnection nextConnection) { public void disconnect() { final DuplexConnection activeConnection = this.activeConnection; - if (activeConnection != DisposedConnection.INSTANCE) { + if (activeConnection != DisposedConnection.INSTANCE && !activeConnection.isDisposed()) { activeConnection.dispose(); } } @@ -161,9 +172,9 @@ public void disconnect() { @Override public void sendFrame(int streamId, ByteBuf frame) { if (streamId == 0) { - savableFramesSender.onNextPrioritized(frame); + savableFramesSender.tryEmitPrioritized(frame); } else { - savableFramesSender.onNext(frame); + savableFramesSender.tryEmitNormal(frame); } } @@ -184,32 +195,25 @@ public void sendErrorAndClose(RSocketErrorException rSocketErrorException) { return; } - activeConnection.sendErrorAndClose(rSocketErrorException); + savableFramesSender.tryEmitFinal( + ErrorFrameCodec.encode(activeConnection.alloc(), 0, rSocketErrorException)); + activeConnection .onClose() .subscribe( null, t -> { - framesSaverDisposable.dispose(); - activeReceivingSubscriber.dispose(); - savableFramesSender.onComplete(); - savableFramesSender.cancel(); onConnectionClosedSink.tryEmitComplete(); - - onClose.tryEmitError(t); + onLastConnectionClose.tryEmitEmpty(); }, () -> { - framesSaverDisposable.dispose(); - activeReceivingSubscriber.dispose(); - savableFramesSender.onComplete(); - savableFramesSender.cancel(); onConnectionClosedSink.tryEmitComplete(); final Throwable cause = rSocketErrorException.getCause(); if (cause == null) { - onClose.tryEmitEmpty(); + onLastConnectionClose.tryEmitEmpty(); } else { - onClose.tryEmitError(cause); + onLastConnectionClose.tryEmitError(cause); } }); } @@ -226,50 +230,66 @@ public ByteBufAllocator alloc() { @Override public Mono onClose() { - return onClose.asMono(); + return Mono.whenDelayError( + onQueueClose.asMono(), resumableFramesStore.onClose(), onLastConnectionClose.asMono()); } @Override public void dispose() { - dispose(null); - } - - void dispose(@Nullable Throwable e) { final DuplexConnection activeConnection = ACTIVE_CONNECTION.getAndSet(this, DisposedConnection.INSTANCE); if (activeConnection == DisposedConnection.INSTANCE) { return; } - - if (activeConnection != null) { - activeConnection.dispose(); - } - - if (logger.isDebugEnabled()) { - logger.debug( - "Side[{}]|Session[{}]|DuplexConnection[{}]. Disposing...", - side, - session, - connectionIndex); - } - - framesSaverDisposable.dispose(); - activeReceivingSubscriber.dispose(); savableFramesSender.onComplete(); - savableFramesSender.cancel(); - onConnectionClosedSink.tryEmitComplete(); + activeConnection + .onClose() + .subscribe( + null, + t -> { + onConnectionClosedSink.tryEmitComplete(); + onLastConnectionClose.tryEmitEmpty(); + }, + () -> { + onConnectionClosedSink.tryEmitComplete(); + onLastConnectionClose.tryEmitEmpty(); + }); + } - if (e != null) { - onClose.tryEmitError(e); - } else { - onClose.tryEmitEmpty(); + void dispose(DuplexConnection nextConnection, @Nullable Throwable e) { + final DuplexConnection activeConnection = + ACTIVE_CONNECTION.getAndSet(this, DisposedConnection.INSTANCE); + if (activeConnection == DisposedConnection.INSTANCE) { + return; } + savableFramesSender.onComplete(); + nextConnection + .onClose() + .subscribe( + null, + t -> { + if (e != null) { + onLastConnectionClose.tryEmitError(e); + } else { + onLastConnectionClose.tryEmitEmpty(); + } + onConnectionClosedSink.tryEmitComplete(); + }, + () -> { + if (e != null) { + onLastConnectionClose.tryEmitError(e); + } else { + onLastConnectionClose.tryEmitEmpty(); + } + onConnectionClosedSink.tryEmitComplete(); + }); } @Override @SuppressWarnings("ConstantConditions") public boolean isDisposed() { - return onClose.scan(Scannable.Attr.TERMINATED) || onClose.scan(Scannable.Attr.CANCELLED); + return onQueueClose.scan(Scannable.Attr.TERMINATED) + || onQueueClose.scan(Scannable.Attr.CANCELLED); } @Override @@ -280,6 +300,7 @@ public SocketAddress remoteAddress() { @Override public void request(long n) { if (state == 1 && STATE.compareAndSet(this, 1, 2)) { + // happens for the very first time with the initial connection initConnection(this.activeConnection); } } 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 83c5bf8c1..c4dc4d837 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -138,7 +138,7 @@ void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplex final RejectedResumeException rejectedResumeException = new RejectedResumeException("resume_internal_error: Session Expired"); nextDuplexConnection.sendErrorAndClose(rejectedResumeException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); return; } @@ -180,7 +180,7 @@ void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplex final RejectedResumeException rejectedResumeException = new RejectedResumeException(t.getMessage(), t); nextDuplexConnection.sendErrorAndClose(rejectedResumeException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); return; } @@ -200,7 +200,7 @@ void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplex final RejectedResumeException rejectedResumeException = new RejectedResumeException("resume_internal_error: Session Expired"); nextDuplexConnection.sendErrorAndClose(rejectedResumeException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); // resumableConnection is likely to be disposed at this stage. Thus we have // nothing to do @@ -224,7 +224,7 @@ void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplex "resumption_pos=[ remote: { pos: %d, impliedPos: %d }, local: { pos: %d, impliedPos: %d }]", remotePos, remoteImpliedPos, position, impliedPosition)); nextDuplexConnection.sendErrorAndClose(rejectedResumeException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); } } @@ -289,7 +289,6 @@ public void setKeepAliveSupport(KeepAliveSupport keepAliveSupport) { public void dispose() { Operators.terminate(S, this); resumableConnection.dispose(); - resumableFramesStore.dispose(); } @Override diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java index f34bb5d64..bdd46f8c6 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java @@ -457,7 +457,7 @@ void shouldTerminateConnectionOnIllegalStateInKeepAliveFrame() { .typeOf(FrameType.ERROR) .matches(ReferenceCounted::release); - resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); + resumableDuplexConnection.onClose().as(StepVerifier::create).expectError().verify(); transport.alloc().assertHasNoLeaks(); } finally { diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java index a3a682d94..eff65f587 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java @@ -175,7 +175,7 @@ void shouldTerminateConnectionOnIllegalStateInKeepAliveFrame() { .typeOf(FrameType.ERROR) .matches(ReferenceCounted::release); - resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); + resumableDuplexConnection.onClose().as(StepVerifier::create).expectError().verify(); transport.alloc().assertHasNoLeaks(); } From 5da37cde06ec9ed66dbf3c23214207be5eb88acb Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 11 Apr 2023 14:46:29 +0300 Subject: [PATCH 100/116] ensures `onClose` awaits all underlying components to be closed (#1085) --- ...nMemoryResumableFramesStoreStressTest.java | 118 ++++++++++++++++++ .../core/ClientServerInputMultiplexer.java | 46 ++++++- .../io/rsocket/core/RSocketConnector.java | 18 ++- .../io/rsocket/core/RSocketRequester.java | 97 ++++++++++++-- .../io/rsocket/core/RSocketResponder.java | 24 +++- .../java/io/rsocket/core/RSocketServer.java | 12 +- .../java/io/rsocket/core/ServerSetup.java | 4 +- .../core/SetupHandlingDuplexConnection.java | 5 + .../rsocket/internal/UnboundedProcessor.java | 9 +- .../rsocket/keepalive/KeepAliveHandler.java | 8 -- .../rsocket/resume/ClientRSocketSession.java | 14 +-- .../resume/InMemoryResumableFramesStore.java | 15 +-- .../resume/ResumableDuplexConnection.java | 20 +++ .../core/DefaultRSocketClientTests.java | 7 +- .../io/rsocket/core/RSocketLeaseTest.java | 11 +- .../core/RSocketRequesterSubscribersTest.java | 9 +- .../io/rsocket/core/RSocketRequesterTest.java | 10 +- .../io/rsocket/core/RSocketResponderTest.java | 5 +- .../java/io/rsocket/core/RSocketTest.java | 12 +- .../io/rsocket/core/SetupRejectionTest.java | 11 +- rsocket-examples/build.gradle | 2 +- .../transport/local/LocalClientTransport.java | 6 +- .../local/LocalDuplexConnection.java | 9 +- .../transport/netty/TcpDuplexConnection.java | 24 ++-- .../netty/WebsocketDuplexConnection.java | 25 +++- .../netty/client/TcpClientTransport.java | 2 +- .../client/WebsocketClientTransport.java | 2 +- .../netty/server/TcpServerTransport.java | 2 +- .../netty/server/WebsocketRouteTransport.java | 4 +- .../server/WebsocketServerTransport.java | 2 +- 30 files changed, 445 insertions(+), 88 deletions(-) create mode 100644 rsocket-core/src/jcstress/java/io/rsocket/resume/InMemoryResumableFramesStoreStressTest.java diff --git a/rsocket-core/src/jcstress/java/io/rsocket/resume/InMemoryResumableFramesStoreStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/resume/InMemoryResumableFramesStoreStressTest.java new file mode 100644 index 000000000..f0b209552 --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/resume/InMemoryResumableFramesStoreStressTest.java @@ -0,0 +1,118 @@ +package io.rsocket.resume; + +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.rsocket.exceptions.ConnectionErrorException; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.internal.UnboundedProcessor; +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.LL_Result; +import reactor.core.Disposable; + +public class InMemoryResumableFramesStoreStressTest { + boolean storeClosed; + + InMemoryResumableFramesStore store = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 128); + boolean processorClosed; + UnboundedProcessor processor = new UnboundedProcessor(() -> processorClosed = true); + + void subscribe() { + store.saveFrames(processor).subscribe(); + store.onClose().subscribe(null, t -> storeClosed = true, () -> storeClosed = true); + } + + @JCStressTest + @Outcome( + id = {"true, true"}, + expect = ACCEPTABLE) + @State + public static class TwoSubscribesRaceStressTest extends InMemoryResumableFramesStoreStressTest { + + Disposable d1; + + final ByteBuf b1 = + PayloadFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + true, + false, + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello1"), + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello2")); + final ByteBuf b2 = + PayloadFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 3, + false, + true, + false, + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello3"), + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello4")); + final ByteBuf b3 = + PayloadFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 5, + false, + true, + false, + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello5"), + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello6")); + + final ByteBuf c1 = + ErrorFrameCodec.encode(ByteBufAllocator.DEFAULT, 0, new ConnectionErrorException("closed")); + + { + subscribe(); + d1 = store.doOnDiscard(ByteBuf.class, ByteBuf::release).subscribe(ByteBuf::release, t -> {}); + } + + @Actor + public void producer1() { + processor.tryEmitNormal(b1); + processor.tryEmitNormal(b2); + processor.tryEmitNormal(b3); + } + + @Actor + public void producer2() { + processor.tryEmitFinal(c1); + } + + @Actor + public void producer3() { + d1.dispose(); + store + .doOnDiscard(ByteBuf.class, ByteBuf::release) + .subscribe(ByteBuf::release, t -> {}) + .dispose(); + store + .doOnDiscard(ByteBuf.class, ByteBuf::release) + .subscribe(ByteBuf::release, t -> {}) + .dispose(); + store.doOnDiscard(ByteBuf.class, ByteBuf::release).subscribe(ByteBuf::release, t -> {}); + } + + @Actor + public void producer4() { + store.releaseFrames(0); + store.releaseFrames(0); + store.releaseFrames(0); + } + + @Arbiter + public void arbiter(LL_Result r) { + r.r1 = storeClosed; + r.r2 = processorClosed; + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java index d6cb46d98..e19d31924 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java @@ -67,8 +67,8 @@ public ClientServerInputMultiplexer( this.source = source; this.isClient = isClient; - this.serverReceiver = new InternalDuplexConnection(this, source); - this.clientReceiver = new InternalDuplexConnection(this, source); + this.serverReceiver = new InternalDuplexConnection(Type.SERVER, this, source); + this.clientReceiver = new InternalDuplexConnection(Type.CLIENT, this, source); this.serverConnection = registry.initConnection(Type.SERVER, serverReceiver); this.clientConnection = registry.initConnection(Type.CLIENT, clientReceiver); } @@ -195,8 +195,33 @@ int incrementAndGetCheckingState() { } } + @Override + public String toString() { + return "ClientServerInputMultiplexer{" + + "serverReceiver=" + + serverReceiver + + ", clientReceiver=" + + clientReceiver + + ", serverConnection=" + + serverConnection + + ", clientConnection=" + + clientConnection + + ", source=" + + source + + ", isClient=" + + isClient + + ", s=" + + s + + ", t=" + + t + + ", state=" + + state + + '}'; + } + private static class InternalDuplexConnection extends Flux implements Subscription, DuplexConnection { + private final Type type; private final ClientServerInputMultiplexer clientServerInputMultiplexer; private final DuplexConnection source; @@ -207,7 +232,10 @@ private static class InternalDuplexConnection extends Flux CoreSubscriber actual; public InternalDuplexConnection( - ClientServerInputMultiplexer clientServerInputMultiplexer, DuplexConnection source) { + Type type, + ClientServerInputMultiplexer clientServerInputMultiplexer, + DuplexConnection source) { + this.type = type; this.clientServerInputMultiplexer = clientServerInputMultiplexer; this.source = source; } @@ -304,5 +332,17 @@ public Mono onClose() { public double availability() { return source.availability(); } + + @Override + public String toString() { + return "InternalDuplexConnection{" + + "type=" + + type + + ", source=" + + source + + ", state=" + + state + + '}'; + } } } 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 432c0f0f5..de494c4e3 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -47,6 +47,7 @@ import java.util.function.Supplier; import reactor.core.Disposable; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; import reactor.util.annotation.Nullable; import reactor.util.function.Tuples; import reactor.util.retry.Retry; @@ -633,8 +634,7 @@ public Mono connect(Supplier transportSupplier) { wrappedConnection = resumableDuplexConnection; } else { keepAliveHandler = - new KeepAliveHandler.DefaultKeepAliveHandler( - clientServerConnection); + new KeepAliveHandler.DefaultKeepAliveHandler(); wrappedConnection = clientServerConnection; } @@ -655,6 +655,11 @@ public Mono connect(Supplier transportSupplier) { requesterLeaseTracker = null; } + final Sinks.Empty requesterOnAllClosedSink = + Sinks.unsafe().empty(); + final Sinks.Empty responderOnAllClosedSink = + Sinks.unsafe().empty(); + RSocket rSocketRequester = new RSocketRequester( multiplexer.asClientConnection(), @@ -667,7 +672,11 @@ public Mono connect(Supplier transportSupplier) { (int) keepAliveMaxLifeTime.toMillis(), keepAliveHandler, interceptors::initRequesterRequestInterceptor, - requesterLeaseTracker); + requesterLeaseTracker, + requesterOnAllClosedSink, + Mono.whenDelayError( + responderOnAllClosedSink.asMono(), + requesterOnAllClosedSink.asMono())); RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); @@ -715,7 +724,8 @@ public Mono connect(Supplier transportSupplier) { (RequestInterceptor) leases.sender) : interceptors - ::initResponderRequestInterceptor); + ::initResponderRequestInterceptor, + responderOnAllClosedSink); return wrappedRSocketRequester; }) 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 bf298706a..9e8d349bf 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -66,8 +66,10 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { RSocketRequester.class, Throwable.class, "terminationError"); @Nullable private final RequesterLeaseTracker requesterLeaseTracker; + + private final Sinks.Empty onThisSideClosedSink; + private final Mono onAllClosed; private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; - private final Sinks.Empty onClose; RSocketRequester( DuplexConnection connection, @@ -80,7 +82,9 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { int keepAliveAckTimeout, @Nullable KeepAliveHandler keepAliveHandler, Function requestInterceptorFunction, - @Nullable RequesterLeaseTracker requesterLeaseTracker) { + @Nullable RequesterLeaseTracker requesterLeaseTracker, + Sinks.Empty onThisSideClosedSink, + Mono onAllClosed) { super( mtu, maxFrameLength, @@ -91,10 +95,11 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { requestInterceptorFunction); this.requesterLeaseTracker = requesterLeaseTracker; - this.onClose = Sinks.empty(); + this.onThisSideClosedSink = onThisSideClosedSink; + this.onAllClosed = onAllClosed; // DO NOT Change the order here. The Send processor must be subscribed to before receiving - connection.onClose().subscribe(null, this::tryTerminateOnConnectionError, this::tryShutdown); + connection.onClose().subscribe(null, this::tryShutdown, this::tryShutdown); connection.receive().subscribe(this::handleIncomingFrames, e -> {}); @@ -188,7 +193,11 @@ public double availability() { @Override public void dispose() { - tryShutdown(); + if (terminationError != null) { + return; + } + + getDuplexConnection().sendErrorAndClose(new ConnectionErrorException("Disposed")); } @Override @@ -198,7 +207,7 @@ public boolean isDisposed() { @Override public Mono onClose() { - return onClose.asMono(); + return onAllClosed; } private void handleIncomingFrames(ByteBuf frame) { @@ -305,8 +314,31 @@ private void tryTerminateOnKeepAlive(KeepAliveSupport.KeepAlive keepAlive) { String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()))); } - private void tryTerminateOnConnectionError(Throwable e) { - tryTerminate(() -> e); + private void tryShutdown(Throwable e) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("trying to close requester " + getDuplexConnection()); + } + if (terminationError == null) { + if (TERMINATION_ERROR.compareAndSet(this, null, e)) { + terminate(CLOSED_CHANNEL_EXCEPTION); + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); + } + } + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.info( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); + } + } } private void tryTerminateOnZeroError(ByteBuf errorFrame) { @@ -314,27 +346,67 @@ private void tryTerminateOnZeroError(ByteBuf errorFrame) { } private void tryTerminate(Supplier errorSupplier) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("trying to close requester " + getDuplexConnection()); + } if (terminationError == null) { Throwable e = errorSupplier.get(); if (TERMINATION_ERROR.compareAndSet(this, null, e)) { terminate(e); + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); + } + } + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); } } } private void tryShutdown() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("trying to close requester " + getDuplexConnection()); + } if (terminationError == null) { if (TERMINATION_ERROR.compareAndSet(this, null, CLOSED_CHANNEL_EXCEPTION)) { terminate(CLOSED_CHANNEL_EXCEPTION); + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); + } + } + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); } } } private void terminate(Throwable e) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("closing requester " + getDuplexConnection() + " due to " + e); + } if (keepAliveFramesAcceptor != null) { keepAliveFramesAcceptor.dispose(); } - getDuplexConnection().dispose(); final RequestInterceptor requestInterceptor = getRequestInterceptor(); if (requestInterceptor != null) { requestInterceptor.dispose(); @@ -361,9 +433,12 @@ private void terminate(Throwable e) { } if (e == CLOSED_CHANNEL_EXCEPTION) { - onClose.tryEmitEmpty(); + onThisSideClosedSink.tryEmitEmpty(); } else { - onClose.tryEmitError(e); + onThisSideClosedSink.tryEmitError(e); + } + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("requester closed " + getDuplexConnection()); } } } 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 ce4fe70a3..50c5ba54c 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -44,6 +44,7 @@ import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; import reactor.util.annotation.Nullable; /** Responder side of RSocket. Receives {@link ByteBuf}s from a peer's {@link RSocketRequester} */ @@ -54,6 +55,7 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { private static final Exception CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException(); private final RSocket requestHandler; + private final Sinks.Empty onThisSideClosedSink; @Nullable private final ResponderLeaseTracker leaseHandler; @@ -70,7 +72,8 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { int mtu, int maxFrameLength, int maxInboundPayloadSize, - Function requestInterceptorFunction) { + Function requestInterceptorFunction, + Sinks.Empty onThisSideClosedSink) { super( mtu, maxFrameLength, @@ -83,19 +86,27 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { this.requestHandler = requestHandler; this.leaseHandler = leaseHandler; - - connection.receive().subscribe(this::handleFrame, e -> {}); + this.onThisSideClosedSink = onThisSideClosedSink; connection .onClose() .subscribe(null, this::tryTerminateOnConnectionError, this::tryTerminateOnConnectionClose); + + connection.receive().subscribe(this::handleFrame, e -> {}); } private void tryTerminateOnConnectionError(Throwable e) { + if (LOGGER.isDebugEnabled()) { + + LOGGER.debug("Try terminate connection on responder side"); + } tryTerminate(() -> e); } private void tryTerminateOnConnectionClose() { + if (LOGGER.isDebugEnabled()) { + LOGGER.info("Try terminate connection on responder side"); + } tryTerminate(() -> CLOSED_CHANNEL_EXCEPTION); } @@ -169,6 +180,9 @@ public Mono onClose() { } final void doOnDispose() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("closing responder " + getDuplexConnection()); + } cleanUpSendingSubscriptions(); getDuplexConnection().dispose(); @@ -183,6 +197,10 @@ final void doOnDispose() { } requestHandler.dispose(); + onThisSideClosedSink.tryEmitEmpty(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("responder closed " + getDuplexConnection()); + } } private void cleanUpSendingSubscriptions() { 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 3208bb4fd..0c68db6df 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -46,6 +46,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; /** * The main class for starting an RSocket server. @@ -437,6 +438,9 @@ private Mono acceptSetup( requesterLeaseTracker = null; } + final Sinks.Empty requesterOnAllClosedSink = Sinks.unsafe().empty(); + final Sinks.Empty responderOnAllClosedSink = Sinks.unsafe().empty(); + RSocket rSocketRequester = new RSocketRequester( multiplexer.asServerConnection(), @@ -449,7 +453,10 @@ private Mono acceptSetup( setupPayload.keepAliveMaxLifetime(), keepAliveHandler, interceptors::initRequesterRequestInterceptor, - requesterLeaseTracker); + requesterLeaseTracker, + requesterOnAllClosedSink, + Mono.whenDelayError( + responderOnAllClosedSink.asMono(), requesterOnAllClosedSink.asMono())); RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); @@ -481,7 +488,8 @@ private Mono acceptSetup( ? rSocket -> interceptors.initResponderRequestInterceptor( rSocket, (RequestInterceptor) leases.sender) - : interceptors::initResponderRequestInterceptor); + : interceptors::initResponderRequestInterceptor, + responderOnAllClosedSink); }) .doFinally(signalType -> setupPayload.release()) .then(); 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 ddad96047..5aae22e89 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java @@ -79,7 +79,7 @@ public Mono acceptRSocketSetup( sendError(duplexConnection, new UnsupportedSetupException("resume not supported")); return duplexConnection.onClose(); } else { - return then.apply(new DefaultKeepAliveHandler(duplexConnection), duplexConnection); + return then.apply(new DefaultKeepAliveHandler(), duplexConnection); } } @@ -141,7 +141,7 @@ public Mono acceptRSocketSetup( resumableDuplexConnection, serverRSocketSession, serverRSocketSession), resumableDuplexConnection); } else { - return then.apply(new DefaultKeepAliveHandler(duplexConnection), duplexConnection); + return then.apply(new DefaultKeepAliveHandler(), duplexConnection); } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java index 2da572de3..3beedf97f 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java @@ -168,4 +168,9 @@ public void sendErrorAndClose(RSocketErrorException e) { public ByteBufAllocator alloc() { return source.alloc(); } + + @Override + public String toString() { + return "SetupHandlingDuplexConnection{" + "source=" + source + ", done=" + done + '}'; + } } 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 95bc210fe..23ada95fe 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -619,13 +619,8 @@ public ByteBuf poll() { t = this.last; if (t != null) { - try { - this.last = null; - return t; - } finally { - - clearAndFinalize(this); - } + this.last = null; + return t; } return null; 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 b92c25f46..4fd7a772d 100644 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java @@ -1,7 +1,6 @@ package io.rsocket.keepalive; import io.netty.buffer.ByteBuf; -import io.rsocket.Closeable; import io.rsocket.keepalive.KeepAliveSupport.KeepAlive; import io.rsocket.resume.RSocketSession; import io.rsocket.resume.ResumableDuplexConnection; @@ -16,18 +15,11 @@ 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().doFinally(s -> keepAliveSupport.stop()).subscribe(); return keepAliveSupport .onSendKeepAliveFrame(onSendKeepAliveFrame) .onTimeout(onTimeout) 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 d6a0c9292..ca4f5dcb4 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -110,17 +110,9 @@ public ClientRSocketSession( position); } - return connectionTransformer - .apply(dc) - .doOnDiscard( - Tuple2.class, - tuple2 -> { - if (logger.isDebugEnabled()) { - logger.debug("try to reestablish from discard"); - } - tryReestablishSession(tuple2); - }); - }); + return connectionTransformer.apply(dc); + }) + .doOnDiscard(Tuple2.class, this::tryReestablishSession); this.resumableFramesStore = resumableFramesStore; this.allocator = resumableDuplexConnection.alloc(); this.resumeSessionDuration = resumeSessionDuration; 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 b71693f0d..e23bc154b 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java @@ -118,7 +118,7 @@ public class InMemoryResumableFramesStore extends Flux * the {@link InMemoryResumableFramesStore#drain(long)} method. */ static final long MAX_WORK_IN_PROGRESS = - 0b0000_0000_0000_0000_0000_0000_0000_0000_1111_1111_1111_1111_1111_1111_1111_1111L; + 0b0000_0000_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111L; public InMemoryResumableFramesStore(String side, ByteBuf session, int cacheSizeBytes) { this.side = side; @@ -374,7 +374,7 @@ public void dispose() { return; } - drain(previousState | DISPOSED_FLAG); + drain((previousState + 1) | DISPOSED_FLAG); } void clearCache() { @@ -557,12 +557,13 @@ public void onNext(ByteBuf byteBuf) { return; } - if (isWorkInProgress(previousState) - || (!isConnected(previousState) && !hasPendingConnection(previousState))) { + if (isWorkInProgress(previousState)) { return; } - parent.drain(previousState + 1); + if (isConnected(previousState) || hasPendingConnection(previousState)) { + parent.drain((previousState + 1) | HAS_FRAME_FLAG); + } } @Override @@ -587,7 +588,7 @@ public void onError(Throwable t) { return; } - parent.drain(previousState | TERMINATED_FLAG); + parent.drain((previousState + 1) | TERMINATED_FLAG); } @Override @@ -609,7 +610,7 @@ public void onComplete() { return; } - parent.drain(previousState | TERMINATED_FLAG); + parent.drain((previousState + 1) | TERMINATED_FLAG); } @Override 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 9704d9aba..c8811b9b3 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -322,6 +322,26 @@ static boolean isResumableFrame(ByteBuf frame) { return FrameHeaderCodec.streamId(frame) != 0; } + @Override + public String toString() { + return "ResumableDuplexConnection{" + + "side='" + + side + + '\'' + + ", session='" + + session + + '\'' + + ", remoteAddress=" + + remoteAddress + + ", state=" + + state + + ", activeConnection=" + + activeConnection + + ", connectionIndex=" + + connectionIndex + + '}'; + } + private static final class DisposedConnection implements DuplexConnection { static final DisposedConnection INSTANCE = new DisposedConnection(); diff --git a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java index 3c95fd65c..fa208269b 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -643,6 +643,8 @@ public static class ClientSocketRule extends AbstractSocketRule { protected Runnable delayer; protected Sinks.One producer; + protected Sinks.Empty thisClosedSink; + @Override protected void doInit() { super.doInit(); @@ -660,6 +662,7 @@ protected void doInit() { @Override protected RSocket newRSocket() { + this.thisClosedSink = Sinks.empty(); return new RSocketRequester( connection, PayloadDecoder.ZERO_COPY, @@ -671,7 +674,9 @@ protected RSocket newRSocket() { Integer.MAX_VALUE, null, __ -> null, - null); + null, + thisClosedSink, + thisClosedSink.asMono()); } } } 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 a9c9ed9a5..aad0aaaca 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -91,6 +91,8 @@ class RSocketLeaseTest { private Sinks.Many leaseSender = Sinks.many().multicast().onBackpressureBuffer(); private RequesterLeaseTracker requesterLeaseTracker; + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; @BeforeEach void setUp() { @@ -100,6 +102,8 @@ void setUp() { connection = new TestDuplexConnection(byteBufAllocator); requesterLeaseTracker = new RequesterLeaseTracker(TAG, 0); responderLeaseTracker = new ResponderLeaseTracker(TAG, connection, () -> leaseSender.asFlux()); + this.thisClosedSink = Sinks.empty(); + this.otherClosedSink = Sinks.empty(); ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(connection, new InitializingInterceptorRegistry(), true); @@ -115,7 +119,9 @@ void setUp() { 0, null, __ -> null, - requesterLeaseTracker); + requesterLeaseTracker, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); mockRSocketHandler = mock(RSocket.class); when(mockRSocketHandler.metadataPush(any())) @@ -182,7 +188,8 @@ protected void hookOnError(Throwable throwable) { 0, FRAME_LENGTH_MASK, Integer.MAX_VALUE, - __ -> null); + __ -> null, + otherClosedSink); } @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 25d91b25b..d736ae190 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -44,6 +44,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; import reactor.test.util.RaceTestUtils; class RSocketRequesterSubscribersTest { @@ -60,11 +61,15 @@ class RSocketRequesterSubscribersTest { private LeaksTrackingByteBufAllocator allocator; private RSocket rSocketRequester; private TestDuplexConnection connection; + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; @BeforeEach void setUp() { allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); connection = new TestDuplexConnection(allocator); + this.thisClosedSink = Sinks.empty(); + this.otherClosedSink = Sinks.empty(); rSocketRequester = new RSocketRequester( connection, @@ -77,7 +82,9 @@ void setUp() { 0, null, __ -> null, - null); + null, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); } @ParameterizedTest 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 183785d2f..5861a459a 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -1453,8 +1453,14 @@ public void testWorkaround959(String type) { } public static class ClientSocketRule extends AbstractSocketRule { + + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; + @Override protected RSocketRequester newRSocket() { + this.thisClosedSink = Sinks.empty(); + this.otherClosedSink = Sinks.empty(); return new RSocketRequester( connection, PayloadDecoder.ZERO_COPY, @@ -1466,7 +1472,9 @@ protected RSocketRequester newRSocket() { Integer.MAX_VALUE, null, (__) -> null, - null); + null, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); } public int getStreamIdForRequestType(FrameType expectedFrameType) { 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 c0f64469c..cbfc05ea3 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -1184,6 +1184,7 @@ public static class ServerSocketRule extends AbstractSocketRule onCloseSink; @Override protected void doInit() { @@ -1220,6 +1221,7 @@ public void setAcceptingSocket(RSocket acceptingSocket, int prefetch) { @Override protected RSocketResponder newRSocket() { + onCloseSink = Sinks.empty(); return new RSocketResponder( connection, acceptingSocket, @@ -1228,7 +1230,8 @@ protected RSocketResponder newRSocket() { 0, maxFrameLength, maxInboundPayloadSize, - __ -> requestInterceptor); + __ -> requestInterceptor, + onCloseSink); } 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 c9904d583..98cc94087 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -509,6 +509,8 @@ public static class SocketRule { private RSocket requestAcceptor; private LeaksTrackingByteBufAllocator allocator; + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; public LeaksTrackingByteBufAllocator alloc() { return allocator; @@ -519,6 +521,9 @@ public void init() { serverProcessor = Sinks.many().multicast().directBestEffort(); clientProcessor = Sinks.many().multicast().directBestEffort(); + this.thisClosedSink = Sinks.empty(); + this.otherClosedSink = Sinks.empty(); + LocalDuplexConnection serverConnection = new LocalDuplexConnection("server", allocator, clientProcessor, serverProcessor); LocalDuplexConnection clientConnection = @@ -566,7 +571,8 @@ public Flux requestChannel(Publisher payloads) { 0, FRAME_LENGTH_MASK, Integer.MAX_VALUE, - __ -> null); + __ -> null, + otherClosedSink); crs = new RSocketRequester( @@ -580,7 +586,9 @@ public Flux requestChannel(Publisher payloads) { 0, null, __ -> null, - null); + null, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); } 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 44ff78a64..e85c5856e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -69,6 +69,8 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); TestDuplexConnection conn = new TestDuplexConnection(allocator); + Sinks.Empty onThisSideClosedSink = Sinks.empty(); + RSocketRequester rSocket = new RSocketRequester( conn, @@ -81,7 +83,9 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { 0, null, __ -> null, - null); + null, + onThisSideClosedSink, + onThisSideClosedSink.asMono()); String errorMsg = "error"; @@ -107,6 +111,7 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); TestDuplexConnection conn = new TestDuplexConnection(allocator); + Sinks.Empty onThisSideClosedSink = Sinks.empty(); RSocketRequester rSocket = new RSocketRequester( conn, @@ -119,7 +124,9 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { 0, null, __ -> null, - null); + null, + onThisSideClosedSink, + onThisSideClosedSink.asMono()); conn.addToReceivedBuffer( ErrorFrameCodec.encode(ByteBufAllocator.DEFAULT, 0, new RejectedSetupException("error"))); diff --git a/rsocket-examples/build.gradle b/rsocket-examples/build.gradle index 423acec79..a49570776 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -28,7 +28,6 @@ dependencies { implementation "io.micrometer:micrometer-core" implementation "io.micrometer:micrometer-tracing" implementation project(":rsocket-micrometer") - testImplementation 'org.awaitility:awaitility' runtimeOnly 'ch.qos.logback:logback-classic' @@ -37,6 +36,7 @@ dependencies { testImplementation 'org.mockito:mockito-core' testImplementation 'org.assertj:assertj-core' testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.awaitility:awaitility' testImplementation "io.micrometer:micrometer-test" testImplementation "io.micrometer:micrometer-tracing-integration-test" 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 113b7a2f8..1b3779e85 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 @@ -79,10 +79,10 @@ public Mono connect() { Sinks.One inSink = Sinks.one(); Sinks.One outSink = Sinks.one(); - UnboundedProcessor in = new UnboundedProcessor(() -> inSink.tryEmitValue(inSink)); - UnboundedProcessor out = new UnboundedProcessor(() -> outSink.tryEmitValue(outSink)); + UnboundedProcessor in = new UnboundedProcessor(inSink::tryEmitEmpty); + UnboundedProcessor out = new UnboundedProcessor(outSink::tryEmitEmpty); - Mono onClose = inSink.asMono().zipWith(outSink.asMono()).then(); + Mono onClose = inSink.asMono().and(outSink.asMono()); server.apply(new LocalDuplexConnection(name, allocator, out, in, onClose)).subscribe(); 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 08fd780dc..c1d0fd2a3 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 @@ -36,7 +36,7 @@ final class LocalDuplexConnection implements DuplexConnection { private final LocalSocketAddress address; private final ByteBufAllocator allocator; - private final Flux in; + private final UnboundedProcessor in; private final Mono onClose; @@ -54,7 +54,7 @@ final class LocalDuplexConnection implements DuplexConnection { LocalDuplexConnection( String name, ByteBufAllocator allocator, - Flux in, + UnboundedProcessor in, UnboundedProcessor out, Mono onClose) { this.address = new LocalSocketAddress(name); @@ -111,6 +111,11 @@ public SocketAddress remoteAddress() { return address; } + @Override + public String toString() { + return "LocalDuplexConnection{" + "address=" + address + "hash=" + hashCode() + '}'; + } + static class ByteBufReleaserOperator implements CoreSubscriber, Subscription, Fuseable.QueueSubscription { 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 0445f5c02..f5d36269c 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,7 @@ /** An implementation of {@link DuplexConnection} that connects via TCP. */ public final class TcpDuplexConnection extends BaseDuplexConnection { - + private final String side; private final Connection connection; /** @@ -40,14 +40,19 @@ public final class TcpDuplexConnection extends BaseDuplexConnection { * @param connection the {@link Connection} for managing the server */ public TcpDuplexConnection(Connection connection) { + this("unknown", connection); + } + + /** + * Creates a new instance + * + * @param connection the {@link Connection} for managing the server + */ + public TcpDuplexConnection(String side, Connection connection) { this.connection = Objects.requireNonNull(connection, "connection must not be null"); + this.side = side; - connection - .outbound() - .send(sender.hide()) - .then() - .doFinally(__ -> connection.dispose()) - .subscribe(); + connection.outbound().send(sender).then().doFinally(__ -> connection.dispose()).subscribe(); } @Override @@ -85,4 +90,9 @@ public Flux receive() { public void sendFrame(int streamId, ByteBuf frame) { super.sendFrame(streamId, FrameLengthCodec.encode(alloc(), frame.readableBytes(), frame)); } + + @Override + public String toString() { + return "TcpDuplexConnection{" + "side='" + side + '\'' + ", connection=" + connection + '}'; + } } 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 9deef6030..8f1170c5b 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 @@ -36,7 +36,7 @@ * stitched back on for frames received. */ public final class WebsocketDuplexConnection extends BaseDuplexConnection { - + private final String side; private final Connection connection; /** @@ -45,11 +45,21 @@ public final class WebsocketDuplexConnection extends BaseDuplexConnection { * @param connection the {@link Connection} to for managing the server */ public WebsocketDuplexConnection(Connection connection) { + this("unknown", connection); + } + + /** + * Creates a new instance + * + * @param connection the {@link Connection} to for managing the server + */ + public WebsocketDuplexConnection(String side, Connection connection) { this.connection = Objects.requireNonNull(connection, "connection must not be null"); + this.side = side; connection .outbound() - .sendObject(sender.map(BinaryWebSocketFrame::new).hide()) + .sendObject(sender.map(BinaryWebSocketFrame::new)) .then() .doFinally(__ -> connection.dispose()) .subscribe(); @@ -85,4 +95,15 @@ public void sendErrorAndClose(RSocketErrorException e) { final ByteBuf errorFrame = ErrorFrameCodec.encode(alloc(), 0, e); sender.tryEmitFinal(errorFrame); } + + @Override + public String toString() { + return "WebsocketDuplexConnection{" + + "side='" + + side + + '\'' + + ", connection=" + + connection + + '}'; + } } 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 f64c6063c..84214b98c 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 @@ -116,6 +116,6 @@ public Mono connect() { return client .doOnConnected(c -> c.addHandlerLast(new RSocketLengthCodec(maxFrameLength))) .connect() - .map(TcpDuplexConnection::new); + .map(connection -> new TcpDuplexConnection("client", connection)); } } 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 fe66da50a..86be47893 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 @@ -172,6 +172,6 @@ public Mono connect() { .websocket(specBuilder.build()) .uri(path) .connect() - .map(WebsocketDuplexConnection::new); + .map(connection -> new WebsocketDuplexConnection("client", 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 effc7bed5..32562c4a4 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 @@ -114,7 +114,7 @@ public Mono start(ConnectionAcceptor acceptor) { c -> { c.addHandlerLast(new RSocketLengthCodec(maxFrameLength)); acceptor - .apply(new TcpDuplexConnection(c)) + .apply(new TcpDuplexConnection("server", c)) .then(Mono.never()) .subscribe(c.disposeSubscriber()); }) 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 38344c472..db13720e7 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 @@ -80,6 +80,8 @@ public Mono start(ConnectionAcceptor acceptor) { public static BiFunction> newHandler( ConnectionAcceptor acceptor) { return (in, out) -> - acceptor.apply(new WebsocketDuplexConnection((Connection) in)).then(out.neverComplete()); + acceptor + .apply(new WebsocketDuplexConnection("server", (Connection) in)) + .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 81ac8dcb6..4fe736fad 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 @@ -117,7 +117,7 @@ public Mono start(ConnectionAcceptor acceptor) { return response.sendWebsocket( (in, out) -> acceptor - .apply(new WebsocketDuplexConnection((Connection) in)) + .apply(new WebsocketDuplexConnection("server", (Connection) in)) .then(out.neverComplete()), specBuilder.build()); }) From 47e4e3b561a7ea0c52b8df77a520ebe32eccb673 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 14 Apr 2023 12:22:19 +0300 Subject: [PATCH 101/116] update version Signed-off-by: Oleh Dokuka --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7f8f4ca23..b234fff5b 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.1.4 +version=1.2.0 perfBaselineVersion=1.1.3 From 5547cb1843c361e0f2a36bb70f5c6ceb80c81ccb Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Fri, 21 Apr 2023 22:33:10 +0300 Subject: [PATCH 102/116] improves tests & ensures thereare no leaks of bufs at execution (#1090) --- .../io/rsocket/core/DefaultRSocketClient.java | 2 +- .../ClientServerInputMultiplexerTest.java | 24 +- .../core/DefaultRSocketClientTests.java | 56 ++ .../java/io/rsocket/core/KeepAliveTest.java | 796 +++++++++--------- .../io/rsocket/core/RSocketConnectorTest.java | 6 + .../io/rsocket/core/RSocketLeaseTest.java | 54 +- .../io/rsocket/core/RSocketReconnectTest.java | 63 +- .../core/RSocketRequesterSubscribersTest.java | 14 +- .../core/RSocketRequesterTerminationTest.java | 95 ++- .../io/rsocket/core/RSocketRequesterTest.java | 47 +- .../io/rsocket/core/RSocketResponderTest.java | 12 +- .../core/RSocketServerFragmentationTest.java | 29 +- .../io/rsocket/core/RSocketServerTest.java | 28 +- .../java/io/rsocket/core/RSocketTest.java | 6 + .../io/rsocket/core/SetupRejectionTest.java | 4 + .../io/rsocket/exceptions/ExceptionsTest.java | 218 +++-- .../LoadbalanceRSocketClientTest.java | 5 +- .../rsocket/loadbalance/LoadbalanceTest.java | 10 +- .../metadata/CompositeMetadataCodecTest.java | 103 +-- .../metadata/MimeTypeMetadataCodecTest.java | 26 +- .../plugins/RequestInterceptorTest.java | 46 +- .../resume/ClientRSocketSessionTest.java | 5 +- .../resume/ServerRSocketSessionTest.java | 310 +++---- .../test/util/TestClientTransport.java | 4 +- .../rsocket/integration/TestingStreaming.java | 2 - 25 files changed, 1180 insertions(+), 785 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java index 5119814cd..9cd89c0b1 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java @@ -450,8 +450,8 @@ public void accept(RSocket rSocket, Throwable t) { @Override public void request(long n) { - this.main.request(n); super.request(n); + this.main.request(n); } public void cancel() { diff --git a/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java b/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java index c9ecb6eb6..195df9434 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java @@ -56,12 +56,20 @@ public void clientSplits() { clientMultiplexer .asClientConnection() .receive() - .doOnNext(f -> clientFrames.incrementAndGet()) + .doOnNext( + f -> { + clientFrames.incrementAndGet(); + f.release(); + }) .subscribe(); clientMultiplexer .asServerConnection() .receive() - .doOnNext(f -> serverFrames.incrementAndGet()) + .doOnNext( + f -> { + serverFrames.incrementAndGet(); + f.release(); + }) .subscribe(); source.addToReceivedBuffer(errorFrame(1).retain()); @@ -101,12 +109,20 @@ public void serverSplits() { serverMultiplexer .asClientConnection() .receive() - .doOnNext(f -> clientFrames.incrementAndGet()) + .doOnNext( + f -> { + clientFrames.incrementAndGet(); + f.release(); + }) .subscribe(); serverMultiplexer .asServerConnection() .receive() - .doOnNext(f -> serverFrames.incrementAndGet()) + .doOnNext( + f -> { + serverFrames.incrementAndGet(); + f.release(); + }) .subscribe(); source.addToReceivedBuffer(errorFrame(1).retain()); diff --git a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java index fa208269b..a8a5f2e58 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -19,6 +19,7 @@ import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; +import io.rsocket.FrameAssert; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.RaceTestConstants; @@ -434,6 +435,8 @@ public void shouldBeAbleToResolveOriginalSource() { assertSubscriber1.assertTerminated().assertValueCount(1); Assertions.assertThat(assertSubscriber1.values()).isEqualTo(assertSubscriber.values()); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -457,6 +460,13 @@ public void shouldDisposeOriginalSource() { .assertErrorMessage("Disposed"); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -494,6 +504,13 @@ public Mono onClose() { onCloseSubscriber.assertTerminated().assertComplete(); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -515,6 +532,13 @@ public void shouldResolveOnStartSource() { assertSubscriber1.assertTerminated().assertComplete(); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -536,6 +560,13 @@ public void shouldNotStartIfAlreadyDisposed() { assertSubscriber1.assertTerminated().assertComplete(); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -553,6 +584,11 @@ public void shouldBeRestartedIfSourceWasClosed() { rule.socket.dispose(); + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + terminateSubscriber.assertNotTerminated(); Assertions.assertThat(rule.client.isDisposed()).isFalse(); @@ -576,6 +612,13 @@ public void shouldBeRestartedIfSourceWasClosed() { Assertions.assertThat(rule.client.connect()).isFalse(); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -603,6 +646,13 @@ public void shouldDisposeOriginalSourceIfRacing() { .assertTerminated() .assertError(CancellationException.class) .assertErrorMessage("Disposed"); + + ByteBuf buf; + while ((buf = rule.connection.pollFrame()) != null) { + FrameAssert.assertThat(buf).hasStreamIdZero().hasData("Disposed").hasNoLeaks(); + } + + rule.allocator.assertHasNoLeaks(); } } @@ -632,8 +682,14 @@ public void shouldStartOriginalSourceOnceIfRacing() { AssertSubscriber assertSubscriber1 = AssertSubscriber.create(); rule.client.onClose().subscribe(assertSubscriber1); + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); assertSubscriber1.assertTerminated().assertComplete(); + + rule.allocator.assertHasNoLeaks(); } } 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 5bd5f9999..5be59235c 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java @@ -1,376 +1,420 @@ -/// * -// * 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.core; -// -// import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; -// import static io.rsocket.keepalive.KeepAliveHandler.DefaultKeepAliveHandler; -// import static io.rsocket.keepalive.KeepAliveHandler.ResumableKeepAliveHandler; -// -// import io.netty.buffer.ByteBuf; -// 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.FrameHeaderCodec; -// import io.rsocket.frame.FrameType; -// import io.rsocket.frame.KeepAliveFrameCodec; -// import io.rsocket.core.RequesterLeaseHandler; -// 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; -// import org.assertj.core.api.Assertions; -// import org.junit.jupiter.api.AfterEach; -// import org.junit.jupiter.api.BeforeEach; -// import org.junit.jupiter.api.Test; -// import reactor.core.Disposable; -// import reactor.core.publisher.Flux; -// import reactor.core.publisher.Mono; -// import reactor.test.StepVerifier; -// import reactor.test.scheduler.VirtualTimeScheduler; -// -// 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; -// -// VirtualTimeScheduler virtualTimeScheduler; -// -// @BeforeEach -// public void setUp() { -// virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); -// } -// -// @AfterEach -// public void tearDown() { -// VirtualTimeScheduler.reset(); -// } -// -// static RSocketState requester(int tickPeriod, int timeout) { -// LeaksTrackingByteBufAllocator allocator = -// LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); -// TestDuplexConnection connection = new TestDuplexConnection(allocator); -// RSocketRequester rSocket = -// new RSocketRequester( -// connection, -// DefaultPayload::create, -// StreamIdSupplier.clientSupplier(), -// 0, -// FRAME_LENGTH_MASK, -// Integer.MAX_VALUE, -// tickPeriod, -// timeout, -// new DefaultKeepAliveHandler(connection), -// RequesterLeaseHandler.None); -// return new RSocketState(rSocket, allocator, connection); -// } -// -// static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { -// LeaksTrackingByteBufAllocator allocator = -// LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); -// TestDuplexConnection connection = new TestDuplexConnection(allocator); -//// ResumableDuplexConnection resumableConnection = -//// new ResumableDuplexConnection( -//// "test", -//// connection, -//// new InMemoryResumableFramesStore("test", 10_000), -//// Duration.ofSeconds(10), -//// false); -// -// RSocketRequester rSocket = -// new RSocketRequester( -// resumableConnection, -// DefaultPayload::create, -// StreamIdSupplier.clientSupplier(), -// 0, -// FRAME_LENGTH_MASK, -// Integer.MAX_VALUE, -// tickPeriod, -// timeout, -// new ResumableKeepAliveHandler(resumableConnection), -// RequesterLeaseHandler.None); -// return new ResumableRSocketState(rSocket, connection, resumableConnection, allocator); -// } -// -// @Test -// void rSocketNotDisposedOnPresentKeepAlives() { -// RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// -// TestDuplexConnection connection = requesterState.connection(); -// -// Disposable disposable = -// Flux.interval(Duration.ofMillis(KEEP_ALIVE_INTERVAL)) -// .subscribe( -// n -> -// connection.addToReceivedBuffer( -// KeepAliveFrameCodec.encode( -// requesterState.allocator, true, 0, Unpooled.EMPTY_BUFFER))); -// -// virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_TIMEOUT * 2)); -// -// RSocket rSocket = requesterState.rSocket(); -// -// Assertions.assertThat(rSocket.isDisposed()).isFalse(); -// -// disposable.dispose(); -// -// requesterState.connection.dispose(); -// requesterState.rSocket.dispose(); -// -// Assertions.assertThat(requesterState.connection.getSent()).allMatch(ByteBuf::release); -// -// requesterState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void noKeepAlivesSentAfterRSocketDispose() { -// RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// -// requesterState.rSocket().dispose(); -// -// Duration duration = Duration.ofMillis(500); -// -// StepVerifier.create(Flux.from(requesterState.connection().getSentAsPublisher()).take(duration)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) -// .expectComplete() -// .verify(Duration.ofSeconds(1)); -// -// requesterState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void rSocketDisposedOnMissingKeepAlives() { -// RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// -// RSocket rSocket = requesterState.rSocket(); -// -// virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_TIMEOUT * 2)); -// -// Assertions.assertThat(rSocket.isDisposed()).isTrue(); -// rSocket -// .onClose() -// .as(StepVerifier::create) -// .expectError(ConnectionErrorException.class) -// .verify(Duration.ofMillis(100)); -// -// Assertions.assertThat(requesterState.connection.getSent()).allMatch(ByteBuf::release); -// -// requesterState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void clientRequesterSendsKeepAlives() { -// RSocketState RSocketState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// TestDuplexConnection connection = RSocketState.connection(); -// -// StepVerifier.create(Flux.from(connection.getSentAsPublisher()).take(3)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) -// .expectNextMatches(this::keepAliveFrameWithRespondFlag) -// .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) -// .expectNextMatches(this::keepAliveFrameWithRespondFlag) -// .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) -// .expectNextMatches(this::keepAliveFrameWithRespondFlag) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// RSocketState.rSocket.dispose(); -// RSocketState.connection.dispose(); -// -// RSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void requesterRespondsToKeepAlives() { -// RSocketState rSocketState = requester(100_000, 100_000); -// TestDuplexConnection connection = rSocketState.connection(); -// Duration duration = Duration.ofMillis(100); -// Mono.delay(duration) -// .subscribe( -// l -> -// connection.addToReceivedBuffer( -// KeepAliveFrameCodec.encode( -// rSocketState.allocator, true, 0, Unpooled.EMPTY_BUFFER))); -// -// StepVerifier.create(Flux.from(connection.getSentAsPublisher()).take(1)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) -// .expectNextMatches(this::keepAliveFrameWithoutRespondFlag) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// rSocketState.rSocket.dispose(); -// rSocketState.connection.dispose(); -// -// rSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void resumableRequesterNoKeepAlivesAfterDisconnect() { -// ResumableRSocketState rSocketState = -// resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// TestDuplexConnection testConnection = rSocketState.connection(); -// ResumableDuplexConnection resumableDuplexConnection = -// rSocketState.resumableDuplexConnection(); -// -// resumableDuplexConnection.disconnect(); -// -// Duration duration = Duration.ofMillis(500); -// StepVerifier.create(Flux.from(testConnection.getSentAsPublisher()).take(duration)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// rSocketState.rSocket.dispose(); -// rSocketState.connection.dispose(); -// -// rSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void resumableRequesterKeepAlivesAfterReconnect() { -// ResumableRSocketState rSocketState = -// resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// ResumableDuplexConnection resumableDuplexConnection = -// rSocketState.resumableDuplexConnection(); -// resumableDuplexConnection.disconnect(); -// TestDuplexConnection newTestConnection = new TestDuplexConnection(rSocketState.alloc()); -// resumableDuplexConnection.reconnect(newTestConnection); -// resumableDuplexConnection.resume(0, 0, ignored -> Mono.empty()); -// -// StepVerifier.create(Flux.from(newTestConnection.getSentAsPublisher()).take(1)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) -// .expectNextMatches(frame -> keepAliveFrame(frame) && frame.release()) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// rSocketState.rSocket.dispose(); -// rSocketState.connection.dispose(); -// -// rSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void resumableRequesterNoKeepAlivesAfterDispose() { -// ResumableRSocketState rSocketState = -// resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// rSocketState.rSocket().dispose(); -// Duration duration = Duration.ofMillis(500); -// StepVerifier.create(Flux.from(rSocketState.connection().getSentAsPublisher()).take(duration)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// rSocketState.rSocket.dispose(); -// rSocketState.connection.dispose(); -// -// rSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void resumableRSocketsNotDisposedOnMissingKeepAlives() throws InterruptedException { -// ResumableRSocketState resumableRequesterState = -// resumableRequester(KEEP_ALIVE_INTERVAL, RESUMABLE_KEEP_ALIVE_TIMEOUT); -// RSocket rSocket = resumableRequesterState.rSocket(); -// TestDuplexConnection connection = resumableRequesterState.connection(); -// -// virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(500)); -// -// Assertions.assertThat(rSocket.isDisposed()).isFalse(); -// Assertions.assertThat(connection.isDisposed()).isTrue(); -// -// -// Assertions.assertThat(resumableRequesterState.connection.getSent()).allMatch(ByteBuf::release); -// -// resumableRequesterState.connection.dispose(); -// resumableRequesterState.rSocket.dispose(); -// -// resumableRequesterState.allocator.assertHasNoLeaks(); -// } -// -// private boolean keepAliveFrame(ByteBuf frame) { -// return FrameHeaderCodec.frameType(frame) == FrameType.KEEPALIVE; -// } -// -// private boolean keepAliveFrameWithRespondFlag(ByteBuf frame) { -// return keepAliveFrame(frame) && KeepAliveFrameCodec.respondFlag(frame) && frame.release(); -// } -// -// private boolean keepAliveFrameWithoutRespondFlag(ByteBuf frame) { -// return keepAliveFrame(frame) && !KeepAliveFrameCodec.respondFlag(frame) && frame.release(); -// } -// -// static class RSocketState { -// private final RSocket rSocket; -// private final TestDuplexConnection connection; -// private final LeaksTrackingByteBufAllocator allocator; -// -// public RSocketState( -// RSocket rSocket, LeaksTrackingByteBufAllocator allocator, TestDuplexConnection connection) -// { -// this.rSocket = rSocket; -// this.connection = connection; -// this.allocator = allocator; -// } -// -// public TestDuplexConnection connection() { -// return connection; -// } -// -// public RSocket rSocket() { -// return rSocket; -// } -// -// public LeaksTrackingByteBufAllocator alloc() { -// return allocator; -// } -// } -// -// static class ResumableRSocketState { -// private final RSocket rSocket; -// private final TestDuplexConnection connection; -// private final ResumableDuplexConnection resumableDuplexConnection; -// private final LeaksTrackingByteBufAllocator allocator; -// -// public ResumableRSocketState( -// RSocket rSocket, -// TestDuplexConnection connection, -// ResumableDuplexConnection resumableDuplexConnection, -// LeaksTrackingByteBufAllocator allocator) { -// this.rSocket = rSocket; -// this.connection = connection; -// this.resumableDuplexConnection = resumableDuplexConnection; -// this.allocator = allocator; -// } -// -// public TestDuplexConnection connection() { -// return connection; -// } -// -// public ResumableDuplexConnection resumableDuplexConnection() { -// return resumableDuplexConnection; -// } -// -// public RSocket rSocket() { -// return rSocket; -// } -// -// public LeaksTrackingByteBufAllocator alloc() { -// return allocator; -// } -// } -// } +/* + * 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.core; + +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; +import static io.rsocket.keepalive.KeepAliveHandler.DefaultKeepAliveHandler; +import static io.rsocket.keepalive.KeepAliveHandler.ResumableKeepAliveHandler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.FrameAssert; +import io.rsocket.RSocket; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.exceptions.ConnectionErrorException; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.KeepAliveFrameCodec; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.resume.InMemoryResumableFramesStore; +import io.rsocket.resume.RSocketSession; +import io.rsocket.resume.ResumableDuplexConnection; +import io.rsocket.resume.ResumeStateHolder; +import io.rsocket.test.util.TestDuplexConnection; +import java.time.Duration; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; +import reactor.test.StepVerifier; +import reactor.test.scheduler.VirtualTimeScheduler; + +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; + + VirtualTimeScheduler virtualTimeScheduler; + + @BeforeEach + public void setUp() { + virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); + } + + @AfterEach + public void tearDown() { + VirtualTimeScheduler.reset(); + } + + static RSocketState requester(int tickPeriod, int timeout) { + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + TestDuplexConnection connection = new TestDuplexConnection(allocator); + Sinks.Empty empty = Sinks.empty(); + RSocketRequester rSocket = + new RSocketRequester( + connection, + PayloadDecoder.ZERO_COPY, + StreamIdSupplier.clientSupplier(), + 0, + FRAME_LENGTH_MASK, + Integer.MAX_VALUE, + tickPeriod, + timeout, + new DefaultKeepAliveHandler(), + r -> null, + null, + empty, + empty.asMono()); + return new RSocketState(rSocket, allocator, connection, empty); + } + + static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + TestDuplexConnection connection = new TestDuplexConnection(allocator); + ResumableDuplexConnection resumableConnection = + new ResumableDuplexConnection( + "test", + Unpooled.EMPTY_BUFFER, + connection, + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 10_000)); + Sinks.Empty onClose = Sinks.empty(); + + RSocketRequester rSocket = + new RSocketRequester( + resumableConnection, + PayloadDecoder.ZERO_COPY, + StreamIdSupplier.clientSupplier(), + 0, + FRAME_LENGTH_MASK, + Integer.MAX_VALUE, + tickPeriod, + timeout, + new ResumableKeepAliveHandler( + resumableConnection, + Mockito.mock(RSocketSession.class), + Mockito.mock(ResumeStateHolder.class)), + __ -> null, + null, + onClose, + onClose.asMono()); + return new ResumableRSocketState(rSocket, connection, resumableConnection, onClose, allocator); + } + + @Test + void rSocketNotDisposedOnPresentKeepAlives() { + RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + + TestDuplexConnection connection = requesterState.connection(); + + Disposable disposable = + Flux.interval(Duration.ofMillis(KEEP_ALIVE_INTERVAL)) + .subscribe( + n -> + connection.addToReceivedBuffer( + KeepAliveFrameCodec.encode( + requesterState.allocator, true, 0, Unpooled.EMPTY_BUFFER))); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_TIMEOUT * 2)); + + RSocket rSocket = requesterState.rSocket(); + + Assertions.assertThat(rSocket.isDisposed()).isFalse(); + + disposable.dispose(); + + requesterState.connection.dispose(); + requesterState.rSocket.dispose(); + + Assertions.assertThat(requesterState.connection.getSent()).allMatch(ByteBuf::release); + + requesterState.allocator.assertHasNoLeaks(); + } + + @Test + void noKeepAlivesSentAfterRSocketDispose() { + RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + + requesterState.rSocket().dispose(); + + Duration duration = Duration.ofMillis(500); + + virtualTimeScheduler.advanceTimeBy(duration); + + FrameAssert.assertThat(requesterState.connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasData("Disposed") + .hasNoLeaks(); + FrameAssert.assertThat(requesterState.connection.pollFrame()).isNull(); + requesterState.allocator.assertHasNoLeaks(); + } + + @Test + void rSocketDisposedOnMissingKeepAlives() { + RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + + RSocket rSocket = requesterState.rSocket(); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_TIMEOUT * 2)); + + Assertions.assertThat(rSocket.isDisposed()).isTrue(); + rSocket + .onClose() + .as(StepVerifier::create) + .expectError(ConnectionErrorException.class) + .verify(Duration.ofMillis(100)); + + Assertions.assertThat(requesterState.connection.getSent()).allMatch(ByteBuf::release); + + requesterState.allocator.assertHasNoLeaks(); + } + + @Test + void clientRequesterSendsKeepAlives() { + RSocketState RSocketState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + TestDuplexConnection connection = RSocketState.connection(); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL)); + this.keepAliveFrameWithRespondFlag(connection.pollFrame()); + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL)); + this.keepAliveFrameWithRespondFlag(connection.pollFrame()); + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL)); + this.keepAliveFrameWithRespondFlag(connection.pollFrame()); + + RSocketState.rSocket.dispose(); + FrameAssert.assertThat(connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasData("Disposed") + .hasNoLeaks(); + RSocketState.connection.dispose(); + + RSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void requesterRespondsToKeepAlives() { + RSocketState rSocketState = requester(100_000, 100_000); + TestDuplexConnection connection = rSocketState.connection(); + Duration duration = Duration.ofMillis(100); + Mono.delay(duration) + .subscribe( + l -> + connection.addToReceivedBuffer( + KeepAliveFrameCodec.encode( + rSocketState.allocator, true, 0, Unpooled.EMPTY_BUFFER))); + + virtualTimeScheduler.advanceTimeBy(duration); + FrameAssert.assertThat(connection.awaitFrame()) + .typeOf(FrameType.KEEPALIVE) + .matches(this::keepAliveFrameWithoutRespondFlag); + + rSocketState.rSocket.dispose(); + FrameAssert.assertThat(rSocketState.connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + rSocketState.connection.dispose(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRequesterNoKeepAlivesAfterDisconnect() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + TestDuplexConnection testConnection = rSocketState.connection(); + ResumableDuplexConnection resumableDuplexConnection = rSocketState.resumableDuplexConnection(); + + resumableDuplexConnection.disconnect(); + + Duration duration = Duration.ofMillis(KEEP_ALIVE_INTERVAL * 5); + virtualTimeScheduler.advanceTimeBy(duration); + Assertions.assertThat(testConnection.pollFrame()).isNull(); + + rSocketState.rSocket.dispose(); + rSocketState.connection.dispose(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRequesterKeepAlivesAfterReconnect() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + ResumableDuplexConnection resumableDuplexConnection = rSocketState.resumableDuplexConnection(); + resumableDuplexConnection.disconnect(); + TestDuplexConnection newTestConnection = new TestDuplexConnection(rSocketState.alloc()); + resumableDuplexConnection.connect(newTestConnection); + // resumableDuplexConnection.(0, 0, ignored -> Mono.empty()); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL)); + + FrameAssert.assertThat(newTestConnection.awaitFrame()) + .typeOf(FrameType.KEEPALIVE) + .hasStreamIdZero() + .hasNoLeaks(); + + rSocketState.rSocket.dispose(); + FrameAssert.assertThat(newTestConnection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + FrameAssert.assertThat(newTestConnection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Connection Closed Unexpectedly") // API limitations + .hasNoLeaks(); + newTestConnection.dispose(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRequesterNoKeepAlivesAfterDispose() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + rSocketState.rSocket().dispose(); + Duration duration = Duration.ofMillis(500); + StepVerifier.create(Flux.from(rSocketState.connection().getSentAsPublisher()).take(duration)) + .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + rSocketState.rSocket.dispose(); + FrameAssert.assertThat(rSocketState.connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + rSocketState.connection.dispose(); + FrameAssert.assertThat(rSocketState.connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Connection Closed Unexpectedly") + .hasNoLeaks(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRSocketsNotDisposedOnMissingKeepAlives() throws InterruptedException { + ResumableRSocketState resumableRequesterState = + resumableRequester(KEEP_ALIVE_INTERVAL, RESUMABLE_KEEP_ALIVE_TIMEOUT); + RSocket rSocket = resumableRequesterState.rSocket(); + TestDuplexConnection connection = resumableRequesterState.connection(); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(500)); + + Assertions.assertThat(rSocket.isDisposed()).isFalse(); + Assertions.assertThat(connection.isDisposed()).isTrue(); + + Assertions.assertThat(resumableRequesterState.connection.getSent()).allMatch(ByteBuf::release); + + resumableRequesterState.connection.dispose(); + resumableRequesterState.rSocket.dispose(); + + resumableRequesterState.allocator.assertHasNoLeaks(); + } + + private boolean keepAliveFrame(ByteBuf frame) { + return FrameHeaderCodec.frameType(frame) == FrameType.KEEPALIVE; + } + + private boolean keepAliveFrameWithRespondFlag(ByteBuf frame) { + return keepAliveFrame(frame) && KeepAliveFrameCodec.respondFlag(frame) && frame.release(); + } + + private boolean keepAliveFrameWithoutRespondFlag(ByteBuf frame) { + return keepAliveFrame(frame) && !KeepAliveFrameCodec.respondFlag(frame) && frame.release(); + } + + static class RSocketState { + private final RSocket rSocket; + private final TestDuplexConnection connection; + private final LeaksTrackingByteBufAllocator allocator; + private final Sinks.Empty onClose; + + public RSocketState( + RSocket rSocket, + LeaksTrackingByteBufAllocator allocator, + TestDuplexConnection connection, + Sinks.Empty onClose) { + this.rSocket = rSocket; + this.connection = connection; + this.allocator = allocator; + this.onClose = onClose; + } + + public TestDuplexConnection connection() { + return connection; + } + + public RSocket rSocket() { + return rSocket; + } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } + } + + static class ResumableRSocketState { + private final RSocket rSocket; + private final TestDuplexConnection connection; + private final ResumableDuplexConnection resumableDuplexConnection; + private final LeaksTrackingByteBufAllocator allocator; + private final Sinks.Empty onClose; + + public ResumableRSocketState( + RSocket rSocket, + TestDuplexConnection connection, + ResumableDuplexConnection resumableDuplexConnection, + Sinks.Empty onClose, + LeaksTrackingByteBufAllocator allocator) { + this.rSocket = rSocket; + this.connection = connection; + this.resumableDuplexConnection = resumableDuplexConnection; + this.onClose = onClose; + this.allocator = allocator; + } + + public TestDuplexConnection connection() { + return connection; + } + + public ResumableDuplexConnection resumableDuplexConnection() { + return resumableDuplexConnection; + } + + public RSocket rSocket() { + return rSocket; + } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java index 40487bec1..7cf12a81e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java @@ -100,6 +100,8 @@ public void unexpectedFramesBeforeResumeOKFrame(String frameType) { .hasData("RESUME_OK frame must be received before any others") .hasStreamIdZero() .hasNoLeaks(); + + transport.alloc().assertHasNoLeaks(); } @Test @@ -204,6 +206,8 @@ public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions return byteBuf.release(); }); assertThat(setupPayload.refCnt()).isZero(); + + testClientTransport.alloc().assertHasNoLeaks(); } @Test @@ -263,6 +267,8 @@ public void ensuresThatSetupPayloadProvidedAsMonoIsReleased() { payload.refCnt() == 1 && payload.data().refCnt() == 0 && payload.metadata().refCnt() == 0); + + testClientTransport.alloc().assertHasNoLeaks(); } @Test 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 aad0aaaca..a461833d3 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -64,6 +64,7 @@ import java.util.function.BiFunction; 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.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -192,6 +193,11 @@ protected void hookOnError(Throwable throwable) { otherClosedSink); } + @AfterEach + void tearDownAndCheckForLeaks() { + byteBufAllocator.assertHasNoLeaks(); + } + @Test public void serverRSocketFactoryRejectsUnsupportedLease() { Payload payload = DefaultPayload.create(DefaultPayload.EMPTY_BUFFER); @@ -217,18 +223,26 @@ public void serverRSocketFactoryRejectsUnsupportedLease() { Assertions.assertThat(FrameHeaderCodec.frameType(error)).isEqualTo(ERROR); Assertions.assertThat(Exceptions.from(0, error).getMessage()) .isEqualTo("lease is not supported"); + error.release(); + connection.dispose(); + transport.alloc().assertHasNoLeaks(); } @Test public void clientRSocketFactorySetsLeaseFlag() { TestClientTransport clientTransport = new TestClientTransport(); - RSocketConnector.create().lease().connect(clientTransport).block(); - - Collection sent = clientTransport.testConnection().getSent(); - Assertions.assertThat(sent).hasSize(1); - ByteBuf setup = sent.iterator().next(); - Assertions.assertThat(FrameHeaderCodec.frameType(setup)).isEqualTo(SETUP); - Assertions.assertThat(SetupFrameCodec.honorLease(setup)).isTrue(); + try { + RSocketConnector.create().lease().connect(clientTransport).block(); + Collection sent = clientTransport.testConnection().getSent(); + Assertions.assertThat(sent).hasSize(1); + ByteBuf setup = sent.iterator().next(); + Assertions.assertThat(FrameHeaderCodec.frameType(setup)).isEqualTo(SETUP); + Assertions.assertThat(SetupFrameCodec.honorLease(setup)).isTrue(); + setup.release(); + } finally { + clientTransport.testConnection().dispose(); + clientTransport.alloc().assertHasNoLeaks(); + } } @ParameterizedTest @@ -382,10 +396,16 @@ void requesterExpiredLeaseRequestsAreRejected( @Test void requesterAvailabilityRespectsTransport() { - requesterLeaseTracker.handleLeaseFrame(leaseFrame(5_000, 1, Unpooled.EMPTY_BUFFER)); - double unavailable = 0.0; - connection.setAvailability(unavailable); - Assertions.assertThat(rSocketRequester.availability()).isCloseTo(unavailable, offset(1e-2)); + ByteBuf frame = leaseFrame(5_000, 1, Unpooled.EMPTY_BUFFER); + try { + + requesterLeaseTracker.handleLeaseFrame(frame); + double unavailable = 0.0; + connection.setAvailability(unavailable); + Assertions.assertThat(rSocketRequester.availability()).isCloseTo(unavailable, offset(1e-2)); + } finally { + frame.release(); + } } @ParameterizedTest @@ -637,10 +657,14 @@ void sendLease() { .findFirst() .orElseThrow(() -> new IllegalStateException("Lease frame not sent")); - Assertions.assertThat(LeaseFrameCodec.ttl(leaseFrame)).isEqualTo(ttl); - Assertions.assertThat(LeaseFrameCodec.numRequests(leaseFrame)).isEqualTo(numberOfRequests); - Assertions.assertThat(LeaseFrameCodec.metadata(leaseFrame).toString(utf8)) - .isEqualTo(metadataContent); + try { + Assertions.assertThat(LeaseFrameCodec.ttl(leaseFrame)).isEqualTo(ttl); + Assertions.assertThat(LeaseFrameCodec.numRequests(leaseFrame)).isEqualTo(numberOfRequests); + Assertions.assertThat(LeaseFrameCodec.metadata(leaseFrame).toString(utf8)) + .isEqualTo(metadataContent); + } finally { + leaseFrame.release(); + } } // @Test 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 8c662d67d..966fd65f2 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java @@ -17,8 +17,11 @@ import static org.assertj.core.api.Assertions.assertThat; +import io.rsocket.FrameAssert; import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; import io.rsocket.test.util.TestClientTransport; +import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.transport.ClientTransport; import java.io.UncheckedIOException; import java.time.Duration; @@ -49,27 +52,44 @@ public void shouldBeASharedReconnectableInstanceOfRSocketMono() throws Interrupt RSocket rSocket1 = rSocketMono.block(); RSocket rSocket2 = rSocketMono.block(); + FrameAssert.assertThat(testClientTransport[0].testConnection().awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + assertThat(rSocket1).isEqualTo(rSocket2); testClientTransport[0].testConnection().dispose(); + rSocket1.onClose().block(Duration.ofSeconds(1)); + testClientTransport[0].alloc().assertHasNoLeaks(); testClientTransport[0] = new TestClientTransport(); RSocket rSocket3 = rSocketMono.block(); RSocket rSocket4 = rSocketMono.block(); + FrameAssert.assertThat(testClientTransport[0].testConnection().awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + assertThat(rSocket3).isEqualTo(rSocket4).isNotEqualTo(rSocket2); + + testClientTransport[0].testConnection().dispose(); + rSocket3.onClose().block(Duration.ofSeconds(1)); + testClientTransport[0].alloc().assertHasNoLeaks(); } @Test - @SuppressWarnings({"rawtype", "unchecked"}) + @SuppressWarnings({"rawtype"}) public void shouldBeRetrieableConnectionSharedReconnectableInstanceOfRSocketMono() { ClientTransport transport = Mockito.mock(ClientTransport.class); + TestClientTransport transport1 = new TestClientTransport(); Mockito.when(transport.connect()) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) - .thenReturn(new TestClientTransport().connect()); + .thenReturn(transport1.connect()); Mono rSocketMono = RSocketConnector.create() .reconnect( @@ -87,19 +107,29 @@ public void shouldBeRetrieableConnectionSharedReconnectableInstanceOfRSocketMono UncheckedIOException.class, UncheckedIOException.class, UncheckedIOException.class); + + FrameAssert.assertThat(transport1.testConnection().awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + + transport1.testConnection().dispose(); + rSocket1.onClose().block(Duration.ofSeconds(1)); + transport1.alloc().assertHasNoLeaks(); } @Test - @SuppressWarnings({"rawtype", "unchecked"}) + @SuppressWarnings({"rawtype"}) public void shouldBeExaustedRetrieableConnectionSharedReconnectableInstanceOfRSocketMono() { ClientTransport transport = Mockito.mock(ClientTransport.class); + TestClientTransport transport1 = new TestClientTransport(); Mockito.when(transport.connect()) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) - .thenReturn(new TestClientTransport().connect()); + .thenReturn(transport1.connect()); Mono rSocketMono = RSocketConnector.create() .reconnect( @@ -121,17 +151,38 @@ public void shouldBeExaustedRetrieableConnectionSharedReconnectableInstanceOfRSo UncheckedIOException.class, UncheckedIOException.class, UncheckedIOException.class); + + transport1.alloc().assertHasNoLeaks(); } @Test public void shouldBeNotBeASharedReconnectableInstanceOfRSocketMono() { - - Mono rSocketMono = RSocketConnector.connectWith(new TestClientTransport()); + TestClientTransport transport = new TestClientTransport(); + Mono rSocketMono = RSocketConnector.connectWith(transport); RSocket rSocket1 = rSocketMono.block(); + TestDuplexConnection connection1 = transport.testConnection(); + + FrameAssert.assertThat(connection1.awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + RSocket rSocket2 = rSocketMono.block(); + TestDuplexConnection connection2 = transport.testConnection(); assertThat(rSocket1).isNotEqualTo(rSocket2); + + FrameAssert.assertThat(connection2.awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + + connection1.dispose(); + connection2.dispose(); + rSocket1.onClose().block(Duration.ofSeconds(1)); + rSocket2.onClose().block(Duration.ofSeconds(1)); + transport.alloc().assertHasNoLeaks(); } @SafeVarargs 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 d736ae190..01eb998c7 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -21,6 +21,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.CharsetUtil; +import io.rsocket.FrameAssert; import io.rsocket.RSocket; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.FrameHeaderCodec; @@ -37,6 +38,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.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -64,6 +66,11 @@ class RSocketRequesterSubscribersTest { protected Sinks.Empty thisClosedSink; protected Sinks.Empty otherClosedSink; + @AfterEach + void tearDownAndCheckNoLeaks() { + allocator.assertHasNoLeaks(); + } + @BeforeEach void setUp() { allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); @@ -89,6 +96,7 @@ void setUp() { @ParameterizedTest @MethodSource("allInteractions") + @SuppressWarnings({"rawtypes", "unchecked"}) void singleSubscriber(Function> interaction, FrameType requestType) { Flux response = Flux.from(interaction.apply(rSocketRequester)); @@ -105,7 +113,11 @@ void singleSubscriber(Function> interaction, FrameType req assertSubscriberA.assertTerminated(); assertSubscriberB.assertTerminated(); - Assertions.assertThat(requestFramesCount(connection.getSent())).isEqualTo(1); + FrameAssert.assertThat(connection.pollFrame()).typeOf(requestType).hasNoLeaks(); + + if (requestType == FrameType.REQUEST_CHANNEL) { + FrameAssert.assertThat(connection.pollFrame()).typeOf(FrameType.COMPLETE).hasNoLeaks(); + } } @ParameterizedTest diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java index 6ccff3701..5cfa76a1c 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java @@ -1,15 +1,19 @@ package io.rsocket.core; +import io.rsocket.FrameAssert; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.core.RSocketRequesterTest.ClientSocketRule; +import io.rsocket.frame.FrameType; import io.rsocket.util.EmptyPayload; import java.nio.channels.ClosedChannelException; import java.time.Duration; import java.util.Arrays; import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -25,15 +29,23 @@ public void setup() { rule.init(); } + @AfterEach + public void tearDownAndCheckNoLeaks() { + rule.assertHasNoLeaks(); + } + @ParameterizedTest @MethodSource("rsocketInteractions") public void testCurrentStreamIsTerminatedOnConnectionClose( - Function> interaction) { + FrameType requestType, Function> interaction) { RSocketRequester rSocket = rule.socket; - Mono.delay(Duration.ofSeconds(1)).doOnNext(v -> rule.connection.dispose()).subscribe(); - StepVerifier.create(interaction.apply(rSocket)) + .then( + () -> { + FrameAssert.assertThat(rule.connection.pollFrame()).typeOf(requestType).hasNoLeaks(); + }) + .then(() -> rule.connection.dispose()) .expectError(ClosedChannelException.class) .verify(Duration.ofSeconds(5)); } @@ -41,7 +53,7 @@ public void testCurrentStreamIsTerminatedOnConnectionClose( @ParameterizedTest @MethodSource("rsocketInteractions") public void testSubsequentStreamIsTerminatedAfterConnectionClose( - Function> interaction) { + FrameType requestType, Function> interaction) { RSocketRequester rSocket = rule.socket; rule.connection.dispose(); @@ -50,46 +62,51 @@ public void testSubsequentStreamIsTerminatedAfterConnectionClose( .verify(Duration.ofSeconds(5)); } - public static Iterable>> rsocketInteractions() { + public static Iterable rsocketInteractions() { EmptyPayload payload = EmptyPayload.INSTANCE; - Publisher payloadStream = Flux.just(payload); - Function> resp = - new Function>() { - @Override - public Mono apply(RSocket rSocket) { - return rSocket.requestResponse(payload); - } + Arguments resp = + Arguments.of( + FrameType.REQUEST_RESPONSE, + new Function>() { + @Override + public Mono apply(RSocket rSocket) { + return rSocket.requestResponse(payload); + } - @Override - public String toString() { - return "Request Response"; - } - }; - Function> stream = - new Function>() { - @Override - public Flux apply(RSocket rSocket) { - return rSocket.requestStream(payload); - } + @Override + public String toString() { + return "Request Response"; + } + }); + Arguments stream = + Arguments.of( + FrameType.REQUEST_STREAM, + new Function>() { + @Override + public Flux apply(RSocket rSocket) { + return rSocket.requestStream(payload); + } - @Override - public String toString() { - return "Request Stream"; - } - }; - Function> channel = - new Function>() { - @Override - public Flux apply(RSocket rSocket) { - return rSocket.requestChannel(payloadStream); - } + @Override + public String toString() { + return "Request Stream"; + } + }); + Arguments channel = + Arguments.of( + FrameType.REQUEST_CHANNEL, + new Function>() { + @Override + public Flux apply(RSocket rSocket) { + return rSocket.requestChannel(Flux.never().startWith(payload)); + } - @Override - public String toString() { - return "Request Channel"; - } - }; + @Override + public String toString() { + return "Request Channel"; + } + }); return Arrays.asList(resp, stream, channel); } 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 5861a459a..a1199f698 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -122,6 +122,7 @@ public void setUp() throws Throwable { public void tearDown() { Hooks.resetOnErrorDropped(); Hooks.resetOnNextDropped(); + rule.assertHasNoLeaks(); } @Test @@ -765,16 +766,21 @@ private static Stream racingCases() { @Test public void simpleOnDiscardRequestChannelTest() { AssertSubscriber assertSubscriber = AssertSubscriber.create(1); - TestPublisher testPublisher = TestPublisher.create(); + Sinks.Many testPublisher = Sinks.many().unicast().onBackpressureBuffer(); - Flux payloadFlux = rule.socket.requestChannel(testPublisher); + Flux payloadFlux = rule.socket.requestChannel(testPublisher.asFlux()); payloadFlux.subscribe(assertSubscriber); - testPublisher.next( - ByteBufPayload.create("d", "m"), - ByteBufPayload.create("d1", "m1"), - ByteBufPayload.create("d2", "m2")); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d"), ByteBufUtil.writeUtf8(rule.alloc(), "m"))); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d1"), ByteBufUtil.writeUtf8(rule.alloc(), "m1"))); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d2"), ByteBufUtil.writeUtf8(rule.alloc(), "m2"))); assertSubscriber.cancel(); @@ -787,16 +793,23 @@ public void simpleOnDiscardRequestChannelTest() { public void simpleOnDiscardRequestChannelTest2() { ByteBufAllocator allocator = rule.alloc(); AssertSubscriber assertSubscriber = AssertSubscriber.create(1); - TestPublisher testPublisher = TestPublisher.create(); + Sinks.Many testPublisher = Sinks.many().unicast().onBackpressureBuffer(); - Flux payloadFlux = rule.socket.requestChannel(testPublisher); + Flux payloadFlux = rule.socket.requestChannel(testPublisher.asFlux()); payloadFlux.subscribe(assertSubscriber); - testPublisher.next(ByteBufPayload.create("d", "m")); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d"), ByteBufUtil.writeUtf8(rule.alloc(), "m"))); int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); - testPublisher.next(ByteBufPayload.create("d1", "m1"), ByteBufPayload.create("d2", "m2")); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d1"), ByteBufUtil.writeUtf8(rule.alloc(), "m1"))); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d2"), ByteBufUtil.writeUtf8(rule.alloc(), "m2"))); rule.connection.addToReceivedBuffer( ErrorFrameCodec.encode( @@ -820,7 +833,7 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( switch (frameType) { case REQUEST_FNF: response = - testPublisher.mono().flatMap(p -> rule.socket.fireAndForget(p).then(Mono.empty())); + testPublisher.mono().flatMap(p -> rule.socket.fireAndForget(p)).then(Mono.empty()); break; case REQUEST_RESPONSE: response = testPublisher.mono().flatMap(p -> rule.socket.requestResponse(p)); @@ -836,7 +849,7 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( } response.subscribe(assertSubscriber); - testPublisher.next(ByteBufPayload.create("d")); + testPublisher.next(ByteBufPayload.create(ByteBufUtil.writeUtf8(rule.alloc(), "d"))); int streamId = rule.getStreamIdForRequestType(frameType); @@ -870,7 +883,7 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( } for (int i = 1; i < framesCnt; i++) { - testPublisher.next(ByteBufPayload.create("d" + i)); + testPublisher.next(ByteBufPayload.create(ByteBufUtil.writeUtf8(rule.alloc(), "d" + i))); } assertThat(rule.connection.getSent()) @@ -908,7 +921,10 @@ static Stream encodeDecodePayloadCases() { @MethodSource("refCntCases") public void ensureSendsErrorOnIllegalRefCntPayload( BiFunction> sourceProducer) { - Payload invalidPayload = ByteBufPayload.create("test", "test"); + Payload invalidPayload = + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "test"), + ByteBufUtil.writeUtf8(rule.alloc(), "test")); invalidPayload.release(); Publisher source = sourceProducer.apply(invalidPayload, rule); @@ -926,7 +942,8 @@ private static Stream>> refCn (p, clientSocketRule) -> clientSocketRule.socket.requestChannel(Mono.just(p)), (p, clientSocketRule) -> { Flux.from(clientSocketRule.connection.getSentAsPublisher()) - .filter(bb -> FrameHeaderCodec.frameType(bb) == REQUEST_CHANNEL) + .filter(bb -> frameType(bb) == REQUEST_CHANNEL) + .doOnDiscard(ByteBuf.class, ReferenceCounted::release) .subscribe( bb -> { clientSocketRule.connection.addToReceivedBuffer( 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 cbfc05ea3..4f689e396 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -113,6 +113,7 @@ public void setUp() { public void tearDown() { Hooks.resetOnErrorDropped(); Hooks.resetOnNextDropped(); + rule.assertHasNoLeaks(); } @Test @@ -146,9 +147,7 @@ public Mono requestResponse(Payload payload) { }); rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE); testPublisher.complete(); - assertThat(frameType(rule.connection.awaitFrame())) - .describedAs("Unexpected frame sent.") - .isIn(FrameType.COMPLETE, FrameType.NEXT_COMPLETE); + FrameAssert.assertThat(rule.connection.awaitFrame()).typeOf(FrameType.COMPLETE).hasNoLeaks(); testPublisher.assertWasNotCancelled(); } @@ -158,9 +157,10 @@ public void testHandlerEmitsError() { final int streamId = 4; rule.prefetch = 1; rule.sendRequest(streamId, FrameType.REQUEST_STREAM); - assertThat(frameType(rule.connection.awaitFrame())) - .describedAs("Unexpected frame sent.") - .isEqualTo(FrameType.ERROR); + FrameAssert.assertThat(rule.connection.awaitFrame()) + .typeOf(FrameType.ERROR) + .hasData("Request-Stream not implemented.") + .hasNoLeaks(); } @Test 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 fd588cda3..90e881257 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java @@ -1,5 +1,8 @@ package io.rsocket.core; +import io.rsocket.Closeable; +import io.rsocket.FrameAssert; +import io.rsocket.frame.FrameType; import io.rsocket.test.util.TestClientTransport; import io.rsocket.test.util.TestServerTransport; import org.assertj.core.api.Assertions; @@ -16,12 +19,18 @@ public void serverErrorsWithEnabledFragmentationOnInsufficientMtu() { @Test public void serverSucceedsWithEnabledFragmentationOnSufficientMtu() { - RSocketServer.create().fragment(100).bind(new TestServerTransport()).block(); + TestServerTransport transport = new TestServerTransport(); + Closeable closeable = RSocketServer.create().fragment(100).bind(transport).block(); + closeable.dispose(); + transport.alloc().assertHasNoLeaks(); } @Test public void serverSucceedsWithDisabledFragmentation() { - RSocketServer.create().bind(new TestServerTransport()).block(); + TestServerTransport transport = new TestServerTransport(); + Closeable closeable = RSocketServer.create().bind(transport).block(); + closeable.dispose(); + transport.alloc().assertHasNoLeaks(); } @Test @@ -33,11 +42,23 @@ public void clientErrorsWithEnabledFragmentationOnInsufficientMtu() { @Test public void clientSucceedsWithEnabledFragmentationOnSufficientMtu() { - RSocketConnector.create().fragment(100).connect(new TestClientTransport()).block(); + TestClientTransport transport = new TestClientTransport(); + RSocketConnector.create().fragment(100).connect(transport).block(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .typeOf(FrameType.SETUP) + .hasNoLeaks(); + transport.testConnection().dispose(); + transport.alloc().assertHasNoLeaks(); } @Test public void clientSucceedsWithDisabledFragmentation() { - RSocketConnector.connectWith(new TestClientTransport()).block(); + TestClientTransport transport = new TestClientTransport(); + RSocketConnector.connectWith(transport).block(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .typeOf(FrameType.SETUP) + .hasNoLeaks(); + transport.testConnection().dispose(); + transport.alloc().assertHasNoLeaks(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java index 24bf95215..0b5ca38f7 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java @@ -20,6 +20,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.rsocket.Closeable; import io.rsocket.FrameAssert; import io.rsocket.RSocket; import io.rsocket.frame.FrameType; @@ -60,13 +61,14 @@ public void unexpectedFramesBeforeSetupFrame() { .hasData("SETUP or RESUME frame must be received before any others") .hasStreamIdZero() .hasNoLeaks(); + duplexConnection.alloc().assertHasNoLeaks(); } @Test public void timeoutOnNoFirstFrame() { final VirtualTimeScheduler scheduler = VirtualTimeScheduler.getOrSet(); + TestServerTransport transport = new TestServerTransport(); try { - TestServerTransport transport = new TestServerTransport(); RSocketServer.create().maxTimeToFirstFrame(Duration.ofMinutes(2)).bind(transport).block(); final TestDuplexConnection duplexConnection = transport.connect(); @@ -84,6 +86,7 @@ public void timeoutOnNoFirstFrame() { FrameAssert.assertThat(duplexConnection.pollFrame()).isNull(); } finally { + transport.alloc().assertHasNoLeaks(); VirtualTimeScheduler.reset(); } } @@ -128,14 +131,15 @@ public void unexpectedFramesBeforeSetup() { Sinks.Empty connectedSink = Sinks.empty(); TestServerTransport transport = new TestServerTransport(); - RSocketServer.create() - .acceptor( - (setup, sendingSocket) -> { - connectedSink.tryEmitEmpty(); - return Mono.just(new RSocket() {}); - }) - .bind(transport) - .block(); + Closeable server = + RSocketServer.create() + .acceptor( + (setup, sendingSocket) -> { + connectedSink.tryEmitEmpty(); + return Mono.just(new RSocket() {}); + }) + .bind(transport) + .block(); byte[] bytes = new byte[16_000_000]; new Random().nextBytes(bytes); @@ -153,5 +157,11 @@ public void unexpectedFramesBeforeSetup() { assertThat(connectedSink.scan(Scannable.Attr.TERMINATED)) .as("Connection should not succeed") .isFalse(); + FrameAssert.assertThat(connection.pollFrame()) + .hasStreamIdZero() + .hasData("SETUP or RESUME frame must be received before any others") + .hasNoLeaks(); + server.dispose(); + transport.alloc().assertHasNoLeaks(); } } 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 98cc94087..e01e6ebdc 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,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicReference; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -56,6 +57,11 @@ public void setup() { rule.init(); } + @AfterEach + public void tearDownAndCheckOnLeaks() { + rule.alloc().assertHasNoLeaks(); + } + @Test public void rsocketDisposalShouldEndupWithNoErrorsOnClose() { RSocket requestHandlingRSocket = 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 e85c5856e..87c3a865f 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -58,10 +58,12 @@ void responderRejectSetup() { ByteBuf sentFrame = transport.awaitSent(); assertThat(FrameHeaderCodec.frameType(sentFrame)).isEqualTo(FrameType.ERROR); RuntimeException error = Exceptions.from(0, sentFrame); + sentFrame.release(); assertThat(errorMsg).isEqualTo(error.getMessage()); assertThat(error).isInstanceOf(RejectedSetupException.class); RSocket acceptorSender = acceptor.senderRSocket().block(); assertThat(acceptorSender.isDisposed()).isTrue(); + transport.allocator.assertHasNoLeaks(); } @Test @@ -104,6 +106,7 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { .verify(Duration.ofSeconds(5)); assertThat(rSocket.isDisposed()).isTrue(); + allocator.assertHasNoLeaks(); } @Test @@ -138,6 +141,7 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { .expectErrorMatches( err -> err instanceof RejectedSetupException && "error".equals(err.getMessage())) .verify(Duration.ofSeconds(5)); + allocator.assertHasNoLeaks(); } private static class RejectingAcceptor implements SocketAcceptor { 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 ff7e4ff17..a316aed8b 100644 --- a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java @@ -43,14 +43,18 @@ final class ExceptionsTest { void fromApplicationException() { ByteBuf byteBuf = createErrorFrame(1, APPLICATION_ERROR, "test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(ApplicationErrorException.class) - .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"); + try { + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(ApplicationErrorException.class) + .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"); + } finally { + byteBuf.release(); + } } @DisplayName("from returns CanceledException") @@ -58,28 +62,37 @@ void fromApplicationException() { void fromCanceledException() { ByteBuf byteBuf = createErrorFrame(1, CANCELED, "test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(CanceledException.class) - .hasMessage("test-message"); + try { - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", CANCELED, "test-message"); + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(CanceledException.class) + .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"); + } finally { + byteBuf.release(); + } } @DisplayName("from returns ConnectionCloseException") @Test void fromConnectionCloseException() { ByteBuf byteBuf = createErrorFrame(0, CONNECTION_CLOSE, "test-message"); + try { - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(ConnectionCloseException.class) - .hasMessage("test-message"); + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(ConnectionCloseException.class) + .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"); + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", CONNECTION_CLOSE, "test-message"); + } finally { + byteBuf.release(); + } } @DisplayName("from returns ConnectionErrorException") @@ -87,116 +100,146 @@ void fromConnectionCloseException() { void fromConnectionErrorException() { ByteBuf byteBuf = createErrorFrame(0, CONNECTION_ERROR, "test-message"); - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(ConnectionErrorException.class) - .hasMessage("test-message"); + try { - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage( - "Invalid Error frame in Stream ID 1: 0x%08X '%s'", CONNECTION_ERROR, "test-message"); + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(ConnectionErrorException.class) + .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"); + } finally { + byteBuf.release(); + } } @DisplayName("from returns IllegalArgumentException if error frame has illegal error code") @Test void fromIllegalErrorFrame() { ByteBuf byteBuf = createErrorFrame(0, 0x00000000, "test-message"); + try { - 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(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); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 1: 0x%08X '%s'", 0x00000000, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns InvalidException") @Test void fromInvalidException() { ByteBuf byteBuf = createErrorFrame(1, INVALID, "test-message"); + try { + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(InvalidException.class) + .hasMessage("test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(InvalidException.class) - .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); + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", INVALID, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns InvalidSetupException") @Test void fromInvalidSetupException() { ByteBuf byteBuf = createErrorFrame(0, INVALID_SETUP, "test-message"); + try { + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(InvalidSetupException.class) + .hasMessage("test-message"); - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(InvalidSetupException.class) - .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); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", INVALID_SETUP, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns RejectedException") @Test void fromRejectedException() { ByteBuf byteBuf = createErrorFrame(1, REJECTED, "test-message"); + try { - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(RejectedException.class) - .withFailMessage("test-message"); + 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); + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", REJECTED, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns RejectedResumeException") @Test void fromRejectedResumeException() { ByteBuf byteBuf = createErrorFrame(0, REJECTED_RESUME, "test-message"); + try { - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(RejectedResumeException.class) - .hasMessage("test-message"); + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(RejectedResumeException.class) + .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); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", REJECTED_RESUME, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns RejectedSetupException") @Test void fromRejectedSetupException() { ByteBuf byteBuf = createErrorFrame(0, REJECTED_SETUP, "test-message"); + try { - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(RejectedSetupException.class) - .withFailMessage("test-message"); + 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); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", REJECTED_SETUP, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns UnsupportedSetupException") @Test void fromUnsupportedSetupException() { ByteBuf byteBuf = createErrorFrame(0, UNSUPPORTED_SETUP, "test-message"); + try { + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(UnsupportedSetupException.class) + .hasMessage("test-message"); - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(UnsupportedSetupException.class) - .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); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", UNSUPPORTED_SETUP, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns CustomRSocketException") @@ -210,15 +253,18 @@ void fromCustomRSocketException() { : ThreadLocalRandom.current() .nextInt(ErrorFrameCodec.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); - byteBuf.release(); + try { + 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); + } finally { + byteBuf.release(); + } } } diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java index c838d704c..a35e89391 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java @@ -33,7 +33,10 @@ class LoadbalanceRSocketClientTest { public static final Duration LONG_DURATION = Duration.ofMillis(75); private static final Publisher SOURCE = - Flux.interval(SHORT_DURATION).map(String::valueOf).map(DefaultPayload::create); + Flux.interval(SHORT_DURATION) + .onBackpressureBuffer() + .map(String::valueOf) + .map(DefaultPayload::create); private static final Mono PROGRESSING_HANDLER = Mono.just( diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java index fcd3ae4a9..c1b509297 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java @@ -20,7 +20,6 @@ import io.rsocket.RaceTestConstants; import io.rsocket.core.RSocketConnector; import io.rsocket.internal.subscriber.AssertSubscriber; -import io.rsocket.plugins.RSocketInterceptor; import io.rsocket.test.util.TestClientTransport; import io.rsocket.transport.ClientTransport; import io.rsocket.util.EmptyPayload; @@ -71,10 +70,11 @@ public Mono fireAndForget(Payload payload) { return Mono.empty(); } }; - final RSocketConnector rSocketConnectorMock = - RSocketConnector.create() - .interceptors( - ir -> ir.forRequester((RSocketInterceptor) socket -> new TestRSocket(rSocket))); + + final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + final ClientTransport mockTransport1 = Mockito.mock(ClientTransport.class); + Mockito.when(rSocketConnectorMock.connect(Mockito.any(ClientTransport.class))) + .then(im -> Mono.just(new TestRSocket(rSocket))); final List collectionOfDestination1 = Collections.singletonList(LoadbalanceTarget.from("1", mockTransport)); diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataCodecTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataCodecTest.java index 3ce07729d..a4e8fb2d8 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataCodecTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataCodecTest.java @@ -23,12 +23,22 @@ import io.netty.buffer.*; import io.netty.util.CharsetUtil; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.test.util.ByteBufUtils; import io.rsocket.util.NumberUtils; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; class CompositeMetadataCodecTest { + final LeaksTrackingByteBufAllocator testAllocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + + @AfterEach + void tearDownAndCheckForLeaks() { + testAllocator.assertHasNoLeaks(); + } + static String byteToBitsString(byte b) { return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'); } @@ -48,17 +58,14 @@ void customMimeHeaderLatin1_encodingFails() { assertThatIllegalArgumentException() .isThrownBy( - () -> - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + () -> CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeNotAscii, 0)) .withMessage("custom mime type must be US_ASCII characters only"); } @Test void customMimeHeaderLength0_encodingFails() { assertThatIllegalArgumentException() - .isThrownBy( - () -> CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "", 0)) + .isThrownBy(() -> CompositeMetadataCodec.encodeMetadataHeader(testAllocator, "", 0)) .withMessage( "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); } @@ -70,8 +77,7 @@ void customMimeHeaderLength127() { builder.append('a'); } String mimeString = builder.toString(); - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeString, 0); // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111110"); @@ -99,6 +105,7 @@ void customMimeHeaderLength127() { .hasToString(mimeString); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -108,8 +115,7 @@ void customMimeHeaderLength128() { builder.append('a'); } String mimeString = builder.toString(); - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeString, 0); // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111111"); @@ -137,6 +143,7 @@ void customMimeHeaderLength128() { .hasToString(mimeString); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -148,9 +155,7 @@ void customMimeHeaderLength129_encodingFails() { assertThatIllegalArgumentException() .isThrownBy( - () -> - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, builder.toString(), 0)) + () -> CompositeMetadataCodec.encodeMetadataHeader(testAllocator, builder.toString(), 0)) .withMessage( "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); } @@ -158,8 +163,7 @@ void customMimeHeaderLength129_encodingFails() { @Test void customMimeHeaderLengthOne() { String mimeString = "w"; - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeString, 0); // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000000"); @@ -185,13 +189,13 @@ void customMimeHeaderLengthOne() { .hasToString(mimeString); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test void customMimeHeaderLengthTwo() { String mimeString = "ww"; - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeString, 0); // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000001"); @@ -219,6 +223,7 @@ void customMimeHeaderLengthTwo() { .hasToString(mimeString); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -227,9 +232,7 @@ void customMimeHeaderUtf8_encodingFails() { "mime/tyࠒe"; // this is the SAMARITAN LETTER QUF u+0812 represented on 3 bytes assertThatIllegalArgumentException() .isThrownBy( - () -> - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + () -> CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeNotAscii, 0)) .withMessage("custom mime type must be US_ASCII characters only"); } @@ -317,72 +320,73 @@ void decodeTypeSkipsFirstByte() { @Test void encodeMetadataCustomTypeDelegates() { - ByteBuf expected = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo", 2); + ByteBuf expected = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, "foo", 2); - CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf test = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadata( - test, ByteBufAllocator.DEFAULT, "foo", ByteBufUtils.getRandomByteBuf(2)); + test, testAllocator, "foo", ByteBufUtils.getRandomByteBuf(2)); assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + test.release(); + expected.release(); } @Test void encodeMetadataKnownTypeDelegates() { ByteBuf expected = CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, - WellKnownMimeType.APPLICATION_OCTET_STREAM.getIdentifier(), - 2); + testAllocator, WellKnownMimeType.APPLICATION_OCTET_STREAM.getIdentifier(), 2); - CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf test = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadata( test, - ByteBufAllocator.DEFAULT, + testAllocator, WellKnownMimeType.APPLICATION_OCTET_STREAM, ByteBufUtils.getRandomByteBuf(2)); assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + test.release(); + expected.release(); } @Test void encodeMetadataReservedTypeDelegates() { - ByteBuf expected = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, (byte) 120, 2); + ByteBuf expected = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, (byte) 120, 2); - CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf test = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadata( - test, ByteBufAllocator.DEFAULT, (byte) 120, ByteBufUtils.getRandomByteBuf(2)); + test, testAllocator, (byte) 120, ByteBufUtils.getRandomByteBuf(2)); assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + test.release(); + expected.release(); } @Test void encodeTryCompressWithCompressableType() { ByteBuf metadata = ByteBufUtils.getRandomByteBuf(2); - CompositeByteBuf target = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf target = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadataWithCompression( - target, - UnpooledByteBufAllocator.DEFAULT, - WellKnownMimeType.APPLICATION_AVRO.getString(), - metadata); + target, testAllocator, WellKnownMimeType.APPLICATION_AVRO.getString(), metadata); assertThat(target.readableBytes()).as("readableBytes 1 + 3 + 2").isEqualTo(6); + target.release(); } @Test void encodeTryCompressWithCustomType() { ByteBuf metadata = ByteBufUtils.getRandomByteBuf(2); - CompositeByteBuf target = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf target = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadataWithCompression( - target, UnpooledByteBufAllocator.DEFAULT, "custom/example", metadata); + target, testAllocator, "custom/example", metadata); assertThat(target.readableBytes()).as("readableBytes 1 + 14 + 3 + 2").isEqualTo(20); + target.release(); } @Test @@ -390,19 +394,20 @@ void hasEntry() { WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; CompositeByteBuf buffer = - Unpooled.compositeBuffer() + testAllocator + .compositeBuffer() .addComponent( true, - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0)) + CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mime.getIdentifier(), 0)) .addComponent( true, CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0)); + testAllocator, mime.getIdentifier(), 0)); assertThat(CompositeMetadataCodec.hasEntry(buffer, 0)).isTrue(); assertThat(CompositeMetadataCodec.hasEntry(buffer, 4)).isTrue(); assertThat(CompositeMetadataCodec.hasEntry(buffer, 8)).isFalse(); + buffer.release(); } @Test @@ -417,8 +422,7 @@ void isWellKnownMimeType() { @Test void knownMimeHeader120_reserved() { byte mime = (byte) 120; - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mime, 0); assertThat(mime) .as("smoke test RESERVED_120 unsigned 7 bits representation") @@ -443,6 +447,7 @@ void knownMimeHeader120_reserved() { assertThat(decodeMimeIdFromMimeBuffer(header)).as("decoded mime id").isEqualTo(mime); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -453,8 +458,7 @@ void knownMimeHeader127_compositeMetadata() { .isEqualTo((byte) 127) .isEqualTo((byte) 0b01111111); ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); + CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mime.getIdentifier(), 0); assertThat(toHeaderBits(encoded)) .startsWith("1") @@ -480,6 +484,7 @@ void knownMimeHeader127_compositeMetadata() { .isEqualTo(mime.getIdentifier()); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -490,8 +495,7 @@ void knownMimeHeaderZero_avro() { .isEqualTo((byte) 0) .isEqualTo((byte) 0b00000000); ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); + CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mime.getIdentifier(), 0); assertThat(toHeaderBits(encoded)) .startsWith("1") @@ -517,6 +521,7 @@ void knownMimeHeaderZero_avro() { .isEqualTo(mime.getIdentifier()); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java index 9227bcaca..5c8d40306 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java @@ -30,20 +30,28 @@ public class MimeTypeMetadataCodecTest { public void wellKnownMimeType() { WellKnownMimeType mimeType = WellKnownMimeType.APPLICATION_HESSIAN; ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, mimeType); - List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); + try { + List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); - assertThat(mimeTypes.size()).isEqualTo(1); - assertThat(WellKnownMimeType.fromString(mimeTypes.get(0))).isEqualTo(mimeType); + assertThat(mimeTypes.size()).isEqualTo(1); + assertThat(WellKnownMimeType.fromString(mimeTypes.get(0))).isEqualTo(mimeType); + } finally { + byteBuf.release(); + } } @Test public void customMimeType() { String mimeType = "aaa/bb"; ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, mimeType); - List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); + try { + List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); - assertThat(mimeTypes.size()).isEqualTo(1); - assertThat(mimeTypes.get(0)).isEqualTo(mimeType); + assertThat(mimeTypes.size()).isEqualTo(1); + assertThat(mimeTypes.get(0)).isEqualTo(mimeType); + } finally { + byteBuf.release(); + } } @Test @@ -51,6 +59,10 @@ public void multipleMimeTypes() { List mimeTypes = Lists.newArrayList("aaa/bbb", "application/x-hessian"); ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, mimeTypes); - assertThat(MimeTypeMetadataCodec.decode(byteBuf)).isEqualTo(mimeTypes); + try { + assertThat(MimeTypeMetadataCodec.decode(byteBuf)).isEqualTo(mimeTypes); + } finally { + byteBuf.release(); + } } } diff --git a/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java b/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java index 2bb718ef7..9a19050f9 100644 --- a/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java @@ -1,16 +1,19 @@ package io.rsocket.plugins; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.Closeable; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.frame.FrameType; 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.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.Function; @@ -29,6 +32,9 @@ public class RequestInterceptorTest { @ParameterizedTest @ValueSource(booleans = {true, false}) void interceptorShouldBeInstalledProperlyOnTheClientRequesterSide(boolean errorOutcome) { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); final Closeable closeable = RSocketServer.create( SocketAcceptor.with( @@ -69,7 +75,7 @@ public Flux requestChannel(Publisher payloads) { ir.forRequestsInRequester( (Function) (__) -> testRequestInterceptor)) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -130,6 +136,7 @@ public Flux requestChannel(Publisher payloads) { } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @@ -137,6 +144,10 @@ public Flux requestChannel(Publisher payloads) { @ValueSource(booleans = {true, false}) void interceptorShouldBeInstalledProperlyOnTheClientResponderSide(boolean errorOutcome) throws InterruptedException { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + CountDownLatch latch = new CountDownLatch(1); final Closeable closeable = RSocketServer.create( @@ -209,7 +220,7 @@ public Flux requestChannel(Publisher payloads) { ir.forRequestsInResponder( (Function) (__) -> testRequestInterceptor)) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -253,12 +264,17 @@ public Flux requestChannel(Publisher payloads) { } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @ParameterizedTest @ValueSource(booleans = {true, false}) void interceptorShouldBeInstalledProperlyOnTheServerRequesterSide(boolean errorOutcome) { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); final Closeable closeable = RSocketServer.create( @@ -297,7 +313,9 @@ public Flux requestChannel(Publisher payloads) { (__) -> testRequestInterceptor)) .bindNow(LocalServerTransport.create("test")); final RSocket rSocket = - RSocketConnector.create().connect(LocalClientTransport.create("test")).block(); + RSocketConnector.create() + .connect(LocalClientTransport.create("test", byteBufAllocator)) + .block(); try { rSocket @@ -357,6 +375,7 @@ public Flux requestChannel(Publisher payloads) { } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @@ -364,6 +383,10 @@ public Flux requestChannel(Publisher payloads) { @ValueSource(booleans = {true, false}) void interceptorShouldBeInstalledProperlyOnTheServerResponderSide(boolean errorOutcome) throws InterruptedException { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + CountDownLatch latch = new CountDownLatch(1); final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); final Closeable closeable = @@ -437,7 +460,7 @@ public Flux requestChannel(Publisher payloads) { : Flux.from(payloads); } })) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -481,11 +504,16 @@ public Flux requestChannel(Publisher payloads) { } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @Test void ensuresExceptionInTheInterceptorIsHandledProperly() { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + final Closeable closeable = RSocketServer.create( SocketAcceptor.with( @@ -546,7 +574,7 @@ public void dispose() {} ir.forRequestsInRequester( (Function) (__) -> testRequestInterceptor)) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -575,12 +603,17 @@ public void dispose() {} } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @ParameterizedTest @ValueSource(booleans = {true, false}) void shouldSupportMultipleInterceptors(boolean errorOutcome) { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + final Closeable closeable = RSocketServer.create( SocketAcceptor.with( @@ -655,7 +688,7 @@ public void dispose() {} .forRequestsInRequester( (Function) (__) -> testRequestInterceptor2)) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -751,6 +784,7 @@ public void dispose() {} } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } } diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java index bdd46f8c6..8229bf42b 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java @@ -159,6 +159,7 @@ void sessionTimeoutSmokeTest() { assertThat(session.isDisposed()).isTrue(); resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); + keepAliveSupport.dispose(); transport.alloc().assertHasNoLeaks(); } finally { VirtualTimeScheduler.reset(); @@ -291,6 +292,7 @@ void sessionTerminationOnWrongFrameTest() { .as(StepVerifier::create) .expectErrorMessage("RESUME_OK frame must be received before any others") .verify(); + keepAliveSupport.dispose(); transport.alloc().assertHasNoLeaks(); } finally { VirtualTimeScheduler.reset(); @@ -386,6 +388,7 @@ void shouldErrorWithNoRetriesOnErrorFrameTest() { .as(StepVerifier::create) .expectError(RejectedResumeException.class) .verify(); + keepAliveSupport.dispose(); transport.alloc().assertHasNoLeaks(); } finally { VirtualTimeScheduler.reset(); @@ -458,7 +461,7 @@ void shouldTerminateConnectionOnIllegalStateInKeepAliveFrame() { .matches(ReferenceCounted::release); resumableDuplexConnection.onClose().as(StepVerifier::create).expectError().verify(); - + keepAliveSupport.dispose(); transport.alloc().assertHasNoLeaks(); } finally { VirtualTimeScheduler.reset(); diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java index eff65f587..b5625bf8e 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java @@ -22,161 +22,169 @@ public class ServerRSocketSessionTest { @Test void sessionTimeoutSmokeTest() { final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); - final TestClientTransport transport = new TestClientTransport(); - final InMemoryResumableFramesStore framesStore = - new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); - - transport.connect().subscribe(); - - final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection( - "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); - - resumableDuplexConnection.receive().subscribe(); - - final ServerRSocketSession session = - new ServerRSocketSession( - Unpooled.EMPTY_BUFFER, - resumableDuplexConnection, - transport.testConnection(), - framesStore, - Duration.ofMinutes(1), - true); - - final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = - new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); - session.setKeepAliveSupport(keepAliveSupport); - - // connection is active. just advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - // deactivate connection - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // resubscribe so a new connection is generated - transport.connect().subscribe(); - - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); - // timeout should not terminate current connection - assertThat(transport.testConnection().isDisposed()).isFalse(); - - // send RESUME frame - final ByteBuf resumeFrame = - ResumeFrameCodec.encode(transport.alloc(), Unpooled.EMPTY_BUFFER, 0, 0); - session.resumeWith(resumeFrame, transport.testConnection()); - resumeFrame.release(); - - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be terminated - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME_OK) - .matches(ReferenceCounted::release); - - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); - - // disconnects for the second time - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - transport.connect().subscribe(); - - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(61)); - - final ByteBuf resumeFrame1 = - ResumeFrameCodec.encode(transport.alloc(), Unpooled.EMPTY_BUFFER, 0, 0); - session.resumeWith(resumeFrame1, transport.testConnection()); - resumeFrame1.release(); - - // should obtain new connection - assertThat(transport.testConnection().isDisposed()).isTrue(); - // timeout should be still active since no RESUME_OK frame has been received yet - assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); - assertThat(session.isDisposed()).isTrue(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.ERROR) - .matches(ReferenceCounted::release); - - resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); - transport.alloc().assertHasNoLeaks(); + try { + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ServerRSocketSession session = + new ServerRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.testConnection(), + framesStore, + Duration.ofMinutes(1), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + // deactivate connection + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // resubscribe so a new connection is generated + transport.connect().subscribe(); + + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); + // timeout should not terminate current connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + + // send RESUME frame + final ByteBuf resumeFrame = + ResumeFrameCodec.encode(transport.alloc(), Unpooled.EMPTY_BUFFER, 0, 0); + session.resumeWith(resumeFrame, transport.testConnection()); + resumeFrame.release(); + + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be terminated + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME_OK) + .matches(ReferenceCounted::release); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); + + // disconnects for the second time + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + transport.connect().subscribe(); + + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(61)); + + final ByteBuf resumeFrame1 = + ResumeFrameCodec.encode(transport.alloc(), Unpooled.EMPTY_BUFFER, 0, 0); + session.resumeWith(resumeFrame1, transport.testConnection()); + resumeFrame1.release(); + + // should obtain new connection + assertThat(transport.testConnection().isDisposed()).isTrue(); + // timeout should be still active since no RESUME_OK frame has been received yet + assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); + assertThat(session.isDisposed()).isTrue(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.ERROR) + .matches(ReferenceCounted::release); + + resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } } @Test void shouldTerminateConnectionOnIllegalStateInKeepAliveFrame() { final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); - final TestClientTransport transport = new TestClientTransport(); - final InMemoryResumableFramesStore framesStore = - new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); - - transport.connect().subscribe(); - - final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection( - "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); - - resumableDuplexConnection.receive().subscribe(); - - final ServerRSocketSession session = - new ServerRSocketSession( - Unpooled.EMPTY_BUFFER, - resumableDuplexConnection, - transport.testConnection(), - framesStore, - Duration.ofMinutes(1), - true); - - final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = - new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); - keepAliveSupport.resumeState(session); - session.setKeepAliveSupport(keepAliveSupport); - - // connection is active. just advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - final ByteBuf keepAliveFrame = - KeepAliveFrameCodec.encode(transport.alloc(), false, 1529, Unpooled.EMPTY_BUFFER); - keepAliveSupport.receive(keepAliveFrame); - keepAliveFrame.release(); - - assertThat(transport.testConnection().isDisposed()).isTrue(); - // timeout should be terminated - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isTrue(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.ERROR) - .matches(ReferenceCounted::release); - - resumableDuplexConnection.onClose().as(StepVerifier::create).expectError().verify(); - - transport.alloc().assertHasNoLeaks(); + try { + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ServerRSocketSession session = + new ServerRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.testConnection(), + framesStore, + Duration.ofMinutes(1), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + keepAliveSupport.resumeState(session); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + final ByteBuf keepAliveFrame = + KeepAliveFrameCodec.encode(transport.alloc(), false, 1529, Unpooled.EMPTY_BUFFER); + keepAliveSupport.receive(keepAliveFrame); + keepAliveFrame.release(); + + assertThat(transport.testConnection().isDisposed()).isTrue(); + // timeout should be terminated + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isTrue(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.ERROR) + .matches(ReferenceCounted::release); + + resumableDuplexConnection.onClose().as(StepVerifier::create).expectError().verify(); + keepAliveSupport.dispose(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } } } 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 e307627ff..f02bc99a4 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 @@ -6,11 +6,13 @@ import io.rsocket.DuplexConnection; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.transport.ClientTransport; +import java.time.Duration; import reactor.core.publisher.Mono; public class TestClientTransport implements ClientTransport { private final LeaksTrackingByteBufAllocator allocator = - LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "client"); private volatile TestDuplexConnection testDuplexConnection; 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 625f8fcb1..cd96584ed 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java @@ -78,8 +78,6 @@ public void testRangeOfConsumers() { .block(); Flux.range(1, 6).flatMap(i -> consumer("connection number -> " + i)).blockLast(); - System.out.println("here"); - } finally { server.dispose(); } From 6d0738974fc3451508112dfdaeee2e174666e1cc Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Mon, 24 Apr 2023 12:27:07 +0300 Subject: [PATCH 103/116] adds class check for discarded values (#1091) --- .../io/rsocket/core/DefaultRSocketClient.java | 17 +++++----- .../main/java/io/rsocket/core/SendUtils.java | 12 ++++--- .../core/DefaultRSocketClientTests.java | 22 +++++++++++++ .../java/io/rsocket/core/SendUtilsTest.java | 31 +++++++++++++++++++ 4 files changed, 70 insertions(+), 12 deletions(-) create mode 100644 rsocket-core/src/test/java/io/rsocket/core/SendUtilsTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java index 9cd89c0b1..82a02268d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java @@ -46,13 +46,16 @@ */ class DefaultRSocketClient extends ResolvingOperator implements CoreSubscriber, CorePublisher, RSocketClient { - static final Consumer DISCARD_ELEMENTS_CONSUMER = - referenceCounted -> { - if (referenceCounted.refCnt() > 0) { - try { - referenceCounted.release(); - } catch (IllegalReferenceCountException e) { - // ignored + static final Consumer DISCARD_ELEMENTS_CONSUMER = + data -> { + if (data instanceof ReferenceCounted) { + ReferenceCounted referenceCounted = ((ReferenceCounted) data); + if (referenceCounted.refCnt() > 0) { + try { + referenceCounted.release(); + } catch (IllegalReferenceCountException e) { + // ignored + } } } }; diff --git a/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java b/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java index 53d222605..568dada2e 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java +++ b/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java @@ -40,11 +40,13 @@ final class SendUtils { private static final Consumer DROPPED_ELEMENTS_CONSUMER = data -> { - try { - ReferenceCounted referenceCounted = (ReferenceCounted) data; - referenceCounted.release(); - } catch (Throwable e) { - // ignored + if (data instanceof ReferenceCounted) { + try { + ReferenceCounted referenceCounted = (ReferenceCounted) data; + referenceCounted.release(); + } catch (Throwable e) { + // ignored + } } }; diff --git a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java index a8a5f2e58..84576e6ce 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -39,6 +39,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import org.assertj.core.api.Assertions; @@ -49,6 +50,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; import org.reactivestreams.Publisher; import reactor.core.Disposable; import reactor.core.publisher.Flux; @@ -79,6 +81,26 @@ public void setUp() throws Throwable { public void tearDown() { Hooks.resetOnErrorDropped(); Hooks.resetOnNextDropped(); + rule.allocator.assertHasNoLeaks(); + } + + @Test + @SuppressWarnings("unchecked") + void discardElementsConsumerShouldAcceptOtherTypesThanReferenceCounted() { + Consumer discardElementsConsumer = DefaultRSocketClient.DISCARD_ELEMENTS_CONSUMER; + discardElementsConsumer.accept(new Object()); + } + + @Test + void droppedElementsConsumerReleaseReference() { + ReferenceCounted referenceCounted = Mockito.mock(ReferenceCounted.class); + Mockito.when(referenceCounted.release()).thenReturn(true); + Mockito.when(referenceCounted.refCnt()).thenReturn(1); + + Consumer discardElementsConsumer = DefaultRSocketClient.DISCARD_ELEMENTS_CONSUMER; + discardElementsConsumer.accept(referenceCounted); + + Mockito.verify(referenceCounted).release(); } static Stream interactions() { diff --git a/rsocket-core/src/test/java/io/rsocket/core/SendUtilsTest.java b/rsocket-core/src/test/java/io/rsocket/core/SendUtilsTest.java new file mode 100644 index 000000000..9a51b9419 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/SendUtilsTest.java @@ -0,0 +1,31 @@ +package io.rsocket.core; + +import static org.mockito.Mockito.*; + +import io.netty.util.ReferenceCounted; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; + +public class SendUtilsTest { + + @Test + void droppedElementsConsumerShouldAcceptOtherTypesThanReferenceCounted() { + Consumer value = extractDroppedElementConsumer(); + value.accept(new Object()); + } + + @Test + void droppedElementsConsumerReleaseReference() { + ReferenceCounted referenceCounted = mock(ReferenceCounted.class); + when(referenceCounted.release()).thenReturn(true); + + Consumer value = extractDroppedElementConsumer(); + value.accept(referenceCounted); + + verify(referenceCounted).release(); + } + + private static Consumer extractDroppedElementConsumer() { + return (Consumer) SendUtils.DISCARD_CONTEXT.stream().findAny().get().getValue(); + } +} From 6bba66288c42aaf6f348c8cab12e77325a888036 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 6 Jun 2023 20:43:42 +0300 Subject: [PATCH 104/116] bumps lib versions Signed-off-by: Oleh Dokuka --- build.gradle | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 079a0e1d9..62ee687da 100644 --- a/build.gradle +++ b/build.gradle @@ -16,11 +16,11 @@ plugins { id 'com.github.sherter.google-java-format' version '0.9' apply false - id 'me.champeau.jmh' version '0.6.7' apply false - id 'io.spring.dependency-management' version '1.0.15.RELEASE' apply false + id 'me.champeau.jmh' version '0.7.1' apply false + id 'io.spring.dependency-management' version '1.1.0' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false - id 'io.github.reyerizo.gradle.jcstress' version '0.8.13' apply false - id 'com.github.vlsi.gradle-extensions' version '1.76' apply false + id 'io.github.reyerizo.gradle.jcstress' version '0.8.15' apply false + id 'com.github.vlsi.gradle-extensions' version '1.89' apply false } boolean isCiServer = ["CI", "CONTINUOUS_INTEGRATION", "TRAVIS", "CIRCLECI", "bamboo_planKey", "GITHUB_ACTION"].with { @@ -33,21 +33,21 @@ subprojects { apply plugin: 'com.github.sherter.google-java-format' apply plugin: 'com.github.vlsi.gradle-extensions' - ext['reactor-bom.version'] = '2020.0.31-SNAPSHOT' + ext['reactor-bom.version'] = '2020.0.32' ext['logback.version'] = '1.2.10' - ext['netty-bom.version'] = '4.1.90.Final' - ext['netty-boringssl.version'] = '2.0.59.Final' + ext['netty-bom.version'] = '4.1.93.Final' + ext['netty-boringssl.version'] = '2.0.61.Final' ext['hdrhistogram.version'] = '2.1.12' - ext['mockito.version'] = '4.4.0' + ext['mockito.version'] = '4.11.0' ext['slf4j.version'] = '1.7.36' - ext['jmh.version'] = '1.35' - ext['junit.version'] = '5.8.1' - ext['micrometer.version'] = '1.10.0' - ext['micrometer-tracing.version'] = '1.0.0' - ext['assertj.version'] = '3.22.0' + ext['jmh.version'] = '1.36' + ext['junit.version'] = '5.9.3' + ext['micrometer.version'] = '1.11.0' + ext['micrometer-tracing.version'] = '1.1.1' + ext['assertj.version'] = '3.24.2' ext['netflix.limits.version'] = '0.3.6' ext['bouncycastle-bcpkix.version'] = '1.70' - ext['awaitility.version'] = '4.1.1' + ext['awaitility.version'] = '4.2.0' group = "io.rsocket" From cb811cf6c77c59cc4b62d7caf356f203c92c22c5 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Fri, 9 Jun 2023 20:57:59 +0300 Subject: [PATCH 105/116] increment version Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 7f8f4ca23..ce5421125 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.1.4 -perfBaselineVersion=1.1.3 +version=1.1.5 +perfBaselineVersion=1.1.4 From f591f9d8313b79ea674ee7786000bb1bfd48e1d8 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 27 Feb 2024 13:08:04 +0200 Subject: [PATCH 106/116] update versions and fixes memleak in UnboundedProcessor (#1106) This PR updates dependencies and makes minor modifications to UnboundedProcessor due to repeating failures of UnboundedProcessorJCStreassTest, which started reproducing some unspotted issues. Motivation: UnboundedProcessor is a critical component in the RSocket-Java ecosystem and must work properly. After analysis of its internal state machine, it was spotted that sometimes: The request may not be delivered due to natural concurrency The terminal signal may not be delivered since it checks for demand which might be consumed already (due to natural concurrency) The final value could be delivered violating reactive-streams spec Modifications: This PR adds a minimal set of changes, preserving old implementation but eliminating the mentioned bugs --------- Signed-off-by: Oleh Dokuka --- build.gradle | 8 +- rsocket-core/build.gradle | 5 +- .../UnboundedProcessorStressTest.java | 52 ++++- .../java/io/rsocket/utils/FastLogger.java | 137 +++++++++++ .../rsocket/internal/UnboundedProcessor.java | 220 ++++++++++++++---- 5 files changed, 370 insertions(+), 52 deletions(-) create mode 100644 rsocket-core/src/jcstress/java/io/rsocket/utils/FastLogger.java diff --git a/build.gradle b/build.gradle index 62ee687da..ae8f05d06 100644 --- a/build.gradle +++ b/build.gradle @@ -33,10 +33,10 @@ subprojects { apply plugin: 'com.github.sherter.google-java-format' apply plugin: 'com.github.vlsi.gradle-extensions' - ext['reactor-bom.version'] = '2020.0.32' - ext['logback.version'] = '1.2.10' - ext['netty-bom.version'] = '4.1.93.Final' - ext['netty-boringssl.version'] = '2.0.61.Final' + ext['reactor-bom.version'] = '2020.0.39' + ext['logback.version'] = '1.3.14' + ext['netty-bom.version'] = '4.1.106.Final' + ext['netty-boringssl.version'] = '2.0.62.Final' ext['hdrhistogram.version'] = '2.1.12' ext['mockito.version'] = '4.11.0' ext['slf4j.version'] = '1.7.36' diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index 6f2056da0..da5b69b14 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -41,13 +41,14 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' jcstressImplementation(project(":rsocket-test")) + jcstressImplementation 'org.slf4j:slf4j-api' jcstressImplementation "ch.qos.logback:logback-classic" jcstressImplementation 'io.projectreactor:reactor-test' } jcstress { - mode = 'quick' //quick, default, tough - jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.15" + mode = 'sanity' //sanity, quick, default, tough + jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.16" } jar { diff --git a/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java index 2f5e51f0e..a2d9fcf4d 100644 --- a/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java +++ b/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java @@ -3,6 +3,9 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.UnpooledByteBufAllocator; import io.rsocket.core.StressSubscriber; +import io.rsocket.utils.FastLogger; +import java.util.Arrays; +import java.util.ConcurrentModificationException; import org.openjdk.jcstress.annotations.Actor; import org.openjdk.jcstress.annotations.Arbiter; import org.openjdk.jcstress.annotations.Expect; @@ -14,6 +17,7 @@ import org.openjdk.jcstress.infra.results.L_Result; import reactor.core.Fuseable; import reactor.core.publisher.Hooks; +import reactor.util.Logger; public abstract class UnboundedProcessorStressTest { @@ -21,7 +25,9 @@ public abstract class UnboundedProcessorStressTest { Hooks.onErrorDropped(t -> {}); } - final UnboundedProcessor unboundedProcessor = new UnboundedProcessor(); + final Logger logger = new FastLogger(getClass().getName()); + + final UnboundedProcessor unboundedProcessor = new UnboundedProcessor(logger); @JCStressTest @Outcome( @@ -145,6 +151,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -270,6 +278,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -375,6 +385,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -476,6 +488,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -578,6 +592,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -701,6 +717,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -781,6 +799,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -837,9 +857,15 @@ public void arbiter(LLL_Result r) { + stressSubscriber.onErrorCalls * 2 + stressSubscriber.droppedErrors.size() * 3; + if (stressSubscriber.concurrentOnNext || stressSubscriber.concurrentOnComplete) { + throw new ConcurrentModificationException("boo"); + } + stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -892,6 +918,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1107,6 +1135,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1238,6 +1268,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1390,6 +1422,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1522,6 +1556,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1587,6 +1623,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1652,6 +1690,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1678,6 +1718,16 @@ public void subscribe2() { @Arbiter public void arbiter(L_Result r) { r.r1 = stressSubscriber1.onErrorCalls + stressSubscriber2.onErrorCalls; + + checkOutcomes(this, r.toString(), logger); + } + } + + static void checkOutcomes(Object instance, String result, Logger logger) { + if (Arrays.stream(instance.getClass().getDeclaredAnnotationsByType(Outcome.class)) + .flatMap(o -> Arrays.stream(o.id())) + .noneMatch(s -> s.equalsIgnoreCase(result))) { + throw new RuntimeException(result + " " + logger); } } } diff --git a/rsocket-core/src/jcstress/java/io/rsocket/utils/FastLogger.java b/rsocket-core/src/jcstress/java/io/rsocket/utils/FastLogger.java new file mode 100644 index 000000000..c301d87cf --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/utils/FastLogger.java @@ -0,0 +1,137 @@ +package io.rsocket.utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import reactor.util.Logger; + +/** + * Implementation of {@link Logger} which is based on the {@link ThreadLocal} based queue which + * collects all the events on the per-thread basis.
    Such logger is designed to have all events + * stored during the stress-test run and then sorted and printed out once all the Threads completed + * execution (inside the {@link org.openjdk.jcstress.annotations.Arbiter} annotated method.
    + * Note, this implementation only supports trace-level logs and ignores all others, it is intended + * to be used by {@link reactor.core.publisher.StateLogger}. + */ +public class FastLogger implements Logger { + + final Map> queues = new ConcurrentHashMap<>(); + + final ThreadLocal> logsQueueLocal = + ThreadLocal.withInitial( + () -> { + final ArrayList logs = new ArrayList<>(100); + queues.put(Thread.currentThread(), logs); + return logs; + }); + + private final String name; + + public FastLogger(String name) { + this.name = name; + } + + @Override + public String toString() { + return queues + .values() + .stream() + .flatMap(List::stream) + .sorted( + Comparator.comparingLong( + s -> { + Pattern pattern = Pattern.compile("\\[(.*?)]"); + Matcher matcher = pattern.matcher(s); + matcher.find(); + return Long.parseLong(matcher.group(1)); + })) + .collect(Collectors.joining("\n")); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public boolean isTraceEnabled() { + return true; + } + + @Override + public void trace(String msg) { + logsQueueLocal.get().add(String.format("[%s] %s", System.nanoTime(), msg)); + } + + @Override + public void trace(String format, Object... arguments) { + trace(String.format(format, arguments)); + } + + @Override + public void trace(String msg, Throwable t) { + trace(String.format("%s, %s", msg, Arrays.toString(t.getStackTrace()))); + } + + @Override + public boolean isDebugEnabled() { + return false; + } + + @Override + public void debug(String msg) {} + + @Override + public void debug(String format, Object... arguments) {} + + @Override + public void debug(String msg, Throwable t) {} + + @Override + public boolean isInfoEnabled() { + return false; + } + + @Override + public void info(String msg) {} + + @Override + public void info(String format, Object... arguments) {} + + @Override + public void info(String msg, Throwable t) {} + + @Override + public boolean isWarnEnabled() { + return false; + } + + @Override + public void warn(String msg) {} + + @Override + public void warn(String format, Object... arguments) {} + + @Override + public void warn(String msg, Throwable t) {} + + @Override + public boolean isErrorEnabled() { + return false; + } + + @Override + public void error(String msg) {} + + @Override + public void error(String format, Object... arguments) {} + + @Override + public void error(String msg, Throwable t) {} +} 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 23ada95fe..c96a7aed2 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -32,6 +32,7 @@ import reactor.core.Scannable; import reactor.core.publisher.Flux; import reactor.core.publisher.Operators; +import reactor.util.Logger; import reactor.util.annotation.Nullable; import reactor.util.concurrent.Queues; import reactor.util.context.Context; @@ -51,6 +52,7 @@ public final class UnboundedProcessor extends Flux final Queue queue; final Queue priorityQueue; final Runnable onFinalizedHook; + @Nullable final Logger logger; boolean cancelled; boolean done; @@ -99,10 +101,19 @@ public UnboundedProcessor() { this(() -> {}); } + UnboundedProcessor(Logger logger) { + this(() -> {}, logger); + } + public UnboundedProcessor(Runnable onFinalizedHook) { + this(onFinalizedHook, null); + } + + UnboundedProcessor(Runnable onFinalizedHook, @Nullable Logger logger) { this.onFinalizedHook = onFinalizedHook; this.queue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); this.priorityQueue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); + this.logger = logger; } @Override @@ -153,7 +164,7 @@ public boolean tryEmitPrioritized(ByteBuf t) { } if (hasRequest(previousState)) { - drainRegular(previousState); + drainRegular((previousState | FLAG_HAS_VALUE) + 1); } } return true; @@ -189,7 +200,7 @@ public boolean tryEmitNormal(ByteBuf t) { } if (hasRequest(previousState)) { - drainRegular(previousState); + drainRegular((previousState | FLAG_HAS_VALUE) + 1); } } @@ -223,9 +234,7 @@ public boolean tryEmitFinal(ByteBuf t) { return true; } - if (hasRequest(previousState)) { - drainRegular(previousState); - } + drainRegular((previousState | FLAG_TERMINATED | FLAG_HAS_VALUE) + 1); } return true; @@ -279,9 +288,7 @@ public void onError(Throwable t) { return; } - if (hasRequest(previousState)) { - drainRegular(previousState); - } + drainRegular((previousState | FLAG_TERMINATED) + 1); } } @@ -318,18 +325,15 @@ public void onComplete() { return; } - if (hasRequest(previousState)) { - drainRegular(previousState); - } + drainRegular((previousState | FLAG_TERMINATED) + 1); } } - void drainRegular(long previousState) { + void drainRegular(long expectedState) { final CoreSubscriber a = this.actual; final Queue q = this.queue; final Queue pq = this.priorityQueue; - long expectedState = previousState + 1; for (; ; ) { long r = this.requested; @@ -351,7 +355,7 @@ void drainRegular(long previousState) { empty = t == null; } - if (checkTerminated(done, empty, a)) { + if (checkTerminated(done, empty, true, a)) { if (!empty) { release(t); } @@ -374,7 +378,7 @@ void drainRegular(long previousState) { done = this.done; empty = q.isEmpty() && pq.isEmpty(); - if (checkTerminated(done, empty, a)) { + if (checkTerminated(done, empty, false, a)) { return; } } @@ -401,7 +405,8 @@ void drainRegular(long previousState) { } } - boolean checkTerminated(boolean done, boolean empty, CoreSubscriber a) { + boolean checkTerminated( + boolean done, boolean empty, boolean hasDemand, CoreSubscriber a) { final long state = this.state; if (isCancelled(state)) { clearAndFinalize(this); @@ -415,8 +420,15 @@ boolean checkTerminated(boolean done, boolean empty, CoreSubscriber actual) { previousState = markSubscriberReady(this); + if (isSubscriberReady(previousState)) { + return; + } + if (this.outputFused) { if (isCancelled(previousState)) { return; @@ -523,7 +539,7 @@ public void subscribe(CoreSubscriber actual) { } if (hasRequest(previousState)) { - drainRegular(previousState); + drainRegular((previousState | FLAG_SUBSCRIBER_READY) + 1); } } @@ -549,7 +565,7 @@ public void request(long n) { } if (isSubscriberReady(previousState) && hasValue(previousState)) { - drainRegular(previousState); + drainRegular((previousState | FLAG_HAS_REQUEST) + 1); } } } @@ -727,7 +743,9 @@ static long markSubscribedOnce(UnboundedProcessor instance) { return state; } - if (STATE.compareAndSet(instance, state, state | FLAG_SUBSCRIBED_ONCE)) { + final long nextState = state | FLAG_SUBSCRIBED_ONCE; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mso", state, nextState); return state; } } @@ -743,7 +761,10 @@ static long markSubscriberReady(UnboundedProcessor instance) { for (; ; ) { long state = instance.state; - if (isFinalized(state) || isCancelled(state) || isDisposed(state)) { + if (isFinalized(state) + || isCancelled(state) + || isDisposed(state) + || isSubscriberReady(state)) { return state; } @@ -754,7 +775,9 @@ static long markSubscriberReady(UnboundedProcessor instance) { } } - if (STATE.compareAndSet(instance, state, nextState | FLAG_SUBSCRIBER_READY)) { + nextState = nextState | FLAG_SUBSCRIBER_READY; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " msr", state, nextState); return state; } } @@ -776,11 +799,13 @@ static long markRequestAdded(UnboundedProcessor instance) { } long nextState = state; - if (isSubscriberReady(state) && hasValue(state)) { + if (isWorkInProgress(state) || (isSubscriberReady(state) && hasValue(state))) { nextState = addWork(state); } - if (STATE.compareAndSet(instance, state, nextState | FLAG_HAS_REQUEST)) { + nextState = nextState | FLAG_HAS_REQUEST; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mra", state, nextState); return state; } } @@ -815,7 +840,9 @@ static long markValueAdded(UnboundedProcessor instance) { } } - if (STATE.compareAndSet(instance, state, nextState | FLAG_HAS_VALUE)) { + nextState = nextState | FLAG_HAS_VALUE; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mva", state, nextState); return state; } } @@ -840,12 +867,12 @@ static long markValueAddedAndTerminated(UnboundedProcessor instance) { if (isWorkInProgress(state)) { nextState = addWork(state); } else if (isSubscriberReady(state) && !instance.outputFused) { - if (hasRequest(state)) { - nextState = addWork(state); - } + nextState = addWork(state); } - if (STATE.compareAndSet(instance, state, nextState | FLAG_HAS_VALUE | FLAG_TERMINATED)) { + nextState = nextState | FLAG_HAS_VALUE | FLAG_TERMINATED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, "mva&t", state, nextState); return state; } } @@ -867,16 +894,20 @@ static long markTerminatedOrFinalized(UnboundedProcessor instance) { } long nextState = state; - if (isSubscriberReady(state) && !instance.outputFused) { + if (isWorkInProgress(state)) { + nextState = addWork(state); + } else if (isSubscriberReady(state) && !instance.outputFused) { if (!hasValue(state)) { // fast path for no values and no work in progress nextState = FLAG_FINALIZED; - } else if (hasRequest(state)) { + } else { nextState = addWork(state); } } - if (STATE.compareAndSet(instance, state, nextState | FLAG_TERMINATED)) { + nextState = nextState | FLAG_TERMINATED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mt|f", state, nextState); if (isFinalized(nextState)) { instance.onFinalizedHook.run(); } @@ -899,8 +930,9 @@ static long markCancelled(UnboundedProcessor instance) { return state; } - final long nextState = addWork(state); - if (STATE.compareAndSet(instance, state, nextState | FLAG_CANCELLED)) { + final long nextState = addWork(state) | FLAG_CANCELLED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mc", state, nextState); return state; } } @@ -921,8 +953,9 @@ static long markDisposed(UnboundedProcessor instance) { return state; } - final long nextState = addWork(state); - if (STATE.compareAndSet(instance, state, nextState | FLAG_DISPOSED)) { + final long nextState = addWork(state) | FLAG_DISPOSED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " md", state, nextState); return state; } } @@ -945,12 +978,10 @@ static long addWork(long state) { */ static long markWorkDone( UnboundedProcessor instance, long expectedState, boolean hasRequest, boolean hasValue) { - final long expectedMissed = expectedState & MAX_WIP_VALUE; for (; ; ) { final long state = instance.state; - final long missed = state & MAX_WIP_VALUE; - if (missed != expectedMissed) { + if (state != expectedState) { return state; } @@ -958,11 +989,12 @@ static long markWorkDone( return state; } - final long nextState = state - expectedMissed; - if (STATE.compareAndSet( - instance, - state, - nextState ^ (hasRequest ? 0 : FLAG_HAS_REQUEST) ^ (hasValue ? 0 : FLAG_HAS_VALUE))) { + final long nextState = + (state - (expectedState & MAX_WIP_VALUE)) + ^ (hasRequest ? 0 : FLAG_HAS_REQUEST) + ^ (hasValue ? 0 : FLAG_HAS_VALUE); + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mwd", state, nextState); return nextState; } } @@ -991,8 +1023,9 @@ static void clearAndFinalize(UnboundedProcessor instance) { instance.clearUnsafely(); } - if (STATE.compareAndSet( - instance, state, (state & ~MAX_WIP_VALUE & ~FLAG_HAS_VALUE) | FLAG_FINALIZED)) { + long nextState = (state & ~MAX_WIP_VALUE & ~FLAG_HAS_VALUE) | FLAG_FINALIZED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " c&f", state, nextState); instance.onFinalizedHook.run(); break; } @@ -1034,4 +1067,101 @@ static boolean isSubscriberReady(long state) { static boolean isSubscribedOnce(long state) { return (state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE; } + + static void log( + UnboundedProcessor instance, String action, long initialState, long committedState) { + log(instance, action, initialState, committedState, false); + } + + static void log( + UnboundedProcessor instance, + String action, + long initialState, + long committedState, + boolean logStackTrace) { + Logger logger = instance.logger; + if (logger == null || !logger.isTraceEnabled()) { + return; + } + + if (logStackTrace) { + logger.trace( + String.format( + "[%s][%s][%s][%s-%s]", + instance, + action, + action, + Thread.currentThread().getId(), + formatState(initialState, 64), + formatState(committedState, 64)), + new RuntimeException()); + } else { + logger.trace( + String.format( + "[%s][%s][%s][%s-%s]", + instance, + action, + Thread.currentThread().getId(), + formatState(initialState, 64), + formatState(committedState, 64))); + } + } + + static void log( + UnboundedProcessor instance, String action, int initialState, int committedState) { + log(instance, action, initialState, committedState, false); + } + + static void log( + UnboundedProcessor instance, + String action, + int initialState, + int committedState, + boolean logStackTrace) { + Logger logger = instance.logger; + if (logger == null || !logger.isTraceEnabled()) { + return; + } + + if (logStackTrace) { + logger.trace( + String.format( + "[%s][%s][%s][%s-%s]", + instance, + action, + action, + Thread.currentThread().getId(), + formatState(initialState, 32), + formatState(committedState, 32)), + new RuntimeException()); + } else { + logger.trace( + String.format( + "[%s][%s][%s][%s-%s]", + instance, + action, + Thread.currentThread().getId(), + formatState(initialState, 32), + formatState(committedState, 32))); + } + } + + static String formatState(long state, int size) { + final String defaultFormat = Long.toBinaryString(state); + final StringBuilder formatted = new StringBuilder(); + final int toPrepend = size - defaultFormat.length(); + for (int i = 0; i < size; i++) { + if (i != 0 && i % 4 == 0) { + formatted.append("_"); + } + if (i < toPrepend) { + formatted.append("0"); + } else { + formatted.append(defaultFormat.charAt(i - toPrepend)); + } + } + + formatted.insert(0, "0b"); + return formatted.toString(); + } } From 7abe35ee592843de7cf11f4675615765c690d8a4 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 24 Jan 2025 21:31:24 +0000 Subject: [PATCH 107/116] Fix handling of rejected setup errors (#1117) Closes gh-1092 --- .../java/io/rsocket/core/RSocketServer.java | 12 +++++-- .../rsocket/resume/ServerRSocketSession.java | 3 ++ .../io/rsocket/core/RSocketServerTest.java | 36 ++++++++++++++++++- 3 files changed, 47 insertions(+), 4 deletions(-) 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 0c68db6df..e969c39d2 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2025 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. @@ -463,8 +463,14 @@ private Mono acceptSetup( return interceptors .initSocketAcceptor(acceptor) .accept(setupPayload, wrappedRSocketRequester) - .doOnError( - err -> serverSetup.sendError(wrappedDuplexConnection, rejectedSetupError(err))) + .onErrorResume( + err -> + Mono.fromRunnable( + () -> + serverSetup.sendError( + wrappedDuplexConnection, rejectedSetupError(err))) + .then(wrappedDuplexConnection.onClose()) + .then(Mono.error(err))) .doOnNext( rSocketHandler -> { RSocket wrappedRSocketHandler = interceptors.initResponder(rSocketHandler); 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 c4dc4d837..ad1b38375 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -287,6 +287,9 @@ public void setKeepAliveSupport(KeepAliveSupport keepAliveSupport) { @Override public void dispose() { + if (logger.isDebugEnabled()) { + logger.debug("Side[server]|Session[{}]. Disposing session", session); + } Operators.terminate(S, this); resumableConnection.dispose(); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java index 0b5ca38f7..a335ac1f3 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2021 the original author or authors. + * Copyright 2015-2025 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,11 +23,14 @@ import io.rsocket.Closeable; import io.rsocket.FrameAssert; import io.rsocket.RSocket; +import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.frame.FrameType; import io.rsocket.frame.KeepAliveFrameCodec; import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.SetupFrameCodec; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestServerTransport; +import io.rsocket.util.EmptyPayload; import java.time.Duration; import java.util.Random; import org.assertj.core.api.Assertions; @@ -164,4 +167,35 @@ public void unexpectedFramesBeforeSetup() { server.dispose(); transport.alloc().assertHasNoLeaks(); } + + @Test + public void ensuresErrorFrameDeliveredPriorConnectionDisposal() { + TestServerTransport transport = new TestServerTransport(); + Closeable server = + RSocketServer.create() + .acceptor( + (setup, sendingSocket) -> Mono.error(new RejectedSetupException("ACCESS_DENIED"))) + .bind(transport) + .block(); + + TestDuplexConnection connection = transport.connect(); + connection.addToReceivedBuffer( + SetupFrameCodec.encode( + ByteBufAllocator.DEFAULT, + false, + 0, + 1, + Unpooled.EMPTY_BUFFER, + "metadata_type", + "data_type", + EmptyPayload.INSTANCE)); + + StepVerifier.create(connection.onClose()).expectComplete().verify(Duration.ofSeconds(30)); + FrameAssert.assertThat(connection.pollFrame()) + .hasStreamIdZero() + .hasData("ACCESS_DENIED") + .hasNoLeaks(); + server.dispose(); + transport.alloc().assertHasNoLeaks(); + } } From b1dd3c54148daecc50695b6b2687e578fb641c0d Mon Sep 17 00:00:00 2001 From: sullis Date: Fri, 24 Jan 2025 13:31:58 -0800 Subject: [PATCH 108/116] support netty boringssl aarch_64 classifier (#1107) Signed-off-by: sullis --- rsocket-transport-netty/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 17756dbc6..39a5ceac5 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -22,7 +22,7 @@ plugins { } def os_suffix = "" -if (osdetector.classifier in ["linux-x86_64", "osx-x86_64", "windows-x86_64"]) { +if (osdetector.classifier in ["linux-x86_64", "linux-aarch_64", "osx-x86_64", "osx-aarch_64", "windows-x86_64"]) { os_suffix = "::" + osdetector.classifier } From 50c51c43c5963258af3fbc7562fb9bf2083782f2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 24 Jan 2025 21:31:24 +0000 Subject: [PATCH 109/116] Fix handling of rejected setup errors (#1117) Closes gh-1092 --- .../java/io/rsocket/core/RSocketServer.java | 12 +++++-- .../rsocket/resume/ServerRSocketSession.java | 3 ++ .../io/rsocket/core/RSocketServerTest.java | 36 ++++++++++++++++++- 3 files changed, 47 insertions(+), 4 deletions(-) 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 0c68db6df..e969c39d2 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2025 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. @@ -463,8 +463,14 @@ private Mono acceptSetup( return interceptors .initSocketAcceptor(acceptor) .accept(setupPayload, wrappedRSocketRequester) - .doOnError( - err -> serverSetup.sendError(wrappedDuplexConnection, rejectedSetupError(err))) + .onErrorResume( + err -> + Mono.fromRunnable( + () -> + serverSetup.sendError( + wrappedDuplexConnection, rejectedSetupError(err))) + .then(wrappedDuplexConnection.onClose()) + .then(Mono.error(err))) .doOnNext( rSocketHandler -> { RSocket wrappedRSocketHandler = interceptors.initResponder(rSocketHandler); 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 c4dc4d837..ad1b38375 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -287,6 +287,9 @@ public void setKeepAliveSupport(KeepAliveSupport keepAliveSupport) { @Override public void dispose() { + if (logger.isDebugEnabled()) { + logger.debug("Side[server]|Session[{}]. Disposing session", session); + } Operators.terminate(S, this); resumableConnection.dispose(); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java index 0b5ca38f7..a335ac1f3 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2021 the original author or authors. + * Copyright 2015-2025 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,11 +23,14 @@ import io.rsocket.Closeable; import io.rsocket.FrameAssert; import io.rsocket.RSocket; +import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.frame.FrameType; import io.rsocket.frame.KeepAliveFrameCodec; import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.SetupFrameCodec; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestServerTransport; +import io.rsocket.util.EmptyPayload; import java.time.Duration; import java.util.Random; import org.assertj.core.api.Assertions; @@ -164,4 +167,35 @@ public void unexpectedFramesBeforeSetup() { server.dispose(); transport.alloc().assertHasNoLeaks(); } + + @Test + public void ensuresErrorFrameDeliveredPriorConnectionDisposal() { + TestServerTransport transport = new TestServerTransport(); + Closeable server = + RSocketServer.create() + .acceptor( + (setup, sendingSocket) -> Mono.error(new RejectedSetupException("ACCESS_DENIED"))) + .bind(transport) + .block(); + + TestDuplexConnection connection = transport.connect(); + connection.addToReceivedBuffer( + SetupFrameCodec.encode( + ByteBufAllocator.DEFAULT, + false, + 0, + 1, + Unpooled.EMPTY_BUFFER, + "metadata_type", + "data_type", + EmptyPayload.INSTANCE)); + + StepVerifier.create(connection.onClose()).expectComplete().verify(Duration.ofSeconds(30)); + FrameAssert.assertThat(connection.pollFrame()) + .hasStreamIdZero() + .hasData("ACCESS_DENIED") + .hasNoLeaks(); + server.dispose(); + transport.alloc().assertHasNoLeaks(); + } } From a5fbd96a815b1ef07333df61256d4756227a3642 Mon Sep 17 00:00:00 2001 From: sullis Date: Fri, 24 Jan 2025 13:31:58 -0800 Subject: [PATCH 110/116] support netty boringssl aarch_64 classifier (#1107) Signed-off-by: sullis --- rsocket-transport-netty/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 17756dbc6..39a5ceac5 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -22,7 +22,7 @@ plugins { } def os_suffix = "" -if (osdetector.classifier in ["linux-x86_64", "osx-x86_64", "windows-x86_64"]) { +if (osdetector.classifier in ["linux-x86_64", "linux-aarch_64", "osx-x86_64", "osx-aarch_64", "windows-x86_64"]) { os_suffix = "::" + osdetector.classifier } From 9bc30c44d601fa1e5d937901df979e68a0227fc1 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 28 Jan 2025 11:29:28 +0000 Subject: [PATCH 111/116] Update Logback version to 1.2.13 It must stay < 1.3 to work with slf4j 1.7. Signed-off-by: rstoyanchev --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ae8f05d06..ccf03c4eb 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ subprojects { apply plugin: 'com.github.vlsi.gradle-extensions' ext['reactor-bom.version'] = '2020.0.39' - ext['logback.version'] = '1.3.14' + ext['logback.version'] = '1.2.13' ext['netty-bom.version'] = '4.1.106.Final' ext['netty-boringssl.version'] = '2.0.62.Final' ext['hdrhistogram.version'] = '2.1.12' From ccd67ba20d8a0c242901a180c8369a6315a6b626 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:41:38 +0200 Subject: [PATCH 112/116] ensures connection is closed on keepalive timeout (#1118) * ensures connection is close on keepalive timeout Signed-off-by: Oleh Dokuka * fix format Signed-off-by: Oleh Dokuka * improve KeepaliveTest Signed-off-by: Oleh Dokuka * fix format and failing test Signed-off-by: Oleh Dokuka * adds reference to the original GH issue Signed-off-by: Oleh Dokuka * fixes google format Signed-off-by: Oleh Dokuka --------- Signed-off-by: Oleh Dokuka --- .../io/rsocket/core/RSocketRequester.java | 1 + .../io/rsocket/integration/KeepaliveTest.java | 190 ++++++++++++++++++ .../src/test/resources/logback-test.xml | 1 - 3 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.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 9e8d349bf..b8a9c00ff 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -312,6 +312,7 @@ private void tryTerminateOnKeepAlive(KeepAliveSupport.KeepAlive keepAlive) { () -> new ConnectionErrorException( String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()))); + getDuplexConnection().dispose(); } private void tryShutdown(Throwable e) { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java new file mode 100644 index 000000000..f05713215 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java @@ -0,0 +1,190 @@ +package io.rsocket.integration; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketClient; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.frame.decoder.PayloadDecoder; +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 java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.tcp.TcpClient; +import reactor.netty.tcp.TcpServer; +import reactor.test.StepVerifier; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; + +/** + * Test case that reproduces the following GitHub Issue + */ +public class KeepaliveTest { + + private static final Logger LOG = LoggerFactory.getLogger(KeepaliveTest.class); + private static final int PORT = 23200; + + private CloseableChannel server; + + @BeforeEach + void setUp() { + server = createServer().block(); + } + + @AfterEach + void tearDown() { + server.dispose(); + server.onClose().block(); + } + + @Test + void keepAliveTest() { + RSocketClient rsocketClient = createClient(); + + int expectedCount = 4; + AtomicBoolean sleepOnce = new AtomicBoolean(true); + StepVerifier.create( + Flux.range(0, expectedCount) + .delayElements(Duration.ofMillis(2000)) + .concatMap( + i -> + rsocketClient + .requestResponse(Mono.just(DefaultPayload.create(""))) + .doOnNext( + __ -> { + if (sleepOnce.getAndSet(false)) { + try { + LOG.info("Sleeping..."); + Thread.sleep(1_000); + LOG.info("Waking up."); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }) + .log("id " + i) + .onErrorComplete())) + .expectSubscription() + .expectNextCount(expectedCount) + .verifyComplete(); + } + + @Test + void keepAliveTestLazy() { + Mono rsocketMono = createClientLazy(); + + int expectedCount = 4; + AtomicBoolean sleepOnce = new AtomicBoolean(true); + StepVerifier.create( + Flux.range(0, expectedCount) + .delayElements(Duration.ofMillis(2000)) + .concatMap( + i -> + rsocketMono.flatMap( + rsocket -> + rsocket + .requestResponse(DefaultPayload.create("")) + .doOnNext( + __ -> { + if (sleepOnce.getAndSet(false)) { + try { + LOG.info("Sleeping..."); + Thread.sleep(1_000); + LOG.info("Waking up."); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }) + .log("id " + i) + .onErrorComplete()))) + .expectSubscription() + .expectNextCount(expectedCount) + .verifyComplete(); + } + + private static Mono createServer() { + LOG.info("Starting server at port {}", PORT); + + TcpServer tcpServer = TcpServer.create().host("localhost").port(PORT); + + return RSocketServer.create( + (setupPayload, rSocket) -> { + rSocket + .onClose() + .doFirst(() -> LOG.info("Connected on server side.")) + .doOnTerminate(() -> LOG.info("Connection closed on server side.")) + .subscribe(); + + return Mono.just(new MyServerRsocket()); + }) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(TcpServerTransport.create(tcpServer)) + .doOnNext(closeableChannel -> LOG.info("RSocket server started.")); + } + + private static RSocketClient createClient() { + LOG.info("Connecting...."); + + Function reconnectSpec = + reason -> + Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(10L)) + .doBeforeRetry(retrySignal -> LOG.info("Reconnecting. Reason: {}", reason)); + + Mono rsocketMono = + RSocketConnector.create() + .fragment(16384) + .reconnect(reconnectSpec.apply("connector-close")) + .keepAlive(Duration.ofMillis(100L), Duration.ofMillis(900L)) + .connect(TcpClientTransport.create(TcpClient.create().host("localhost").port(PORT))); + + RSocketClient client = RSocketClient.from(rsocketMono); + + client + .source() + .doOnNext(r -> LOG.info("Got RSocket")) + .flatMap(RSocket::onClose) + .doOnError(err -> LOG.error("Error during onClose.", err)) + .retryWhen(reconnectSpec.apply("client-close")) + .doFirst(() -> LOG.info("Connected on client side.")) + .doOnTerminate(() -> LOG.info("Connection closed on client side.")) + .repeat() + .subscribe(); + + return client; + } + + private static Mono createClientLazy() { + LOG.info("Connecting...."); + + Function reconnectSpec = + reason -> + Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(10L)) + .doBeforeRetry(retrySignal -> LOG.info("Reconnecting. Reason: {}", reason)); + + return RSocketConnector.create() + .fragment(16384) + .reconnect(reconnectSpec.apply("connector-close")) + .keepAlive(Duration.ofMillis(100L), Duration.ofMillis(900L)) + .connect(TcpClientTransport.create(TcpClient.create().host("localhost").port(PORT))); + } + + public static class MyServerRsocket implements RSocket { + + @Override + public Mono requestResponse(Payload payload) { + return Mono.just("Pong").map(DefaultPayload::create); + } + } +} diff --git a/rsocket-transport-netty/src/test/resources/logback-test.xml b/rsocket-transport-netty/src/test/resources/logback-test.xml index b42db6df6..981d6d0b6 100644 --- a/rsocket-transport-netty/src/test/resources/logback-test.xml +++ b/rsocket-transport-netty/src/test/resources/logback-test.xml @@ -27,7 +27,6 @@ - From cff5cdbb16da6393efc04d8f0b80793e54f79026 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 31 Jan 2025 11:53:11 +0000 Subject: [PATCH 113/116] Log data in KEEPALIVE frame Fixes gh-1114 Signed-off-by: rstoyanchev --- .../main/java/io/rsocket/frame/FrameUtil.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 66d18c8a7..d581731a3 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015-2025 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; @@ -99,8 +114,9 @@ private static ByteBuf getData(ByteBuf frame, FrameType frameType) { case REQUEST_CHANNEL: data = RequestChannelFrameCodec.data(frame); break; - // Payload and synthetic types + // Payload, KeepAlive and synthetic types case PAYLOAD: + case KEEPALIVE: case NEXT: case NEXT_COMPLETE: case COMPLETE: From 838e8fbfa2c45d72fa323c7e82e53490cc0d3d8b Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:41:38 +0200 Subject: [PATCH 114/116] ensures connection is closed on keepalive timeout (#1118) * ensures connection is close on keepalive timeout Signed-off-by: Oleh Dokuka * fix format Signed-off-by: Oleh Dokuka * improve KeepaliveTest Signed-off-by: Oleh Dokuka * fix format and failing test Signed-off-by: Oleh Dokuka * adds reference to the original GH issue Signed-off-by: Oleh Dokuka * fixes google format Signed-off-by: Oleh Dokuka --------- Signed-off-by: Oleh Dokuka --- .../io/rsocket/core/RSocketRequester.java | 1 + .../io/rsocket/integration/KeepaliveTest.java | 190 ++++++++++++++++++ .../src/test/resources/logback-test.xml | 1 - 3 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.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 9e8d349bf..b8a9c00ff 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -312,6 +312,7 @@ private void tryTerminateOnKeepAlive(KeepAliveSupport.KeepAlive keepAlive) { () -> new ConnectionErrorException( String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()))); + getDuplexConnection().dispose(); } private void tryShutdown(Throwable e) { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java new file mode 100644 index 000000000..f05713215 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java @@ -0,0 +1,190 @@ +package io.rsocket.integration; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketClient; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.frame.decoder.PayloadDecoder; +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 java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.tcp.TcpClient; +import reactor.netty.tcp.TcpServer; +import reactor.test.StepVerifier; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; + +/** + * Test case that reproduces the following GitHub Issue + */ +public class KeepaliveTest { + + private static final Logger LOG = LoggerFactory.getLogger(KeepaliveTest.class); + private static final int PORT = 23200; + + private CloseableChannel server; + + @BeforeEach + void setUp() { + server = createServer().block(); + } + + @AfterEach + void tearDown() { + server.dispose(); + server.onClose().block(); + } + + @Test + void keepAliveTest() { + RSocketClient rsocketClient = createClient(); + + int expectedCount = 4; + AtomicBoolean sleepOnce = new AtomicBoolean(true); + StepVerifier.create( + Flux.range(0, expectedCount) + .delayElements(Duration.ofMillis(2000)) + .concatMap( + i -> + rsocketClient + .requestResponse(Mono.just(DefaultPayload.create(""))) + .doOnNext( + __ -> { + if (sleepOnce.getAndSet(false)) { + try { + LOG.info("Sleeping..."); + Thread.sleep(1_000); + LOG.info("Waking up."); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }) + .log("id " + i) + .onErrorComplete())) + .expectSubscription() + .expectNextCount(expectedCount) + .verifyComplete(); + } + + @Test + void keepAliveTestLazy() { + Mono rsocketMono = createClientLazy(); + + int expectedCount = 4; + AtomicBoolean sleepOnce = new AtomicBoolean(true); + StepVerifier.create( + Flux.range(0, expectedCount) + .delayElements(Duration.ofMillis(2000)) + .concatMap( + i -> + rsocketMono.flatMap( + rsocket -> + rsocket + .requestResponse(DefaultPayload.create("")) + .doOnNext( + __ -> { + if (sleepOnce.getAndSet(false)) { + try { + LOG.info("Sleeping..."); + Thread.sleep(1_000); + LOG.info("Waking up."); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }) + .log("id " + i) + .onErrorComplete()))) + .expectSubscription() + .expectNextCount(expectedCount) + .verifyComplete(); + } + + private static Mono createServer() { + LOG.info("Starting server at port {}", PORT); + + TcpServer tcpServer = TcpServer.create().host("localhost").port(PORT); + + return RSocketServer.create( + (setupPayload, rSocket) -> { + rSocket + .onClose() + .doFirst(() -> LOG.info("Connected on server side.")) + .doOnTerminate(() -> LOG.info("Connection closed on server side.")) + .subscribe(); + + return Mono.just(new MyServerRsocket()); + }) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(TcpServerTransport.create(tcpServer)) + .doOnNext(closeableChannel -> LOG.info("RSocket server started.")); + } + + private static RSocketClient createClient() { + LOG.info("Connecting...."); + + Function reconnectSpec = + reason -> + Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(10L)) + .doBeforeRetry(retrySignal -> LOG.info("Reconnecting. Reason: {}", reason)); + + Mono rsocketMono = + RSocketConnector.create() + .fragment(16384) + .reconnect(reconnectSpec.apply("connector-close")) + .keepAlive(Duration.ofMillis(100L), Duration.ofMillis(900L)) + .connect(TcpClientTransport.create(TcpClient.create().host("localhost").port(PORT))); + + RSocketClient client = RSocketClient.from(rsocketMono); + + client + .source() + .doOnNext(r -> LOG.info("Got RSocket")) + .flatMap(RSocket::onClose) + .doOnError(err -> LOG.error("Error during onClose.", err)) + .retryWhen(reconnectSpec.apply("client-close")) + .doFirst(() -> LOG.info("Connected on client side.")) + .doOnTerminate(() -> LOG.info("Connection closed on client side.")) + .repeat() + .subscribe(); + + return client; + } + + private static Mono createClientLazy() { + LOG.info("Connecting...."); + + Function reconnectSpec = + reason -> + Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(10L)) + .doBeforeRetry(retrySignal -> LOG.info("Reconnecting. Reason: {}", reason)); + + return RSocketConnector.create() + .fragment(16384) + .reconnect(reconnectSpec.apply("connector-close")) + .keepAlive(Duration.ofMillis(100L), Duration.ofMillis(900L)) + .connect(TcpClientTransport.create(TcpClient.create().host("localhost").port(PORT))); + } + + public static class MyServerRsocket implements RSocket { + + @Override + public Mono requestResponse(Payload payload) { + return Mono.just("Pong").map(DefaultPayload::create); + } + } +} diff --git a/rsocket-transport-netty/src/test/resources/logback-test.xml b/rsocket-transport-netty/src/test/resources/logback-test.xml index b42db6df6..981d6d0b6 100644 --- a/rsocket-transport-netty/src/test/resources/logback-test.xml +++ b/rsocket-transport-netty/src/test/resources/logback-test.xml @@ -27,7 +27,6 @@ - From d28d093aaef21bca01d8de12c2fb9be8492ed982 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 31 Jan 2025 11:53:11 +0000 Subject: [PATCH 115/116] Log data in KEEPALIVE frame Fixes gh-1114 Signed-off-by: rstoyanchev --- .../main/java/io/rsocket/frame/FrameUtil.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 66d18c8a7..d581731a3 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015-2025 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; @@ -99,8 +114,9 @@ private static ByteBuf getData(ByteBuf frame, FrameType frameType) { case REQUEST_CHANNEL: data = RequestChannelFrameCodec.data(frame); break; - // Payload and synthetic types + // Payload, KeepAlive and synthetic types case PAYLOAD: + case KEEPALIVE: case NEXT: case NEXT_COMPLETE: case COMPLETE: From 6e725d6d94f15de87ad7d67c8cc8e3e422e72acd Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 31 Jan 2025 13:58:46 +0000 Subject: [PATCH 116/116] Dependency upgrades Reactor 2020.0.39 -> 2020.0.47 Netty 4.1.106.Final -> 4.1.117.Final netty-tcnative-boringssl-static 2.0.62.Final -> 2.0.69.Final Micrometer 1.11.0 -> 1.11.12 Micrometer Tracing 1.1.1 -> 1.1.13 Signed-off-by: rstoyanchev --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index ccf03c4eb..b2122e4a5 100644 --- a/build.gradle +++ b/build.gradle @@ -33,17 +33,17 @@ subprojects { apply plugin: 'com.github.sherter.google-java-format' apply plugin: 'com.github.vlsi.gradle-extensions' - ext['reactor-bom.version'] = '2020.0.39' + ext['reactor-bom.version'] = '2020.0.47' ext['logback.version'] = '1.2.13' - ext['netty-bom.version'] = '4.1.106.Final' - ext['netty-boringssl.version'] = '2.0.62.Final' + ext['netty-bom.version'] = '4.1.117.Final' + ext['netty-boringssl.version'] = '2.0.69.Final' ext['hdrhistogram.version'] = '2.1.12' ext['mockito.version'] = '4.11.0' ext['slf4j.version'] = '1.7.36' ext['jmh.version'] = '1.36' ext['junit.version'] = '5.9.3' - ext['micrometer.version'] = '1.11.0' - ext['micrometer-tracing.version'] = '1.1.1' + ext['micrometer.version'] = '1.11.12' + ext['micrometer-tracing.version'] = '1.1.13' ext['assertj.version'] = '3.24.2' ext['netflix.limits.version'] = '0.3.6' ext['bouncycastle-bcpkix.version'] = '1.70'