From 32da1318870753f331cc0e1750381d1db4172986 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 25 Oct 2022 18:43:50 +0200 Subject: [PATCH 01/18] 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 02/18] 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 03/18] 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 04/18] 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 05/18] 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 06/18] 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 07/18] 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 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 delta 20228 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 \(.*\)$'` - 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 08/18] 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 09/18] 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 10/18] 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 11/18] 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 12/18] 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 13/18] 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 14/18] 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 15/18] 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 16/18] 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 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 17/18] 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 6bba66288c42aaf6f348c8cab12e77325a888036 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 6 Jun 2023 20:43:42 +0300 Subject: [PATCH 18/18] 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"