From 7a4602c8d3bd38ac8154c7c7ad2ddcaa69f1c707 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 10 Aug 2020 18:16:17 +0300 Subject: [PATCH 001/183] bumps reactor and netty version Signed-off-by: Oleh Dokuka --- build.gradle | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 1829a512f..a576f22f8 100644 --- a/build.gradle +++ b/build.gradle @@ -32,11 +32,10 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = 'Dysprosium-BUILD-SNAPSHOT' + ext['reactor-bom.version'] = 'Dysprosium-SR11' ext['logback.version'] = '1.2.3' - ext['findbugs.version'] = '3.0.2' - ext['netty-bom.version'] = '4.1.50.Final' - ext['netty-boringssl.version'] = '2.0.30.Final' + ext['netty-bom.version'] = '4.1.51.Final' + ext['netty-boringssl.version'] = '2.0.31.Final' ext['hdrhistogram.version'] = '2.1.10' ext['mockito.version'] = '3.2.0' ext['slf4j.version'] = '1.7.25' From 5a3dc4b12f8198b4ca080f503ce5c5ba5ff882ed Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 10 Aug 2020 19:27:19 +0300 Subject: [PATCH 002/183] updates project versions and docs Signed-off-by: Oleh Dokuka --- README.md | 8 ++++---- gradle.properties | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5b5193329..d54a42dad 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ repositories { mavenCentral() } dependencies { - implementation 'io.rsocket:rsocket-core:1.0.1' - implementation 'io.rsocket:rsocket-transport-netty:1.0.1' + implementation 'io.rsocket:rsocket-core:1.0.2' + implementation 'io.rsocket:rsocket-transport-netty:1.0.2' } ``` @@ -40,8 +40,8 @@ repositories { maven { url 'https://oss.jfrog.org/oss-snapshot-local' } } dependencies { - implementation 'io.rsocket:rsocket-core:1.0.2-SNAPSHOT' - implementation 'io.rsocket:rsocket-transport-netty:1.0.2-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:1.0.3-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-netty:1.0.3-SNAPSHOT' } ``` diff --git a/gradle.properties b/gradle.properties index 9021ebfab..09eb2d90f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.2 -perfBaselineVersion=1.0.1 +version=1.0.3 +perfBaselineVersion=1.0.2 From 99f2c458bab942a220b650c80f5267421a2b97bd Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 14 Aug 2020 18:43:17 +0300 Subject: [PATCH 003/183] migrates onto Github Actions Signed-off-by: Oleh Dokuka --- .github/workflows/gradle-all.yml | 45 ++++++++++++++++++++++++++++ .github/workflows/gradle-main.yml | 44 +++++++++++++++++++++++++++ .github/workflows/gradle-pr.yml | 31 +++++++++++++++++++ .github/workflows/gradle-release.yml | 44 +++++++++++++++++++++++++++ .travis.yml | 45 ---------------------------- ci/travis.sh | 44 --------------------------- 6 files changed, 164 insertions(+), 89 deletions(-) create mode 100644 .github/workflows/gradle-all.yml create mode 100644 .github/workflows/gradle-main.yml create mode 100644 .github/workflows/gradle-pr.yml create mode 100644 .github/workflows/gradle-release.yml delete mode 100644 .travis.yml delete mode 100755 ci/travis.sh diff --git a/.github/workflows/gradle-all.yml b/.github/workflows/gradle-all.yml new file mode 100644 index 000000000..a5ec442ac --- /dev/null +++ b/.github/workflows/gradle-all.yml @@ -0,0 +1,45 @@ +name: Branches Java CI + +on: + # Trigger the workflow on push + # but only for the non master/1.0.x branches + push: + branches-ignore: + - 1.0.x + - master + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 11, 14 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build + - name: Publish Packages to Artifactory + if: ${{ matrix.jdk == '1.8' }} + run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PversionSuffix="-${githubRef#refs/heads/}-SNAPSHOT" -PbuildNumber="${buildNumber}" artifactoryPublish --stacktrace + env: + bintrayUser: ${{ secrets.bintrayUser }} + bintrayKey: ${{ secrets.bintrayKey }} + githubRef: ${{ github.ref }} + buildNumber: ${{ github.run_number }} \ No newline at end of file diff --git a/.github/workflows/gradle-main.yml b/.github/workflows/gradle-main.yml new file mode 100644 index 000000000..1a2c4c62a --- /dev/null +++ b/.github/workflows/gradle-main.yml @@ -0,0 +1,44 @@ +name: Main Branches Java CI + +on: + # Trigger the workflow on push + # but only for the master/1.0.x branch + push: + branches: + - master + - 1.0.x + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 14 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build + - name: Publish Packages to Artifactory + if: ${{ matrix.jdk == '1.8' }} + run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PversionSuffix="-SNAPSHOT" -PbuildNumber="${buildNumber}" artifactoryPublish --stacktrace + env: + bintrayUser: ${{ secrets.bintrayUser }} + bintrayKey: ${{ secrets.bintrayKey }} + buildNumber: ${{ github.run_number }} \ No newline at end of file diff --git a/.github/workflows/gradle-pr.yml b/.github/workflows/gradle-pr.yml new file mode 100644 index 000000000..994450faf --- /dev/null +++ b/.github/workflows/gradle-pr.yml @@ -0,0 +1,31 @@ +name: Pull Request Java CI + +on: [pull_request] + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 14 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build \ No newline at end of file diff --git a/.github/workflows/gradle-release.yml b/.github/workflows/gradle-release.yml new file mode 100644 index 000000000..847264da7 --- /dev/null +++ b/.github/workflows/gradle-release.yml @@ -0,0 +1,44 @@ +name: Release Java CI + +on: + # Trigger the workflow on push + push: + # Sequence of patterns matched against refs/tags + tags: + - '*' # Push events to matching *, i.e. 1.0, 20.15.10 + +jobs: + publish: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build + - name: Publish Packages to Bintray + run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" -Pversion="${releaseVersion}" -PbuildNumber="${buildNumber}" bintrayUpload + env: + bintrayUser: ${{ secrets.bintrayUser }} + bintrayKey: ${{ secrets.bintrayKey }} + sonatypeUsername: ${{ secrets.sonatypeUsername }} + sonatypePassword: ${{ secrets.sonatypePassword }} + releaseVersion: ${{ github.ref }} + buildNumber: ${{ github.run_number }} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4722957c8..000000000 --- a/.travis.yml +++ /dev/null @@ -1,45 +0,0 @@ -# -# Copyright 2015-2018 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---- -language: java - -dist: trusty - -matrix: - include: - - jdk: openjdk8 - - jdk: openjdk11 - env: SKIP_RELEASE=true - - jdk: openjdk14 - env: SKIP_RELEASE=true - -env: - global: - - secure: "WBCy0hsF96Xybj4n0AUrGY2m5FWCUa30XR+aVElSOO8d7v7BMypAT8mAd+yC2Y+j8WUGpIv59CqgeK1JrYdR9b3qRKhJmoE1Q92TotrxXMTIC9OKuU51LaaOqGYqx4SqiA2AyaikTFPd8um7KZfUpW/dG4IXySsiJ2OKT1jMUq6TmbWHnAYtjbl3u3WdjBQTIZNMtqG1+H1vIpsWyZrvbB4TWlNzhKBAu/YnlzMtvStrDaF7XrCJ2BQdMomQO18NH2gWxUEvLbQb6ip3wFl9CRe6vID7K1dmFwm08RPt9hRPC9yDahlIy8VvuNcWrP42TV+BVYy8V/hfaIo1pPsDBrtmVyc7YZjXSUM68orDFOkRB35qGkNIaAhy5Yt6G9QfwLXJkDFofW5KMKtDFUzf+j4DwS0CiDMF4k6Qq7YN1tYFXE9R8xa6Gv+wTNHqs4RURbYMS9IlbkhKxNbtyuema2sIUbsIfDezIzLI5BnfH2uli7O6/z0/G0Vfmf6A4q5Olm+7uhzMTI0GKheUIKr16SOxABlrwJtLJftzoKz9hYd3b7C9t61vYzccC3rWYobplwIcK2w50gFHQS8HLeiCjo8yjCx+IRSvAGaZIBPQdHCktrEYCVDUTXOxdaD6k6Ef+ppm8Nn+M+iC8x/G1wYE4x1lDqHw3GfhKsEQmiHL/98=" - - secure: "mbB+rv9eWUFQ9/yr2REH2ztH6r/Uq7cq/OJ5WK6yFp0TmPzlJ8jbEVwe/sdAMW2E4qrfMu1c2h3qsVm41pNx0MwEsIW/lTIZRiRmNYon32n+SHlRWyTn8dJeY/p1HoHs450OjLgB4X4jmRmfSt8IQ/w9ZCjF6HVcgR4ctt+myECTNcRidEIOahljnSJmnFFDsKbt2UJN96AfvvhbxcarEKgKLXLd9tQT2GlvEOM+hVOY9hKD5FvIoRp9heyCEAsSBXe+MIWQlh4jx+B4zCajZJ+8KN6M8KIt40lV8z4Zbc11jgq/xULJwkQIuVZvkJ3huIfUrxwLPgYWeai/TR/m3+2jy1hFajt96pnhJzFEz0IBL0wFALwAY1n2R/6uugEUYnDsFcGQGTsO5OeeOixiRPH5HNgfOhInqJoFh/887f+gq7OLXjlRCTsw+S9KknZ3iBpHX/+khurfAUC9khiMvufEq6Wyu0TvxhmGERFrs7uugeJ1VA85SDVQ6Au9MV831PeBGqzHpYG7w2kJj1EiFjBRUhCthxyDfX2b04egozlKF8JEifZ9EVj7pNMQUvVG2c9Wj6M0fG84NusnlZlA16XxAmfLevc9b/BOSSrqc2r9Z1ZvxFnBPP9H94Uqt9ZninhW/T49jRF+lQzD45MTVogzVk77XtdpzUemf4t5mHc=" - - secure: "GcPu3U4o2Dp7QLCqaAo3mGMJTl9yd+w+elXqqt7WDjrjm5p8mrzvQfyiJA7mRJVDTGpgib8fLctL1X1+QOX4fNKElrDUFhE3bWAqwVwHGPK4D3HCb6THD5XVqE4qcPmdLWPkvJ9ZY5nSIfuRVASjZTcc4XSXISK2jUSGar0PNYlo62/OFGvNvMz/qINU9RU7iYdDlL19yd72TKDfuK0UOKhQEGypamEHam3SMNCw/p8Q5K1vQe+Oba3ILCvYHJvqWc2NLjRXJjXfIaOq/NpCK6Lx2U9etdpkb5lyW5Cx1lkzIcRUq8ZUCwbkHog9LJoZGrZFh5AzlZ6kRuejBqu7AISmZy4s9HVAb7AQmNxvXkK9EIt8lavcaHnLYUIfuxvBqK/ptcUN5P/KXCs1DsbpADjB7YbUu/EQ2OAWncV31Z+O4uMHV29eGTtaz9LoK28+mHRfFHqoazWyuUejor6iSSkrCeqsLEvU8o6rH4oenKz7hLlZsJqHGACYtYNYi2CXYlTu0bMX+Hb1EtTu6Awm9Gn04TqVdmNexgF5CdqW4A696i6jlkPpVCt4B4nq4VPs2RMTkjVl3B7uOkDm18u35dncuhgsnMfVmo9cWX5COeyefdh6kdnKsUf0+IPbV/hix/OCP72dpuhxgcyzN+DvaVLzX7YOx7TpJTzPSKNEQZc=" - - secure: "UFJEzDEv6H2Qscg9UgZFVJq5oFvq7nQkVoSuGfh5Y4ZhL9PCK5f3Ft9oYEZOQwXaxWD1qivtJjQV3DdBiqsHkrnPrJ0hi3iYVDJo26xLNtu3welFw5Veqmgu2NuwjaDn6cjRFCJRLzpszMUWO1DvfLJTs3LuJDuXEyAKDw9eQgfOakqO4xeloyXgM7xnoXz11rgqtJNU6snjVPHftXNPTHGsNDlTR7SAIbjYwLMbdIKM2qjzrXkg+a94QOz2stnTDz9V5iYNH+3XXCcYxD9nb1Ol1XGWvtDnNGEhtGmylLdjHXwGLHiW2HOXskLzSkm7ASie1WdyHVHZb4X8LjxCy62S0FPevBgat1a443Khx5HCMYR/8dQrlOI82GYTr8n9U6QQE4Li8XLw64DVP9HGs9jdbsfEdlIsiPWqB6ujlwiO6pyfmQGQCgjALA+oD87uDQLcgh+SDYgE0ZwmwGzbjeynZpoCrEE8A1GHhSwkM9khx6EJFacm9XzqoUGK0wB1f8su+51fqPglF1zye80IFA4wOMMAY+KUc9du/vQ98f0lfjsNSOC02CKYxbA5RaakQMAYjirsZraA57xLmCSIGMhhW4wClQdJBww6LLz463yZU4WPwyqU+ZW12aV5dVLb5RWXIbZKmdT74DfZajHvqgTYpb05L5cJl7ApMspUkKk=" - -script: ci/travis.sh - -before_cache: -- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock -- rm -fr $HOME/.gradle/caches/*/plugin-resolution/ - -cache: - directories: - - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ diff --git a/ci/travis.sh b/ci/travis.sh deleted file mode 100755 index 74e26fdab..000000000 --- a/ci/travis.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash - -if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then - - echo -e "Building PR #$TRAVIS_PULL_REQUEST [$TRAVIS_PULL_REQUEST_SLUG/$TRAVIS_PULL_REQUEST_BRANCH => $TRAVIS_REPO_SLUG/$TRAVIS_BRANCH]" - ./gradlew build - -elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ] && [ "$bintrayUser" != "" ] && [ "$TRAVIS_BRANCH" == "master" ] ; then - - echo -e "Building Develop Snapshot $TRAVIS_REPO_SLUG/$TRAVIS_BRANCH/$TRAVIS_BUILD_NUMBER" - ./gradlew \ - -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" \ - -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" \ - -PversionSuffix="-SNAPSHOT" \ - -PbuildNumber="$TRAVIS_BUILD_NUMBER" \ - build artifactoryPublish --stacktrace - -elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ] && [ "$bintrayUser" != "" ] ; then - - echo -e "Building Branch Snapshot $TRAVIS_REPO_SLUG/$TRAVIS_BRANCH/$TRAVIS_BUILD_NUMBER" - ./gradlew \ - -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" \ - -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" \ - -PversionSuffix="-${TRAVIS_BRANCH//\//-}-SNAPSHOT" \ - -PbuildNumber="$TRAVIS_BUILD_NUMBER" \ - build artifactoryPublish --stacktrace - -elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ] && [ "$bintrayUser" != "" ] ; then - - echo -e "Building Tag $TRAVIS_REPO_SLUG/$TRAVIS_TAG" - ./gradlew \ - -Pversion="$TRAVIS_TAG" \ - -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" \ - -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" \ - -PbuildNumber="$TRAVIS_BUILD_NUMBER" \ - build bintrayUpload --stacktrace - -else - - echo -e "Building $TRAVIS_REPO_SLUG/$TRAVIS_BRANCH" - ./gradlew build - -fi - From 1ff3a4c0c9d91ae27818b4a72bf6ee5b7e48715d Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 14 Aug 2020 19:22:23 +0300 Subject: [PATCH 004/183] fixes javadocs Signed-off-by: Oleh Dokuka --- build.gradle | 1 + .../src/main/java/io/rsocket/core/RSocketConnector.java | 8 ++++---- .../java/io/rsocket/client/LoadBalancedRSocketMono.java | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index f91160b33..fa73f64ad 100644 --- a/build.gradle +++ b/build.gradle @@ -133,6 +133,7 @@ subprojects { links 'https://projectreactor.io/docs/core/release/api/' links 'https://netty.io/4.1/api/' } + failOnError = false } tasks.named("javadoc").configure { diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index c7caba946..692aaca7d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -190,9 +190,9 @@ public RSocketConnector dataMimeType(String dataMimeType) { *

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

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

For more on the above metadata formats, see the corresponding For server-to-server connections, a reasonable time interval between client {@code * KEEPALIVE} frames is 500ms. *

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

    By default these are set to 20 seconds and 90 seconds respectively. diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java index e6bb5d642..9b544fb24 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java @@ -37,7 +37,7 @@ * *

    It estimates the load of each RSocket based on statistics collected. * - * @deprecated as of 1.1. in favor of {@link LoadBalancedRSocketClient}. + * @deprecated as of 1.1. in favor of {@link LoadbalanceRSocketClient}. */ @Deprecated public abstract class LoadBalancedRSocketMono extends Mono From 4be122e01e5db2397fd5305d4c2a9718c8e09a20 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 14 Aug 2020 19:43:07 +0300 Subject: [PATCH 005/183] adds support for CiMate tests aggregation Signed-off-by: Oleh Dokuka --- .github/workflows/gradle-main.yml | 11 ++++++++++- build.gradle | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gradle-main.yml b/.github/workflows/gradle-main.yml index 1a2c4c62a..d8ba3c3d5 100644 --- a/.github/workflows/gradle-main.yml +++ b/.github/workflows/gradle-main.yml @@ -41,4 +41,13 @@ jobs: env: bintrayUser: ${{ secrets.bintrayUser }} bintrayKey: ${{ secrets.bintrayKey }} - buildNumber: ${{ github.run_number }} \ No newline at end of file + buildNumber: ${{ github.run_number }} + - name: Aggregate test reports with ciMate + if: always() + continue-on-error: true + env: + CIMATE_PROJECT_ID: m84qx17y + run: | + wget -q https://get.cimate.io/release/linux/cimate + chmod +x cimate + ./cimate "**/TEST-*.xml" \ No newline at end of file diff --git a/build.gradle b/build.gradle index fa73f64ad..3c63cf58b 100644 --- a/build.gradle +++ b/build.gradle @@ -178,6 +178,10 @@ subprojects { println "This is the console output of the failing test below:\n$stdOutput" } } + + reports { + junitXml.outputPerTestCase = true + } } if (JavaVersion.current().isJava9Compatible()) { From 6368faebac308324c38ac5b28f95dc02ec877170 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 19 Aug 2020 13:05:23 +0300 Subject: [PATCH 006/183] fixes workflows Signed-off-by: Oleh Dokuka --- .github/workflows/gradle-all.yml | 2 +- .github/workflows/gradle-release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gradle-all.yml b/.github/workflows/gradle-all.yml index a5ec442ac..8540539bb 100644 --- a/.github/workflows/gradle-all.yml +++ b/.github/workflows/gradle-all.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 11, 14 ] + jdk: [ 1.8, 11, 14 ] fail-fast: false steps: diff --git a/.github/workflows/gradle-release.yml b/.github/workflows/gradle-release.yml index 847264da7..08f2698dc 100644 --- a/.github/workflows/gradle-release.yml +++ b/.github/workflows/gradle-release.yml @@ -34,11 +34,11 @@ jobs: - name: Build with Gradle run: ./gradlew clean build - name: Publish Packages to Bintray - run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" -Pversion="${releaseVersion}" -PbuildNumber="${buildNumber}" bintrayUpload + run: ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" -Pversion="${githubRef#refs/tags/}" -PbuildNumber="${buildNumber}" bintrayUpload env: bintrayUser: ${{ secrets.bintrayUser }} bintrayKey: ${{ secrets.bintrayKey }} sonatypeUsername: ${{ secrets.sonatypeUsername }} sonatypePassword: ${{ secrets.sonatypePassword }} - releaseVersion: ${{ github.ref }} + githubRef: ${{ github.ref }} buildNumber: ${{ github.run_number }} \ No newline at end of file From ff9b02af183583b104ad4bca5625845e34d8ecd3 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 22 Aug 2020 10:38:11 +0300 Subject: [PATCH 007/183] ensures RRRSubscriber doesn't cancel subscription on onNext (#918) --- .../RequestResponseResponderSubscriber.java | 30 +++++++++++++++++-- .../io/rsocket/core/RSocketResponderTest.java | 13 ++++++-- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java index adc6cf48c..36177e217 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java @@ -58,6 +58,7 @@ final class RequestResponseResponderSubscriber final RSocket handler; + boolean done; CompositeByteBuf frames; volatile Subscription s; @@ -109,13 +110,24 @@ public void onSubscribe(Subscription subscription) { @Override public void onNext(@Nullable Payload p) { - if (!Operators.terminate(S, this)) { + if (this.done) { if (p != null) { p.release(); } return; } + final Subscription currentSubscription = this.s; + if (currentSubscription == Operators.cancelledSubscription() + || !S.compareAndSet(this, currentSubscription, Operators.cancelledSubscription())) { + if (p != null) { + p.release(); + } + return; + } + + this.done = true; + final int streamId = this.streamId; final UnboundedProcessor sender = this.sendProcessor; final ByteBufAllocator allocator = this.allocator; @@ -131,6 +143,8 @@ public void onNext(@Nullable Payload p) { final int mtu = this.mtu; try { if (!isValid(mtu, this.maxFrameLength, p, false)) { + currentSubscription.cancel(); + p.release(); final ByteBuf errorFrame = @@ -143,6 +157,8 @@ public void onNext(@Nullable Payload p) { return; } } catch (IllegalReferenceCountException e) { + currentSubscription.cancel(); + final ByteBuf errorFrame = ErrorFrameCodec.encode( allocator, @@ -155,16 +171,26 @@ public void onNext(@Nullable Payload p) { try { sendReleasingPayload(streamId, FrameType.NEXT_COMPLETE, mtu, p, sender, allocator, false); } catch (Throwable ignored) { + currentSubscription.cancel(); } } @Override public void onError(Throwable t) { - if (S.getAndSet(this, Operators.cancelledSubscription()) == Operators.cancelledSubscription()) { + if (this.done) { + logger.debug("Dropped error", t); + return; + } + + final Subscription currentSubscription = this.s; + if (currentSubscription == Operators.cancelledSubscription() + || !S.compareAndSet(this, currentSubscription, Operators.cancelledSubscription())) { logger.debug("Dropped error", t); return; } + this.done = true; + final int streamId = this.streamId; this.requesterResponderSupport.remove(streamId, this); 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 a648efa84..de7e48d64 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -143,17 +143,24 @@ public void testHandleKeepAlive() throws Exception { @Test @Timeout(2_000) - @Disabled public void testHandleResponseFrameNoError() throws Exception { final int streamId = 4; rule.connection.clearSendReceiveBuffers(); - + final TestPublisher testPublisher = TestPublisher.create(); + rule.setAcceptingSocket( + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + return testPublisher.mono(); + } + }); rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE); - + testPublisher.complete(); assertThat( "Unexpected frame sent.", frameType(rule.connection.awaitSend()), anyOf(is(FrameType.COMPLETE), is(FrameType.NEXT_COMPLETE))); + testPublisher.assertWasNotCancelled(); } @Test From 7077ba490742aeffc66bfeffaf6a3f944df8cd50 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 21 Aug 2020 21:11:08 +0300 Subject: [PATCH 008/183] changes LoadbalanceStrategy to accept List Signed-off-by: Oleh Dokuka --- .../loadbalance/LoadbalanceStrategy.java | 3 +- .../io/rsocket/loadbalance/RSocketPool.java | 123 +++++++++++++++++- .../RoundRobinLoadbalanceStrategy.java | 8 +- .../WeightedLoadbalanceStrategy.java | 15 ++- 4 files changed, 135 insertions(+), 14 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java index 0d09738a2..8220d1f2a 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java @@ -15,12 +15,13 @@ */ package io.rsocket.loadbalance; +import java.util.List; import java.util.function.Supplier; @FunctionalInterface public interface LoadbalanceStrategy { - PooledRSocket select(PooledRSocket[] availableRSockets); + PooledRSocket select(List availableRSockets); default Supplier statsSupplier() { return Stats::noOps; diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java index 94a42388c..cc2ce1a73 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java @@ -20,8 +20,11 @@ import io.rsocket.RSocket; import io.rsocket.frame.FrameType; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.ListIterator; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Supplier; @@ -34,7 +37,7 @@ import reactor.util.annotation.Nullable; class RSocketPool extends ResolvingOperator - implements CoreSubscriber> { + implements CoreSubscriber>, List { final DeferredResolutionRSocket deferredResolutionRSocket = new DeferredResolutionRSocket(this); final LoadbalanceStrategy loadbalanceStrategy; @@ -200,7 +203,33 @@ RSocket doSelect() { return null; } - return this.loadbalanceStrategy.select(sockets); + return this.loadbalanceStrategy.select(this); + } + + @Override + public PooledRSocket get(int index) { + return activeSockets[index]; + } + + @Override + public int size() { + return activeSockets.length; + } + + @Override + public boolean isEmpty() { + return activeSockets.length == 0; + } + + @Override + public Object[] toArray() { + return activeSockets; + } + + @Override + @SuppressWarnings("unchecked") + public T[] toArray(T[] a) { + return (T[]) activeSockets; } static class DeferredResolutionRSocket implements RSocket { @@ -325,4 +354,94 @@ public void accept(Void aVoid, Throwable t) { } } } + + @Override + public boolean contains(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean add(PooledRSocket pooledRSocket) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public PooledRSocket set(int index, PooledRSocket element) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(int index, PooledRSocket element) { + throw new UnsupportedOperationException(); + } + + @Override + public PooledRSocket remove(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOf(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public int lastIndexOf(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public ListIterator listIterator() { + throw new UnsupportedOperationException(); + } + + @Override + public ListIterator listIterator(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public List subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java index f95a91c90..13bf96e1a 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java @@ -15,6 +15,7 @@ */ package io.rsocket.loadbalance; +import java.util.List; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; public class RoundRobinLoadbalanceStrategy implements LoadbalanceStrategy { @@ -25,12 +26,11 @@ public class RoundRobinLoadbalanceStrategy implements LoadbalanceStrategy { AtomicIntegerFieldUpdater.newUpdater(RoundRobinLoadbalanceStrategy.class, "nextIndex"); @Override - public PooledRSocket select(PooledRSocket[] sockets) { - int length = sockets.length; + public PooledRSocket select(List sockets) { + int length = sockets.size(); int indexToUse = Math.abs(NEXT_INDEX.getAndIncrement(this) % length); - final PooledRSocket pooledRSocket = sockets[indexToUse]; - return pooledRSocket; + return sockets.get(indexToUse); } } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java index 265dfa247..0a5195568 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java @@ -16,6 +16,7 @@ package io.rsocket.loadbalance; +import java.util.List; import java.util.SplittableRandom; import java.util.concurrent.ThreadLocalRandom; import java.util.function.Supplier; @@ -56,19 +57,19 @@ public Supplier statsSupplier() { } @Override - public PooledRSocket select(PooledRSocket[] sockets) { + public PooledRSocket select(List sockets) { final int effort = this.effort; - final int size = sockets.length; + final int size = sockets.size(); PooledRSocket pooledRSocket; switch (size) { case 1: - pooledRSocket = sockets[0]; + pooledRSocket = sockets.get(0); break; case 2: { - PooledRSocket rsc1 = sockets[0]; - PooledRSocket rsc2 = sockets[1]; + PooledRSocket rsc1 = sockets.get(0); + PooledRSocket rsc2 = sockets.get(1); double w1 = algorithmicWeight(rsc1); double w2 = algorithmicWeight(rsc2); @@ -91,8 +92,8 @@ public PooledRSocket select(PooledRSocket[] sockets) { if (i2 >= i1) { i2++; } - rsc1 = sockets[i1]; - rsc2 = sockets[i2]; + rsc1 = sockets.get(i1); + rsc2 = sockets.get(i2); if (rsc1.availability() > 0.0 && rsc2.availability() > 0.0) { break; } From a3016ceff2869c3725f2ca8eb8e7bc834d0c5bfe Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 22 Aug 2020 11:06:05 +0300 Subject: [PATCH 009/183] renames PooledRSocket to WeightedRSocket Signed-off-by: Oleh Dokuka --- .../loadbalance/LoadbalanceStrategy.java | 2 +- ...Socket.java => PooledWeightedRSocket.java} | 39 +++++++-------- .../io/rsocket/loadbalance/RSocketPool.java | 50 +++++++++---------- .../RoundRobinLoadbalanceStrategy.java | 2 +- .../WeightedLoadbalanceStrategy.java | 36 ++++++------- ...ooledRSocket.java => WeightedRSocket.java} | 4 +- 6 files changed, 65 insertions(+), 68 deletions(-) rename rsocket-core/src/main/java/io/rsocket/loadbalance/{DefaultPooledRSocket.java => PooledWeightedRSocket.java} (87%) rename rsocket-core/src/main/java/io/rsocket/loadbalance/{PooledRSocket.java => WeightedRSocket.java} (88%) diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java index 8220d1f2a..2bcf4455b 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java @@ -21,7 +21,7 @@ @FunctionalInterface public interface LoadbalanceStrategy { - PooledRSocket select(List availableRSockets); + WeightedRSocket select(List availableRSockets); default Supplier statsSupplier() { return Stats::noOps; diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/DefaultPooledRSocket.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledWeightedRSocket.java similarity index 87% rename from rsocket-core/src/main/java/io/rsocket/loadbalance/DefaultPooledRSocket.java rename to rsocket-core/src/main/java/io/rsocket/loadbalance/PooledWeightedRSocket.java index a7ba9dfef..0cd5952a2 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/DefaultPooledRSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledWeightedRSocket.java @@ -28,9 +28,9 @@ import reactor.core.publisher.Operators; import reactor.util.context.Context; -/** Default implementation of {@link PooledRSocket} stored in {@link RSocketPool} */ -final class DefaultPooledRSocket extends ResolvingOperator - implements CoreSubscriber, PooledRSocket { +/** Default implementation of {@link WeightedRSocket} stored in {@link RSocketPool} */ +final class PooledWeightedRSocket extends ResolvingOperator + implements CoreSubscriber, WeightedRSocket { final RSocketPool parent; final LoadbalanceRSocketSource loadbalanceRSocketSource; @@ -38,10 +38,10 @@ final class DefaultPooledRSocket extends ResolvingOperator volatile Subscription s; - static final AtomicReferenceFieldUpdater S = - AtomicReferenceFieldUpdater.newUpdater(DefaultPooledRSocket.class, Subscription.class, "s"); + static final AtomicReferenceFieldUpdater S = + AtomicReferenceFieldUpdater.newUpdater(PooledWeightedRSocket.class, Subscription.class, "s"); - DefaultPooledRSocket( + PooledWeightedRSocket( RSocketPool parent, LoadbalanceRSocketSource loadbalanceRSocketSource, Stats stats) { this.parent = parent; this.stats = stats; @@ -128,7 +128,7 @@ public void dispose() { protected void doOnDispose() { final RSocketPool parent = this.parent; for (; ; ) { - final PooledRSocket[] sockets = parent.activeSockets; + final PooledWeightedRSocket[] sockets = parent.activeSockets; final int activeSocketsCount = sockets.length; int index = -1; @@ -144,7 +144,7 @@ protected void doOnDispose() { } final int lastIndex = activeSocketsCount - 1; - final PooledRSocket[] newSockets = new PooledRSocket[lastIndex]; + final PooledWeightedRSocket[] newSockets = new PooledWeightedRSocket[lastIndex]; if (index != 0) { System.arraycopy(sockets, 0, newSockets, 0, index); } @@ -196,8 +196,7 @@ public Stats stats() { return stats; } - @Override - public LoadbalanceRSocketSource source() { + LoadbalanceRSocketSource source() { return loadbalanceRSocketSource; } @@ -211,7 +210,7 @@ static final class RequestTrackingMonoInner long startTime; - RequestTrackingMonoInner(DefaultPooledRSocket parent, Payload payload, FrameType requestType) { + RequestTrackingMonoInner(PooledWeightedRSocket parent, Payload payload, FrameType requestType) { super(parent, payload, requestType); } @@ -245,7 +244,7 @@ public void accept(RSocket rSocket, Throwable t) { return; } - startTime = ((DefaultPooledRSocket) parent).stats.startRequest(); + startTime = ((PooledWeightedRSocket) parent).stats.startRequest(); source.subscribe((CoreSubscriber) this); } else { @@ -257,7 +256,7 @@ public void accept(RSocket rSocket, Throwable t) { public void onComplete() { final long state = this.requested; if (state != TERMINATED_STATE && REQUESTED.compareAndSet(this, state, TERMINATED_STATE)) { - final Stats stats = ((DefaultPooledRSocket) parent).stats; + final Stats stats = ((PooledWeightedRSocket) parent).stats; final long now = stats.stopRequest(startTime); stats.record(now - startTime); super.onComplete(); @@ -268,7 +267,7 @@ public void onComplete() { public void onError(Throwable t) { final long state = this.requested; if (state != TERMINATED_STATE && REQUESTED.compareAndSet(this, state, TERMINATED_STATE)) { - Stats stats = ((DefaultPooledRSocket) parent).stats; + Stats stats = ((PooledWeightedRSocket) parent).stats; stats.stopRequest(startTime); stats.recordError(0.0); super.onError(t); @@ -284,7 +283,7 @@ public void cancel() { if (state == STATE_SUBSCRIBED) { this.s.cancel(); - ((DefaultPooledRSocket) parent).stats.stopRequest(startTime); + ((PooledWeightedRSocket) parent).stats.stopRequest(startTime); } else { this.parent.remove(this); ReferenceCountUtil.safeRelease(this.payload); @@ -296,7 +295,7 @@ static final class RequestTrackingFluxInner extends FluxDeferredResolution { RequestTrackingFluxInner( - DefaultPooledRSocket parent, INPUT fluxOrPayload, FrameType requestType) { + PooledWeightedRSocket parent, INPUT fluxOrPayload, FrameType requestType) { super(parent, fluxOrPayload, requestType); } @@ -329,7 +328,7 @@ public void accept(RSocket rSocket, Throwable t) { return; } - ((DefaultPooledRSocket) parent).stats.startStream(); + ((PooledWeightedRSocket) parent).stats.startStream(); source.subscribe(this); } else { @@ -341,7 +340,7 @@ public void accept(RSocket rSocket, Throwable t) { public void onComplete() { final long state = this.requested; if (state != TERMINATED_STATE && REQUESTED.compareAndSet(this, state, TERMINATED_STATE)) { - ((DefaultPooledRSocket) parent).stats.stopStream(); + ((PooledWeightedRSocket) parent).stats.stopStream(); super.onComplete(); } } @@ -350,7 +349,7 @@ public void onComplete() { public void onError(Throwable t) { final long state = this.requested; if (state != TERMINATED_STATE && REQUESTED.compareAndSet(this, state, TERMINATED_STATE)) { - ((DefaultPooledRSocket) parent).stats.stopStream(); + ((PooledWeightedRSocket) parent).stats.stopStream(); super.onError(t); } } @@ -364,7 +363,7 @@ public void cancel() { if (state == STATE_SUBSCRIBED) { this.s.cancel(); - ((DefaultPooledRSocket) parent).stats.stopStream(); + ((PooledWeightedRSocket) parent).stats.stopStream(); } else { this.parent.remove(this); if (requestType == FrameType.REQUEST_STREAM) { diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java index cc2ce1a73..35a38f3b4 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java @@ -37,20 +37,20 @@ import reactor.util.annotation.Nullable; class RSocketPool extends ResolvingOperator - implements CoreSubscriber>, List { + implements CoreSubscriber>, List { final DeferredResolutionRSocket deferredResolutionRSocket = new DeferredResolutionRSocket(this); final LoadbalanceStrategy loadbalanceStrategy; final Supplier statsSupplier; - volatile PooledRSocket[] activeSockets; + volatile PooledWeightedRSocket[] activeSockets; - static final AtomicReferenceFieldUpdater ACTIVE_SOCKETS = + static final AtomicReferenceFieldUpdater ACTIVE_SOCKETS = AtomicReferenceFieldUpdater.newUpdater( - RSocketPool.class, PooledRSocket[].class, "activeSockets"); + RSocketPool.class, PooledWeightedRSocket[].class, "activeSockets"); - static final PooledRSocket[] EMPTY = new PooledRSocket[0]; - static final PooledRSocket[] TERMINATED = new PooledRSocket[0]; + static final PooledWeightedRSocket[] EMPTY = new PooledWeightedRSocket[0]; + static final PooledWeightedRSocket[] TERMINATED = new PooledWeightedRSocket[0]; volatile Subscription s; static final AtomicReferenceFieldUpdater S = @@ -96,8 +96,8 @@ public void onNext(List loadbalanceRSocketSources) { return; } - PooledRSocket[] previouslyActiveSockets; - PooledRSocket[] activeSockets; + PooledWeightedRSocket[] previouslyActiveSockets; + PooledWeightedRSocket[] activeSockets; for (; ; ) { HashMap rSocketSuppliersCopy = new HashMap<>(); @@ -108,11 +108,11 @@ public void onNext(List loadbalanceRSocketSources) { // checking intersection of active RSocket with the newly received set previouslyActiveSockets = this.activeSockets; - PooledRSocket[] nextActiveSockets = - new PooledRSocket[previouslyActiveSockets.length + rSocketSuppliersCopy.size()]; + PooledWeightedRSocket[] nextActiveSockets = + new PooledWeightedRSocket[previouslyActiveSockets.length + rSocketSuppliersCopy.size()]; int position = 0; for (int i = 0; i < previouslyActiveSockets.length; i++) { - PooledRSocket rSocket = previouslyActiveSockets[i]; + PooledWeightedRSocket rSocket = previouslyActiveSockets[i]; Integer index = rSocketSuppliersCopy.remove(rSocket.source()); if (index == null) { @@ -130,7 +130,7 @@ public void onNext(List loadbalanceRSocketSources) { } else { // put newly create RSocket instance nextActiveSockets[position++] = - new DefaultPooledRSocket( + new PooledWeightedRSocket( this, loadbalanceRSocketSources.get(index), this.statsSupplier.get()); } } @@ -139,7 +139,7 @@ public void onNext(List loadbalanceRSocketSources) { // going though brightly new rsocket for (LoadbalanceRSocketSource newLoadbalanceRSocketSource : rSocketSuppliersCopy.keySet()) { nextActiveSockets[position++] = - new DefaultPooledRSocket(this, newLoadbalanceRSocketSource, this.statsSupplier.get()); + new PooledWeightedRSocket(this, newLoadbalanceRSocketSource, this.statsSupplier.get()); } // shrank to actual length @@ -198,7 +198,7 @@ RSocket select() { @Nullable RSocket doSelect() { - PooledRSocket[] sockets = this.activeSockets; + WeightedRSocket[] sockets = this.activeSockets; if (sockets == EMPTY) { return null; } @@ -207,7 +207,7 @@ RSocket doSelect() { } @Override - public PooledRSocket get(int index) { + public WeightedRSocket get(int index) { return activeSockets[index]; } @@ -361,12 +361,12 @@ public boolean contains(Object o) { } @Override - public Iterator iterator() { + public Iterator iterator() { throw new UnsupportedOperationException(); } @Override - public boolean add(PooledRSocket pooledRSocket) { + public boolean add(WeightedRSocket weightedRSocket) { throw new UnsupportedOperationException(); } @@ -381,12 +381,12 @@ public boolean containsAll(Collection c) { } @Override - public boolean addAll(Collection c) { + public boolean addAll(Collection c) { throw new UnsupportedOperationException(); } @Override - public boolean addAll(int index, Collection c) { + public boolean addAll(int index, Collection c) { throw new UnsupportedOperationException(); } @@ -406,17 +406,17 @@ public void clear() { } @Override - public PooledRSocket set(int index, PooledRSocket element) { + public WeightedRSocket set(int index, WeightedRSocket element) { throw new UnsupportedOperationException(); } @Override - public void add(int index, PooledRSocket element) { + public void add(int index, WeightedRSocket element) { throw new UnsupportedOperationException(); } @Override - public PooledRSocket remove(int index) { + public WeightedRSocket remove(int index) { throw new UnsupportedOperationException(); } @@ -431,17 +431,17 @@ public int lastIndexOf(Object o) { } @Override - public ListIterator listIterator() { + public ListIterator listIterator() { throw new UnsupportedOperationException(); } @Override - public ListIterator listIterator(int index) { + public ListIterator listIterator(int index) { throw new UnsupportedOperationException(); } @Override - public List subList(int fromIndex, int toIndex) { + public List subList(int fromIndex, int toIndex) { throw new UnsupportedOperationException(); } } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java index 13bf96e1a..60227f9ac 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java @@ -26,7 +26,7 @@ public class RoundRobinLoadbalanceStrategy implements LoadbalanceStrategy { AtomicIntegerFieldUpdater.newUpdater(RoundRobinLoadbalanceStrategy.class, "nextIndex"); @Override - public PooledRSocket select(List sockets) { + public WeightedRSocket select(List sockets) { int length = sockets.size(); int indexToUse = Math.abs(NEXT_INDEX.getAndIncrement(this) % length); diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java index 0a5195568..590da3ded 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java @@ -57,33 +57,33 @@ public Supplier statsSupplier() { } @Override - public PooledRSocket select(List sockets) { + public WeightedRSocket select(List sockets) { final int effort = this.effort; final int size = sockets.size(); - PooledRSocket pooledRSocket; + WeightedRSocket weightedRSocket; switch (size) { case 1: - pooledRSocket = sockets.get(0); + weightedRSocket = sockets.get(0); break; case 2: { - PooledRSocket rsc1 = sockets.get(0); - PooledRSocket rsc2 = sockets.get(1); + WeightedRSocket rsc1 = sockets.get(0); + WeightedRSocket rsc2 = sockets.get(1); double w1 = algorithmicWeight(rsc1); double w2 = algorithmicWeight(rsc2); if (w1 < w2) { - pooledRSocket = rsc2; + weightedRSocket = rsc2; } else { - pooledRSocket = rsc1; + weightedRSocket = rsc1; } } break; default: { - PooledRSocket rsc1 = null; - PooledRSocket rsc2 = null; + WeightedRSocket rsc1 = null; + WeightedRSocket rsc2 = null; for (int i = 0; i < effort; i++) { int i1 = ThreadLocalRandom.current().nextInt(size); @@ -102,23 +102,23 @@ public PooledRSocket select(List sockets) { double w1 = algorithmicWeight(rsc1); double w2 = algorithmicWeight(rsc2); if (w1 < w2) { - pooledRSocket = rsc2; + weightedRSocket = rsc2; } else { - pooledRSocket = rsc1; + weightedRSocket = rsc1; } } } - return pooledRSocket; + return weightedRSocket; } - private static double algorithmicWeight(@Nullable final PooledRSocket pooledRSocket) { - if (pooledRSocket == null - || pooledRSocket.isDisposed() - || pooledRSocket.availability() == 0.0) { + private static double algorithmicWeight(@Nullable final WeightedRSocket weightedRSocket) { + if (weightedRSocket == null + || weightedRSocket.isDisposed() + || weightedRSocket.availability() == 0.0) { return 0.0; } - final Stats stats = pooledRSocket.stats(); + final Stats stats = weightedRSocket.stats(); final int pending = stats.pending(); double latency = stats.predictedLatency(); @@ -135,7 +135,7 @@ private static double algorithmicWeight(@Nullable final PooledRSocket pooledRSoc latency *= calculateFactor(latency, high, bandWidth); } - return pooledRSocket.availability() * 1.0 / (1.0 + latency * (pending + 1)); + return weightedRSocket.availability() * 1.0 / (1.0 + latency * (pending + 1)); } private static double calculateFactor(final double u, final double l, final double bandWidth) { diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedRSocket.java similarity index 88% rename from rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java rename to rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedRSocket.java index 825a8ac88..de9e56fa4 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedRSocket.java @@ -17,9 +17,7 @@ import io.rsocket.RSocket; -public interface PooledRSocket extends RSocket { +public interface WeightedRSocket extends RSocket { Stats stats(); - - LoadbalanceRSocketSource source(); } From 1c8bdadbb1758547fe53c29809f1867bd8097a90 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 22 Aug 2020 13:45:35 +0300 Subject: [PATCH 010/183] exposes part of the Stats API Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/loadbalance/Stats.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/Stats.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/Stats.java index c8f40cbf2..12f1d1c3e 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/Stats.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/Stats.java @@ -138,15 +138,15 @@ synchronized long instantaneous(long now) { return duration + (now - stamp0) * pending; } - void startStream() { + public void startStream() { PENDING_STREAMS.incrementAndGet(this); } - void stopStream() { + public void stopStream() { PENDING_STREAMS.decrementAndGet(this); } - synchronized long startRequest() { + public synchronized long startRequest() { long now = Clock.now(); interArrivalTime.insert(now - stamp); duration += Math.max(0, now - stamp0) * pending; @@ -156,7 +156,7 @@ synchronized long startRequest() { return now; } - synchronized long stopRequest(long timestamp) { + public synchronized long stopRequest(long timestamp) { long now = Clock.now(); duration += Math.max(0, now - stamp0) * pending - (now - timestamp); pending -= 1; @@ -164,18 +164,18 @@ synchronized long stopRequest(long timestamp) { return now; } - synchronized void record(double roundTripTime) { + public synchronized void record(double roundTripTime) { median.insert(roundTripTime); lowerQuantile.insert(roundTripTime); higherQuantile.insert(roundTripTime); } - synchronized void recordError(double value) { + public synchronized void recordError(double value) { errorPercentage.insert(value); errorStamp = Clock.now(); } - void setAvailability(double availability) { + public void setAvailability(double availability) { this.availability = availability; } @@ -270,26 +270,26 @@ long instantaneous(long now) { } @Override - void startStream() {} + public void startStream() {} @Override - void stopStream() {} + public void stopStream() {} @Override - long startRequest() { + public long startRequest() { return 0; } @Override - long stopRequest(long timestamp) { + public long stopRequest(long timestamp) { return 0; } @Override - void record(double roundTripTime) {} + public void record(double roundTripTime) {} @Override - void recordError(double value) {} + public void recordError(double value) {} @Override public String toString() { From 92134e82424841e3d66747dadc8d08d69d080adb Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 28 Aug 2020 14:24:36 +0300 Subject: [PATCH 011/183] makes part of the API package-private (#922) Signed-off-by: Oleh Dokuka --- .../rsocket/loadbalance/{stat => }/Ewma.java | 4 +-- .../{stat => }/FrugalQuantile.java | 4 +-- .../loadbalance/LoadbalanceRSocketClient.java | 32 ++++++++++++++++++- .../loadbalance/LoadbalanceStrategy.java | 8 ++--- .../loadbalance/{stat => }/Median.java | 4 +-- .../loadbalance/{stat => }/Quantile.java | 4 +-- .../io/rsocket/loadbalance/RSocketPool.java | 26 ++++++++------- .../RoundRobinLoadbalanceStrategy.java | 5 +-- .../java/io/rsocket/loadbalance/Stats.java | 6 +--- .../WeightedLoadbalanceStrategy.java | 20 +++++------- .../rsocket/loadbalance/WeightedRSocket.java | 2 +- .../loadbalance/stat/package-info.java | 20 ------------ .../RoundRobinRSocketLoadbalancerExample.java | 3 +- .../client/LoadBalancedRSocketMono.java | 5 +-- 14 files changed, 73 insertions(+), 70 deletions(-) rename rsocket-core/src/main/java/io/rsocket/loadbalance/{stat => }/Ewma.java (97%) rename rsocket-core/src/main/java/io/rsocket/loadbalance/{stat => }/FrugalQuantile.java (96%) rename rsocket-core/src/main/java/io/rsocket/loadbalance/{stat => }/Median.java (95%) rename rsocket-core/src/main/java/io/rsocket/loadbalance/{stat => }/Quantile.java (92%) delete mode 100644 rsocket-core/src/main/java/io/rsocket/loadbalance/stat/package-info.java diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/stat/Ewma.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/Ewma.java similarity index 97% rename from rsocket-core/src/main/java/io/rsocket/loadbalance/stat/Ewma.java rename to rsocket-core/src/main/java/io/rsocket/loadbalance/Ewma.java index efdc20da0..4812114dd 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/stat/Ewma.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/Ewma.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.rsocket.loadbalance.stat; +package io.rsocket.loadbalance; import io.rsocket.util.Clock; import java.util.concurrent.TimeUnit; @@ -27,7 +27,7 @@ *

    e.g. with a half-life of 10 unit, if you insert 100 at t=0 and 200 at t=10 the ewma will be * equal to (200 - 100)/2 = 150 (half of the distance between the new and the old value) */ -public class Ewma { +class Ewma { private final long tau; private volatile long stamp; private volatile double ewma; diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/stat/FrugalQuantile.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/FrugalQuantile.java similarity index 96% rename from rsocket-core/src/main/java/io/rsocket/loadbalance/stat/FrugalQuantile.java rename to rsocket-core/src/main/java/io/rsocket/loadbalance/FrugalQuantile.java index 7dcebb424..efa32ff83 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/stat/FrugalQuantile.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/FrugalQuantile.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.rsocket.loadbalance.stat; +package io.rsocket.loadbalance; import java.util.SplittableRandom; @@ -25,7 +25,7 @@ * *

    More info: http://blog.aggregateknowledge.com/2013/09/16/sketch-of-the-day-frugal-streaming/ */ -public class FrugalQuantile implements Quantile { +class FrugalQuantile implements Quantile { private final double increment; volatile double estimate; int step; 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 e865afb29..4b19550bd 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java @@ -79,8 +79,38 @@ public static LoadbalanceRSocketClient create( new RSocketPool(rSocketSuppliersPublisher, loadbalanceStrategy)); } - public static RSocketClient create( + public static LoadbalanceRSocketClient create( Publisher> rSocketSuppliersPublisher) { return create(new RoundRobinLoadbalanceStrategy(), rSocketSuppliersPublisher); } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + LoadbalanceStrategy loadbalanceStrategy; + + Builder() {} + + public Builder withWeightedLoadbalanceStrategy() { + return withCustomLoadbalanceStrategy(new WeightedLoadbalanceStrategy()); + } + + public Builder withRoundRobinLoadbalanceStrategy() { + return withCustomLoadbalanceStrategy(new RoundRobinLoadbalanceStrategy()); + } + + public Builder withCustomLoadbalanceStrategy(LoadbalanceStrategy strategy) { + this.loadbalanceStrategy = strategy; + return this; + } + + public LoadbalanceRSocketClient build( + Publisher> rSocketSuppliersPublisher) { + return new LoadbalanceRSocketClient( + new RSocketPool(rSocketSuppliersPublisher, this.loadbalanceStrategy)); + } + } } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java index 2bcf4455b..2a333959b 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceStrategy.java @@ -15,15 +15,11 @@ */ package io.rsocket.loadbalance; +import io.rsocket.RSocket; import java.util.List; -import java.util.function.Supplier; @FunctionalInterface public interface LoadbalanceStrategy { - WeightedRSocket select(List availableRSockets); - - default Supplier statsSupplier() { - return Stats::noOps; - } + RSocket select(List availableRSockets); } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/stat/Median.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/Median.java similarity index 95% rename from rsocket-core/src/main/java/io/rsocket/loadbalance/stat/Median.java rename to rsocket-core/src/main/java/io/rsocket/loadbalance/Median.java index 5d7c7d034..833bd5380 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/stat/Median.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/Median.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.rsocket.loadbalance.stat; +package io.rsocket.loadbalance; /** This implementation gives better results because it considers more data-point. */ -public class Median extends FrugalQuantile { +class Median extends FrugalQuantile { public Median() { super(0.5, 1.0); } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/stat/Quantile.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/Quantile.java similarity index 92% rename from rsocket-core/src/main/java/io/rsocket/loadbalance/stat/Quantile.java rename to rsocket-core/src/main/java/io/rsocket/loadbalance/Quantile.java index bfaad9e62..84c699197 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/stat/Quantile.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/Quantile.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.loadbalance.stat; +package io.rsocket.loadbalance; -public interface Quantile { +interface Quantile { /** @return the estimation of the current value of the quantile */ double estimation(); diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java index 35a38f3b4..dc776852c 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java @@ -37,7 +37,7 @@ import reactor.util.annotation.Nullable; class RSocketPool extends ResolvingOperator - implements CoreSubscriber>, List { + implements CoreSubscriber>, List { final DeferredResolutionRSocket deferredResolutionRSocket = new DeferredResolutionRSocket(this); final LoadbalanceStrategy loadbalanceStrategy; @@ -59,7 +59,11 @@ class RSocketPool extends ResolvingOperator RSocketPool( Publisher> source, LoadbalanceStrategy loadbalanceStrategy) { this.loadbalanceStrategy = loadbalanceStrategy; - this.statsSupplier = loadbalanceStrategy.statsSupplier(); + if (loadbalanceStrategy instanceof WeightedLoadbalanceStrategy) { + this.statsSupplier = Stats::create; + } else { + this.statsSupplier = Stats::noOps; + } ACTIVE_SOCKETS.lazySet(this, EMPTY); @@ -361,12 +365,12 @@ public boolean contains(Object o) { } @Override - public Iterator iterator() { + public Iterator iterator() { throw new UnsupportedOperationException(); } @Override - public boolean add(WeightedRSocket weightedRSocket) { + public boolean add(RSocket weightedRSocket) { throw new UnsupportedOperationException(); } @@ -381,12 +385,12 @@ public boolean containsAll(Collection c) { } @Override - public boolean addAll(Collection c) { + public boolean addAll(Collection c) { throw new UnsupportedOperationException(); } @Override - public boolean addAll(int index, Collection c) { + public boolean addAll(int index, Collection c) { throw new UnsupportedOperationException(); } @@ -406,12 +410,12 @@ public void clear() { } @Override - public WeightedRSocket set(int index, WeightedRSocket element) { + public WeightedRSocket set(int index, RSocket element) { throw new UnsupportedOperationException(); } @Override - public void add(int index, WeightedRSocket element) { + public void add(int index, RSocket element) { throw new UnsupportedOperationException(); } @@ -431,17 +435,17 @@ public int lastIndexOf(Object o) { } @Override - public ListIterator listIterator() { + public ListIterator listIterator() { throw new UnsupportedOperationException(); } @Override - public ListIterator listIterator(int index) { + public ListIterator listIterator(int index) { throw new UnsupportedOperationException(); } @Override - public List subList(int fromIndex, int toIndex) { + public List subList(int fromIndex, int toIndex) { throw new UnsupportedOperationException(); } } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java index 60227f9ac..0e1c541f2 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java @@ -15,10 +15,11 @@ */ package io.rsocket.loadbalance; +import io.rsocket.RSocket; import java.util.List; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -public class RoundRobinLoadbalanceStrategy implements LoadbalanceStrategy { +class RoundRobinLoadbalanceStrategy implements LoadbalanceStrategy { volatile int nextIndex; @@ -26,7 +27,7 @@ public class RoundRobinLoadbalanceStrategy implements LoadbalanceStrategy { AtomicIntegerFieldUpdater.newUpdater(RoundRobinLoadbalanceStrategy.class, "nextIndex"); @Override - public WeightedRSocket select(List sockets) { + public RSocket select(List sockets) { int length = sockets.size(); int indexToUse = Math.abs(NEXT_INDEX.getAndIncrement(this) % length); diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/Stats.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/Stats.java index 12f1d1c3e..2e9828938 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/Stats.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/Stats.java @@ -1,15 +1,11 @@ package io.rsocket.loadbalance; import io.rsocket.Availability; -import io.rsocket.loadbalance.stat.Ewma; -import io.rsocket.loadbalance.stat.FrugalQuantile; -import io.rsocket.loadbalance.stat.Median; -import io.rsocket.loadbalance.stat.Quantile; import io.rsocket.util.Clock; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLongFieldUpdater; -public class Stats implements Availability { +class Stats implements Availability { private static final double DEFAULT_LOWER_QUANTILE = 0.5; private static final double DEFAULT_HIGHER_QUANTILE = 0.8; diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java index 590da3ded..44ffbd6f5 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java @@ -16,13 +16,14 @@ package io.rsocket.loadbalance; +import io.rsocket.RSocket; import java.util.List; import java.util.SplittableRandom; import java.util.concurrent.ThreadLocalRandom; import java.util.function.Supplier; import reactor.util.annotation.Nullable; -public class WeightedLoadbalanceStrategy implements LoadbalanceStrategy { +class WeightedLoadbalanceStrategy implements LoadbalanceStrategy { private static final double EXP_FACTOR = 4.0; @@ -52,24 +53,19 @@ public WeightedLoadbalanceStrategy( } @Override - public Supplier statsSupplier() { - return this.statsSupplier; - } - - @Override - public WeightedRSocket select(List sockets) { + public RSocket select(List sockets) { final int effort = this.effort; final int size = sockets.size(); WeightedRSocket weightedRSocket; switch (size) { case 1: - weightedRSocket = sockets.get(0); + weightedRSocket = (WeightedRSocket) sockets.get(0); break; case 2: { - WeightedRSocket rsc1 = sockets.get(0); - WeightedRSocket rsc2 = sockets.get(1); + WeightedRSocket rsc1 = (WeightedRSocket) sockets.get(0); + WeightedRSocket rsc2 = (WeightedRSocket) sockets.get(1); double w1 = algorithmicWeight(rsc1); double w2 = algorithmicWeight(rsc2); @@ -92,8 +88,8 @@ public WeightedRSocket select(List sockets) { if (i2 >= i1) { i2++; } - rsc1 = sockets.get(i1); - rsc2 = sockets.get(i2); + rsc1 = (WeightedRSocket) sockets.get(i1); + rsc2 = (WeightedRSocket) sockets.get(i2); if (rsc1.availability() > 0.0 && rsc2.availability() > 0.0) { break; } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedRSocket.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedRSocket.java index de9e56fa4..488a7134d 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedRSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedRSocket.java @@ -17,7 +17,7 @@ import io.rsocket.RSocket; -public interface WeightedRSocket extends RSocket { +interface WeightedRSocket extends RSocket { Stats stats(); } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/stat/package-info.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/stat/package-info.java deleted file mode 100644 index 20c2c9d73..000000000 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/stat/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@NonNullApi -package io.rsocket.loadbalance.stat; - -import reactor.util.annotation.NonNullApi; diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java index 7a55c8274..2c3fd831d 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java @@ -6,7 +6,6 @@ import io.rsocket.core.RSocketServer; import io.rsocket.loadbalance.LoadbalanceRSocketClient; import io.rsocket.loadbalance.LoadbalanceRSocketSource; -import io.rsocket.loadbalance.RoundRobinLoadbalanceStrategy; import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; @@ -120,7 +119,7 @@ public static void main(String[] args) { }); RSocketClient loadBalancedRSocketClient = - LoadbalanceRSocketClient.create(new RoundRobinLoadbalanceStrategy(), producer); + LoadbalanceRSocketClient.builder().withRoundRobinLoadbalanceStrategy().build(producer); for (int i = 0; i < 10000; i++) { try { diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java index 9b544fb24..55b40fe39 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java @@ -22,7 +22,6 @@ import io.rsocket.client.filter.RSocketSupplier; import io.rsocket.loadbalance.LoadbalanceRSocketClient; import io.rsocket.loadbalance.LoadbalanceRSocketSource; -import io.rsocket.loadbalance.WeightedLoadbalanceStrategy; import java.util.Collection; import java.util.stream.Collectors; import org.reactivestreams.Publisher; @@ -79,7 +78,9 @@ public static LoadBalancedRSocketMono create( rsl.stream() .map(rs -> LoadbalanceRSocketSource.from(rs.toString(), rs.get())) .collect(Collectors.toList())) - .as(f -> LoadbalanceRSocketClient.create(new WeightedLoadbalanceStrategy(), f))); + .as( + f -> + LoadbalanceRSocketClient.builder().withWeightedLoadbalanceStrategy().build(f))); } public static LoadBalancedRSocketMono fromClient(LoadbalanceRSocketClient rSocketClient) { From 39be15d44a6e364a15138abba980d6ed51bc8c5d Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 1 Sep 2020 18:54:43 +0100 Subject: [PATCH 012/183] Refactors the input for LoadbalanceRSocketClient (#924) Signed-off-by: Rossen Stoyanchev --- .../loadbalance/LoadbalanceRSocketClient.java | 112 ++- .../loadbalance/LoadbalanceRSocketSource.java | 54 - .../loadbalance/LoadbalanceTarget.java | 76 ++ .../loadbalance/PooledWeightedRSocket.java | 17 +- .../io/rsocket/loadbalance/RSocketPool.java | 39 +- .../RoundRobinRSocketLoadbalancerExample.java | 84 +- .../client/LoadBalancedRSocketMono.java | 947 +++++++++++++++++- 7 files changed, 1144 insertions(+), 185 deletions(-) delete mode 100644 rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketSource.java create mode 100644 rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceTarget.java 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 4b19550bd..18600e633 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java @@ -18,14 +18,16 @@ import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.RSocketClient; +import io.rsocket.core.RSocketConnector; import java.util.List; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.annotation.Nullable; /** - * {@link RSocketClient} implementation that uses a pool and a {@link LoadbalanceStrategy} to select - * the {@code RSocket} to use for each request. + * {@link RSocketClient} implementation that uses a {@link LoadbalanceStrategy} to select the {@code + * RSocket} to use for a given request from a pool of possible targets. * * @since 1.1 */ @@ -72,45 +74,109 @@ public void dispose() { rSocketPool.dispose(); } + /** + * Shortcut to create an {@link LoadbalanceRSocketClient} with round robin loadalancing. + * Effectively a shortcut for: + * + *

    +   * LoadbalanceRSocketClient.builder(targetPublisher)
    +   *    .connector(RSocketConnector.create())
    +   *    .build();
    +   * 
    + * + * @param connector the {@link Builder#connector(RSocketConnector) to use + * @param targetPublisher publisher that periodically refreshes the list of targets to loadbalance across. + * @return the created client instance + */ public static LoadbalanceRSocketClient create( - LoadbalanceStrategy loadbalanceStrategy, - Publisher> rSocketSuppliersPublisher) { - return new LoadbalanceRSocketClient( - new RSocketPool(rSocketSuppliersPublisher, loadbalanceStrategy)); + RSocketConnector connector, Publisher> targetPublisher) { + return builder(targetPublisher).connector(connector).build(); } - public static LoadbalanceRSocketClient create( - Publisher> rSocketSuppliersPublisher) { - return create(new RoundRobinLoadbalanceStrategy(), rSocketSuppliersPublisher); - } - - public static Builder builder() { - return new Builder(); + /** + * Return a builder to create an {@link LoadbalanceRSocketClient} with. + * + * @param targetPublisher publisher that periodically refreshes the list of targets to loadbalance + * across. + * @return the builder instance + */ + public static Builder builder(Publisher> targetPublisher) { + return new Builder(targetPublisher); } + /** Builder for creating an {@link LoadbalanceRSocketClient}. */ public static class Builder { - LoadbalanceStrategy loadbalanceStrategy; + private final Publisher> targetPublisher; + + @Nullable private RSocketConnector connector; - Builder() {} + @Nullable LoadbalanceStrategy loadbalanceStrategy; - public Builder withWeightedLoadbalanceStrategy() { - return withCustomLoadbalanceStrategy(new WeightedLoadbalanceStrategy()); + Builder(Publisher> targetPublisher) { + this.targetPublisher = targetPublisher; } - public Builder withRoundRobinLoadbalanceStrategy() { - return withCustomLoadbalanceStrategy(new RoundRobinLoadbalanceStrategy()); + /** + * The given {@link RSocketConnector} is used as a template to produce the {@code Mono} + * source for each {@link LoadbalanceTarget}. This is done by passing the {@code + * ClientTransport} contained in every target to the {@code connect} method of the given + * connector instance. + * + *

    By default this is initialized with {@link RSocketConnector#create()}. + * + * @param connector the connector to use as a template + */ + public Builder connector(RSocketConnector connector) { + this.connector = connector; + return this; + } + + /** + * Switch to using a round-robin strategy for selecting a target. + * + *

    This is the strategy used by default. + */ + public Builder roundRobinLoadbalanceStrategy() { + this.loadbalanceStrategy = new RoundRobinLoadbalanceStrategy(); + return this; } - public Builder withCustomLoadbalanceStrategy(LoadbalanceStrategy strategy) { + /** + * Switch to using a strategy that assigns a weight to each pooled {@code RSocket} based on + * actual usage stats, and uses that to make a choice. + * + *

    By default this strategy is not used. + */ + public Builder weightedLoadbalanceStrategy() { + this.loadbalanceStrategy = new WeightedLoadbalanceStrategy(); + return this; + } + + /** + * Switch to using a custom strategy for loadbalancing. + * + * @see #roundRobinLoadbalanceStrategy() + */ + public Builder customLoadbalanceStrategy(LoadbalanceStrategy strategy) { this.loadbalanceStrategy = strategy; return this; } - public LoadbalanceRSocketClient build( - Publisher> rSocketSuppliersPublisher) { + /** Build the {@link LoadbalanceRSocketClient} instance. */ + public LoadbalanceRSocketClient build() { return new LoadbalanceRSocketClient( - new RSocketPool(rSocketSuppliersPublisher, this.loadbalanceStrategy)); + new RSocketPool(initConnector(), this.targetPublisher, initLoadbalanceStrategy())); + } + + private RSocketConnector initConnector() { + return (this.connector != null ? this.connector : RSocketConnector.create()); + } + + private LoadbalanceStrategy initLoadbalanceStrategy() { + return (this.loadbalanceStrategy != null + ? this.loadbalanceStrategy + : new RoundRobinLoadbalanceStrategy()); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketSource.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketSource.java deleted file mode 100644 index a3b7243f4..000000000 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketSource.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.loadbalance; - -import io.rsocket.RSocket; -import reactor.core.publisher.Mono; - -public class LoadbalanceRSocketSource { - - final String serverKey; - final Mono source; - - private LoadbalanceRSocketSource(String serverKey, Mono source) { - this.serverKey = serverKey; - this.source = source; - } - - public Mono source() { - return source; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - LoadbalanceRSocketSource that = (LoadbalanceRSocketSource) o; - - return serverKey.equals(that.serverKey); - } - - @Override - public int hashCode() { - return serverKey.hashCode(); - } - - public static LoadbalanceRSocketSource from(String serverKey, Mono source) { - return new LoadbalanceRSocketSource(serverKey, source); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceTarget.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceTarget.java new file mode 100644 index 000000000..e99914caa --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceTarget.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.loadbalance; + +import io.rsocket.transport.ClientTransport; + +/** + * Simple container for a key and a {@link ClientTransport}, representing a specific target for + * loadbalancing purposes. The key is used to compare previous and new targets when refreshing the + * list of target to use. The transport is used to connect to the target. + * + * @since 1.1 + */ +public class LoadbalanceTarget { + + final String key; + final ClientTransport transport; + + private LoadbalanceTarget(String key, ClientTransport transport) { + this.key = key; + this.transport = transport; + } + + /** Return the key for this target. */ + public String getKey() { + return key; + } + + /** Return the transport to use to connect to the target. */ + public ClientTransport getTransport() { + return transport; + } + + /** + * Create a an instance of {@link LoadbalanceTarget} with the given key and {@link + * ClientTransport}. The key can be anything that can be used to identify identical targets, e.g. + * a SocketAddress, URL, etc. + * + * @param key the key to use to identify identical targets + * @param transport the transport to use for connecting to the target + * @return the created instance + */ + public static LoadbalanceTarget from(String key, ClientTransport transport) { + return new LoadbalanceTarget(key, transport); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + LoadbalanceTarget that = (LoadbalanceTarget) other; + return key.equals(that.key); + } + + @Override + public int hashCode() { + return key.hashCode(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledWeightedRSocket.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledWeightedRSocket.java index 0cd5952a2..ad681087e 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledWeightedRSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledWeightedRSocket.java @@ -33,7 +33,8 @@ final class PooledWeightedRSocket extends ResolvingOperator implements CoreSubscriber, WeightedRSocket { final RSocketPool parent; - final LoadbalanceRSocketSource loadbalanceRSocketSource; + final Mono rSocketSource; + final LoadbalanceTarget loadbalanceTarget; final Stats stats; volatile Subscription s; @@ -42,10 +43,14 @@ final class PooledWeightedRSocket extends ResolvingOperator AtomicReferenceFieldUpdater.newUpdater(PooledWeightedRSocket.class, Subscription.class, "s"); PooledWeightedRSocket( - RSocketPool parent, LoadbalanceRSocketSource loadbalanceRSocketSource, Stats stats) { + RSocketPool parent, + Mono rSocketSource, + LoadbalanceTarget loadbalanceTarget, + Stats stats) { this.parent = parent; + this.rSocketSource = rSocketSource; + this.loadbalanceTarget = loadbalanceTarget; this.stats = stats; - this.loadbalanceRSocketSource = loadbalanceRSocketSource; } @Override @@ -103,7 +108,7 @@ public void onNext(RSocket value) { @Override protected void doSubscribe() { - this.loadbalanceRSocketSource.source().subscribe(this); + this.rSocketSource.subscribe(this); } @Override @@ -196,8 +201,8 @@ public Stats stats() { return stats; } - LoadbalanceRSocketSource source() { - return loadbalanceRSocketSource; + LoadbalanceTarget target() { + return loadbalanceTarget; } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java index dc776852c..dbd05abcb 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java @@ -18,6 +18,7 @@ import io.netty.util.ReferenceCountUtil; import io.rsocket.Payload; import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; import io.rsocket.frame.FrameType; import java.util.Arrays; import java.util.Collection; @@ -37,9 +38,10 @@ import reactor.util.annotation.Nullable; class RSocketPool extends ResolvingOperator - implements CoreSubscriber>, List { + implements CoreSubscriber>, List { final DeferredResolutionRSocket deferredResolutionRSocket = new DeferredResolutionRSocket(this); + final RSocketConnector connector; final LoadbalanceStrategy loadbalanceStrategy; final Supplier statsSupplier; @@ -56,8 +58,11 @@ class RSocketPool extends ResolvingOperator static final AtomicReferenceFieldUpdater S = AtomicReferenceFieldUpdater.newUpdater(RSocketPool.class, Subscription.class, "s"); - RSocketPool( - Publisher> source, LoadbalanceStrategy loadbalanceStrategy) { + public RSocketPool( + RSocketConnector connector, + Publisher> targetPublisher, + LoadbalanceStrategy loadbalanceStrategy) { + this.connector = connector; this.loadbalanceStrategy = loadbalanceStrategy; if (loadbalanceStrategy instanceof WeightedLoadbalanceStrategy) { this.statsSupplier = Stats::create; @@ -67,7 +72,7 @@ class RSocketPool extends ResolvingOperator ACTIVE_SOCKETS.lazySet(this, EMPTY); - source.subscribe(this); + targetPublisher.subscribe(this); } @Override @@ -92,10 +97,10 @@ public void onSubscribe(Subscription s) { * method invocations, therefore it is acceptable to have it algorithmically inefficient. The * algorithmic complexity of this method is * - * @param loadbalanceRSocketSources set which represents RSocket source to balance on + * @param targets set which represents RSocket targets to balance on */ @Override - public void onNext(List loadbalanceRSocketSources) { + public void onNext(List targets) { if (isDisposed()) { return; } @@ -103,11 +108,11 @@ public void onNext(List loadbalanceRSocketSources) { PooledWeightedRSocket[] previouslyActiveSockets; PooledWeightedRSocket[] activeSockets; for (; ; ) { - HashMap rSocketSuppliersCopy = new HashMap<>(); + HashMap rSocketSuppliersCopy = new HashMap<>(); int j = 0; - for (LoadbalanceRSocketSource loadbalanceRSocketSource : loadbalanceRSocketSources) { - rSocketSuppliersCopy.put(loadbalanceRSocketSource, j++); + for (LoadbalanceTarget target : targets) { + rSocketSuppliersCopy.put(target, j++); } // checking intersection of active RSocket with the newly received set @@ -118,7 +123,7 @@ public void onNext(List loadbalanceRSocketSources) { for (int i = 0; i < previouslyActiveSockets.length; i++) { PooledWeightedRSocket rSocket = previouslyActiveSockets[i]; - Integer index = rSocketSuppliersCopy.remove(rSocket.source()); + Integer index = rSocketSuppliersCopy.remove(rSocket.target()); if (index == null) { // if one of the active rSockets is not included, we remove it and put in the // pending removal @@ -133,17 +138,25 @@ public void onNext(List loadbalanceRSocketSources) { nextActiveSockets[position++] = rSocket; } else { // put newly create RSocket instance + LoadbalanceTarget target = targets.get(index); nextActiveSockets[position++] = new PooledWeightedRSocket( - this, loadbalanceRSocketSources.get(index), this.statsSupplier.get()); + this, + this.connector.connect(target.getTransport()), + target, + this.statsSupplier.get()); } } } // going though brightly new rsocket - for (LoadbalanceRSocketSource newLoadbalanceRSocketSource : rSocketSuppliersCopy.keySet()) { + for (LoadbalanceTarget target : rSocketSuppliersCopy.keySet()) { nextActiveSockets[position++] = - new PooledWeightedRSocket(this, newLoadbalanceRSocketSource, this.statsSupplier.get()); + new PooledWeightedRSocket( + this, + this.connector.connect(target.getTransport()), + target, + this.statsSupplier.get()); } // shrank to actual length diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java index 2c3fd831d..2c1d8c37c 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java @@ -1,11 +1,25 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.examples.transport.tcp.loadbalancer; import io.rsocket.RSocketClient; import io.rsocket.SocketAcceptor; -import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.loadbalance.LoadbalanceRSocketClient; -import io.rsocket.loadbalance.LoadbalanceRSocketSource; +import io.rsocket.loadbalance.LoadbalanceTarget; import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; @@ -50,7 +64,11 @@ public static void main(String[] args) { })) .bindNow(TcpServerTransport.create(8082)); - Flux> producer = + LoadbalanceTarget target8080 = LoadbalanceTarget.from("8080", TcpClientTransport.create(8080)); + LoadbalanceTarget target8081 = LoadbalanceTarget.from("8081", TcpClientTransport.create(8081)); + LoadbalanceTarget target8082 = LoadbalanceTarget.from("8082", TcpClientTransport.create(8082)); + + Flux> producer = Flux.interval(Duration.ofSeconds(5)) .log() .map( @@ -60,73 +78,31 @@ public static void main(String[] args) { case 0: return Collections.emptyList(); case 1: - return Collections.singletonList( - LoadbalanceRSocketSource.from( - "8080", - RSocketConnector.connectWith(TcpClientTransport.create(8080)))); + return Collections.singletonList(target8080); case 2: - return Arrays.asList( - LoadbalanceRSocketSource.from( - "8080", - RSocketConnector.connectWith(TcpClientTransport.create(8080))), - LoadbalanceRSocketSource.from( - "8081", - RSocketConnector.connectWith(TcpClientTransport.create(8081)))); + return Arrays.asList(target8080, target8081); case 3: - return Arrays.asList( - LoadbalanceRSocketSource.from( - "8080", - RSocketConnector.connectWith(TcpClientTransport.create(8080))), - LoadbalanceRSocketSource.from( - "8082", - RSocketConnector.connectWith(TcpClientTransport.create(8082)))); + return Arrays.asList(target8080, target8082); case 4: - return Arrays.asList( - LoadbalanceRSocketSource.from( - "8081", - RSocketConnector.connectWith(TcpClientTransport.create(8081))), - LoadbalanceRSocketSource.from( - "8082", - RSocketConnector.connectWith(TcpClientTransport.create(8082)))); + return Arrays.asList(target8081, target8082); case 5: - return Arrays.asList( - LoadbalanceRSocketSource.from( - "8080", - RSocketConnector.connectWith(TcpClientTransport.create(8080))), - LoadbalanceRSocketSource.from( - "8081", - RSocketConnector.connectWith(TcpClientTransport.create(8081))), - LoadbalanceRSocketSource.from( - "8082", - RSocketConnector.connectWith(TcpClientTransport.create(8082)))); + return Arrays.asList(target8080, target8081, target8082); case 6: return Collections.emptyList(); case 7: return Collections.emptyList(); default: - case 8: - return Arrays.asList( - LoadbalanceRSocketSource.from( - "8080", - RSocketConnector.connectWith(TcpClientTransport.create(8080))), - LoadbalanceRSocketSource.from( - "8081", - RSocketConnector.connectWith(TcpClientTransport.create(8081))), - LoadbalanceRSocketSource.from( - "8082", - RSocketConnector.connectWith(TcpClientTransport.create(8082)))); + return Arrays.asList(target8080, target8081, target8082); } }); - RSocketClient loadBalancedRSocketClient = - LoadbalanceRSocketClient.builder().withRoundRobinLoadbalanceStrategy().build(producer); + RSocketClient rSocketClient = + LoadbalanceRSocketClient.builder(producer).roundRobinLoadbalanceStrategy().build(); for (int i = 0; i < 10000; i++) { try { - loadBalancedRSocketClient - .requestResponse(Mono.just(DefaultPayload.create("test" + i))) - .block(); + rSocketClient.requestResponse(Mono.just(DefaultPayload.create("test" + i))).block(); } catch (Throwable t) { // no ops } diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java index 55b40fe39..c7f64674c 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java @@ -16,19 +16,33 @@ package io.rsocket.client; -import io.rsocket.Availability; -import io.rsocket.Closeable; -import io.rsocket.RSocket; +import io.rsocket.*; import io.rsocket.client.filter.RSocketSupplier; -import io.rsocket.loadbalance.LoadbalanceRSocketClient; -import io.rsocket.loadbalance.LoadbalanceRSocketSource; +import io.rsocket.stat.Ewma; +import io.rsocket.stat.FrugalQuantile; +import io.rsocket.stat.Median; +import io.rsocket.stat.Quantile; +import io.rsocket.util.Clock; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +import java.util.ArrayList; import java.util.Collection; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; +import reactor.util.retry.Retry; /** * An implementation of {@link Mono} that load balances across a pool of RSockets and emits one when @@ -36,59 +50,922 @@ * *

    It estimates the load of each RSocket based on statistics collected. * - * @deprecated as of 1.1. in favor of {@link LoadbalanceRSocketClient}. + * @deprecated as of 1.1. in favor of {@link io.rsocket.loadbalance.LoadbalanceRSocketClient}. */ @Deprecated public abstract class LoadBalancedRSocketMono extends Mono implements Availability, Closeable { - private final MonoProcessor onClose = MonoProcessor.create(); - private final LoadbalanceRSocketClient loadBalancedRSocketClient; + public static final double DEFAULT_EXP_FACTOR = 4.0; + public static final double DEFAULT_LOWER_QUANTILE = 0.2; + public static final double DEFAULT_HIGHER_QUANTILE = 0.8; + public static final double DEFAULT_MIN_PENDING = 1.0; + public static final double DEFAULT_MAX_PENDING = 2.0; + public static final int DEFAULT_MIN_APERTURE = 3; + public static final int DEFAULT_MAX_APERTURE = 100; + public static final long DEFAULT_MAX_REFRESH_PERIOD_MS = + TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES); + private static final Logger logger = LoggerFactory.getLogger(LoadBalancedRSocketMono.class); + private static final long APERTURE_REFRESH_PERIOD = Clock.unit().convert(15, TimeUnit.SECONDS); + private static final int EFFORT = 5; + private static final long DEFAULT_INITIAL_INTER_ARRIVAL_TIME = + Clock.unit().convert(1L, TimeUnit.SECONDS); + private static final int DEFAULT_INTER_ARRIVAL_FACTOR = 500; + private static final FailingRSocket FAILING_REACTIVE_SOCKET = new FailingRSocket(); protected final Mono rSocketMono; + private final double minPendings; + private final double maxPendings; + private final int minAperture; + private final int maxAperture; + private final long maxRefreshPeriod; + private final double expFactor; + private final Quantile lowerQuantile; + private final Quantile higherQuantile; + private final ArrayList activeSockets; + private final Ewma pendings; + private final MonoProcessor onClose = MonoProcessor.create(); + private final RSocketSupplierPool pool; + private final long weightedSocketRetries; + private final Duration weightedSocketBackOff; + private final Duration weightedSocketMaxBackOff; + private volatile int targetAperture; + private long lastApertureRefresh; + private long refreshPeriod; + private int pendingSockets; + private volatile long lastRefresh; + + /** + * @param factories the source (factories) of RSocket + * @param expFactor how aggressive is the algorithm toward outliers. A higher number means we send + * aggressively less traffic to a server slightly slower. + * @param lowQuantile the lower bound of the latency band of acceptable values. Any server below + * that value will be aggressively favored. + * @param highQuantile the higher bound of the latency band of acceptable values. Any server above + * that value will be aggressively penalized. + * @param minPendings The lower band of the average outstanding messages per server. + * @param maxPendings The higher band of the average outstanding messages per server. + * @param minAperture the minimum number of connections we want to maintain, independently of the + * load. + * @param maxAperture the maximum number of connections we want to maintain, independently of the + * load. + * @param maxRefreshPeriodMs the maximum time between two "refreshes" of the list of active + * RSocket. This is at that time that the slowest RSocket is closed. (unit is millisecond) + * @param weightedSocketRetries the number of times a weighted socket will attempt to retry when + * it receives an error before reconnecting. The default is 5 times. + * @param weightedSocketBackOff the duration a a weighted socket will add to each retry attempt. + * @param weightedSocketMaxBackOff the max duration a weighted socket will delay before retrying + * to connect. The default is 5 seconds. + */ + private LoadBalancedRSocketMono( + Publisher> factories, + double expFactor, + double lowQuantile, + double highQuantile, + double minPendings, + double maxPendings, + int minAperture, + int maxAperture, + long maxRefreshPeriodMs, + long weightedSocketRetries, + Duration weightedSocketBackOff, + Duration weightedSocketMaxBackOff) { + this.weightedSocketRetries = weightedSocketRetries; + this.weightedSocketBackOff = weightedSocketBackOff; + this.weightedSocketMaxBackOff = weightedSocketMaxBackOff; + this.expFactor = expFactor; + this.lowerQuantile = new FrugalQuantile(lowQuantile); + this.higherQuantile = new FrugalQuantile(highQuantile); + + this.activeSockets = new ArrayList<>(); + this.pendingSockets = 0; + + this.minPendings = minPendings; + this.maxPendings = maxPendings; + this.pendings = new Ewma(15, TimeUnit.SECONDS, (minPendings + maxPendings) / 2.0); + + this.minAperture = minAperture; + this.maxAperture = maxAperture; + this.targetAperture = minAperture; + + this.maxRefreshPeriod = Clock.unit().convert(maxRefreshPeriodMs, TimeUnit.MILLISECONDS); + this.lastApertureRefresh = Clock.now(); + this.refreshPeriod = Clock.unit().convert(15L, TimeUnit.SECONDS); + this.lastRefresh = Clock.now(); + this.pool = new RSocketSupplierPool(factories); + refreshSockets(); + + rSocketMono = Mono.fromSupplier(this::select); + + onClose.doFinally(signalType -> pool.dispose()).subscribe(); + } + + public static LoadBalancedRSocketMono create( + Publisher> factories) { + return create( + factories, + DEFAULT_EXP_FACTOR, + DEFAULT_LOWER_QUANTILE, + DEFAULT_HIGHER_QUANTILE, + DEFAULT_MIN_PENDING, + DEFAULT_MAX_PENDING, + DEFAULT_MIN_APERTURE, + DEFAULT_MAX_APERTURE, + DEFAULT_MAX_REFRESH_PERIOD_MS); + } + + public static LoadBalancedRSocketMono create( + Publisher> factories, + double expFactor, + double lowQuantile, + double highQuantile, + double minPendings, + double maxPendings, + int minAperture, + int maxAperture, + long maxRefreshPeriodMs, + long weightedSocketRetries, + Duration weightedSocketBackOff, + Duration weightedSocketMaxBackOff) { + return new LoadBalancedRSocketMono( + factories, + expFactor, + lowQuantile, + highQuantile, + minPendings, + maxPendings, + minAperture, + maxAperture, + maxRefreshPeriodMs, + weightedSocketRetries, + weightedSocketBackOff, + weightedSocketMaxBackOff) { + @Override + public void subscribe(CoreSubscriber s) { + rSocketMono.subscribe(s); + } + }; + } + + public static LoadBalancedRSocketMono create( + Publisher> factories, + double expFactor, + double lowQuantile, + double highQuantile, + double minPendings, + double maxPendings, + int minAperture, + int maxAperture, + long maxRefreshPeriodMs) { + return new LoadBalancedRSocketMono( + factories, + expFactor, + lowQuantile, + highQuantile, + minPendings, + maxPendings, + minAperture, + maxAperture, + maxRefreshPeriodMs, + 5, + Duration.ofMillis(500), + Duration.ofSeconds(5)) { + @Override + public void subscribe(CoreSubscriber s) { + rSocketMono.subscribe(s); + } + }; + } + + /** + * Responsible for: - refreshing the aperture - asynchronously adding/removing reactive sockets to + * match targetAperture - periodically append a new connection + */ + private synchronized void refreshSockets() { + refreshAperture(); + int n = activeSockets.size(); + if (n < targetAperture && !pool.isPoolEmpty()) { + logger.debug( + "aperture {} is below target {}, adding {} sockets", + n, + targetAperture, + targetAperture - n); + addSockets(targetAperture - n); + } else if (targetAperture < activeSockets.size()) { + logger.debug("aperture {} is above target {}, quicking 1 socket", n, targetAperture); + quickSlowestRS(); + } + + long now = Clock.now(); + if (now - lastRefresh >= refreshPeriod) { + long prev = refreshPeriod; + refreshPeriod = (long) Math.min(refreshPeriod * 1.5, maxRefreshPeriod); + logger.debug("Bumping refresh period, {}->{}", prev / 1000, refreshPeriod / 1000); + lastRefresh = now; + addSockets(1); + } + } + + private synchronized void addSockets(int numberOfNewSocket) { + int n = numberOfNewSocket; + int poolSize = pool.poolSize(); + if (n > poolSize) { + n = poolSize; + logger.debug( + "addSockets({}) restricted by the number of factories, i.e. addSockets({})", + numberOfNewSocket, + n); + } + + for (int i = 0; i < n; i++) { + Optional optional = pool.get(); + + if (optional.isPresent()) { + RSocketSupplier supplier = optional.get(); + WeightedSocket socket = new WeightedSocket(supplier, lowerQuantile, higherQuantile); + } else { + break; + } + } + } + + private synchronized void refreshAperture() { + int n = activeSockets.size(); + if (n == 0) { + return; + } + + double p = 0.0; + for (WeightedSocket wrs : activeSockets) { + p += wrs.getPending(); + } + p /= n + pendingSockets; + pendings.insert(p); + double avgPending = pendings.value(); + + long now = Clock.now(); + boolean underRateLimit = now - lastApertureRefresh > APERTURE_REFRESH_PERIOD; + if (avgPending < 1.0 && underRateLimit) { + updateAperture(targetAperture - 1, now); + } else if (2.0 < avgPending && underRateLimit) { + updateAperture(targetAperture + 1, now); + } + } - private LoadBalancedRSocketMono(LoadbalanceRSocketClient loadBalancedRSocketClient) { - this.rSocketMono = loadBalancedRSocketClient.source(); - this.loadBalancedRSocketClient = loadBalancedRSocketClient; + /** + * Update the aperture value and ensure its value stays in the right range. + * + * @param newValue new aperture value + * @param now time of the change (for rate limiting purposes) + */ + private void updateAperture(int newValue, long now) { + int previous = targetAperture; + targetAperture = newValue; + targetAperture = Math.max(minAperture, targetAperture); + int maxAperture = Math.min(this.maxAperture, activeSockets.size() + pool.poolSize()); + targetAperture = Math.min(maxAperture, targetAperture); + lastApertureRefresh = now; + pendings.reset((minPendings + maxPendings) / 2); + + if (targetAperture != previous) { + logger.debug( + "Current pending={}, new target={}, previous target={}", + pendings.value(), + targetAperture, + previous); + } + } + + private synchronized void quickSlowestRS() { + if (activeSockets.size() <= 1) { + return; + } + + WeightedSocket slowest = null; + double lowestAvailability = Double.MAX_VALUE; + for (WeightedSocket socket : activeSockets) { + double load = socket.availability(); + if (load == 0.0) { + slowest = socket; + break; + } + if (socket.getPredictedLatency() != 0) { + load *= 1.0 / socket.getPredictedLatency(); + } + if (load < lowestAvailability) { + lowestAvailability = load; + slowest = socket; + } + } + + if (slowest != null) { + logger.debug("Disposing slowest WeightedSocket {}", slowest); + slowest.dispose(); + } + } + + @Override + public synchronized double availability() { + double currentAvailability = 0.0; + if (!activeSockets.isEmpty()) { + for (WeightedSocket rs : activeSockets) { + currentAvailability += rs.availability(); + } + currentAvailability /= activeSockets.size(); + } + + return currentAvailability; + } + + private synchronized RSocket select() { + refreshSockets(); + + if (activeSockets.isEmpty()) { + return FAILING_REACTIVE_SOCKET; + } + + int size = activeSockets.size(); + if (size == 1) { + return activeSockets.get(0); + } + + WeightedSocket rsc1 = null; + WeightedSocket rsc2 = null; + + Random rng = ThreadLocalRandom.current(); + for (int i = 0; i < EFFORT; i++) { + int i1 = rng.nextInt(size); + int i2 = rng.nextInt(size - 1); + if (i2 >= i1) { + i2++; + } + rsc1 = activeSockets.get(i1); + rsc2 = activeSockets.get(i2); + if (rsc1.availability() > 0.0 && rsc2.availability() > 0.0) { + break; + } + if (i + 1 == EFFORT && !pool.isPoolEmpty()) { + addSockets(1); + } + } + + double w1 = algorithmicWeight(rsc1); + double w2 = algorithmicWeight(rsc2); + if (w1 < w2) { + return rsc2; + } else { + return rsc1; + } + } + + private double algorithmicWeight(WeightedSocket socket) { + if (socket == null || socket.availability() == 0.0) { + return 0.0; + } + + int pendings = socket.getPending(); + double latency = socket.getPredictedLatency(); + + double low = lowerQuantile.estimation(); + double high = + Math.max( + higherQuantile.estimation(), + low * 1.001); // ensure higherQuantile > lowerQuantile + .1% + double bandWidth = Math.max(high - low, 1); + + if (latency < low) { + double alpha = (low - latency) / bandWidth; + double bonusFactor = Math.pow(1 + alpha, expFactor); + latency /= bonusFactor; + } else if (latency > high) { + double alpha = (latency - high) / bandWidth; + double penaltyFactor = Math.pow(1 + alpha, expFactor); + latency *= penaltyFactor; + } + + return socket.availability() * 1.0 / (1.0 + latency * (pendings + 1)); + } + + @Override + public synchronized String toString() { + return "LoadBalancer(a:" + + activeSockets.size() + + ", f: " + + pool.poolSize() + + ", avgPendings=" + + pendings.value() + + ", targetAperture=" + + targetAperture + + ", band=[" + + lowerQuantile.estimation() + + ", " + + higherQuantile.estimation() + + "])"; } @Override public void dispose() { - this.loadBalancedRSocketClient.dispose(); + synchronized (this) { + activeSockets.forEach(WeightedSocket::dispose); + activeSockets.clear(); + onClose.onComplete(); + } } @Override - public Mono onClose() { - return onClose; + public boolean isDisposed() { + return onClose.isDisposed(); } @Override - public double availability() { - return 1.0d; + public Mono onClose() { + return onClose; } - @Deprecated - public static LoadBalancedRSocketMono create( - Publisher> factories) { + /** + * (Null Object Pattern) This failing RSocket never succeed, it is useful for simplifying the code + * when dealing with edge cases. + */ + private static class FailingRSocket implements RSocket { + + private static final Mono errorVoid = Mono.error(NoAvailableRSocketException.INSTANCE); + private static final Mono errorPayload = + Mono.error(NoAvailableRSocketException.INSTANCE); + + @Override + public Mono fireAndForget(Payload payload) { + return errorVoid; + } - return fromClient( - Flux.from(factories) - .map( - rsl -> - rsl.stream() - .map(rs -> LoadbalanceRSocketSource.from(rs.toString(), rs.get())) - .collect(Collectors.toList())) - .as( - f -> - LoadbalanceRSocketClient.builder().withWeightedLoadbalanceStrategy().build(f))); + @Override + public Mono requestResponse(Payload payload) { + return errorPayload; + } + + @Override + public Flux requestStream(Payload payload) { + return errorPayload.flux(); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return errorPayload.flux(); + } + + @Override + public Mono metadataPush(Payload payload) { + return errorVoid; + } + + @Override + public double availability() { + return 0; + } + + @Override + public void dispose() {} + + @Override + public boolean isDisposed() { + return true; + } + + @Override + public Mono onClose() { + return Mono.empty(); + } } - public static LoadBalancedRSocketMono fromClient(LoadbalanceRSocketClient rSocketClient) { - return new LoadBalancedRSocketMono(rSocketClient) { + /** + * Wrapper of a RSocket, it computes statistics about the req/resp calls and update availability + * accordingly. + */ + private class WeightedSocket implements LoadBalancerSocketMetrics, RSocket { + + private static final double STARTUP_PENALTY = Long.MAX_VALUE >> 12; + private final Quantile lowerQuantile; + private final Quantile higherQuantile; + private final long inactivityFactor; + private final MonoProcessor rSocketMono; + private volatile int pending; // instantaneous rate + private long stamp; // last timestamp we sent a request + private long stamp0; // last timestamp we sent a request or receive a response + private long duration; // instantaneous cumulative duration + + private Median median; + private Ewma interArrivalTime; + + private AtomicLong pendingStreams; // number of active streams + + private volatile double availability = 0.0; + private final MonoProcessor onClose = MonoProcessor.create(); + + WeightedSocket( + RSocketSupplier factory, + Quantile lowerQuantile, + Quantile higherQuantile, + int inactivityFactor) { + this.rSocketMono = MonoProcessor.create(); + this.lowerQuantile = lowerQuantile; + this.higherQuantile = higherQuantile; + this.inactivityFactor = inactivityFactor; + long now = Clock.now(); + this.stamp = now; + this.stamp0 = now; + this.duration = 0L; + this.pending = 0; + this.median = new Median(); + this.interArrivalTime = new Ewma(1, TimeUnit.MINUTES, DEFAULT_INITIAL_INTER_ARRIVAL_TIME); + this.pendingStreams = new AtomicLong(); + + logger.debug("Creating WeightedSocket {} from factory {}", WeightedSocket.this, factory); + + WeightedSocket.this + .onClose() + .doFinally( + s -> { + pool.accept(factory); + activeSockets.remove(WeightedSocket.this); + logger.debug( + "Removed {} from factory {} from activeSockets", WeightedSocket.this, factory); + }) + .subscribe(); + + factory + .get() + .retryWhen( + Retry.backoff(weightedSocketRetries, weightedSocketBackOff) + .maxBackoff(weightedSocketMaxBackOff)) + .doOnError( + throwable -> { + logger.error( + "error while connecting {} from factory {}", + WeightedSocket.this, + factory, + throwable); + WeightedSocket.this.dispose(); + }) + .subscribe( + rSocket -> { + // When RSocket is closed, close the WeightedSocket + rSocket + .onClose() + .doFinally( + signalType -> { + logger.info( + "RSocket {} from factory {} closed", WeightedSocket.this, factory); + WeightedSocket.this.dispose(); + }) + .subscribe(); + + // When the factory is closed, close the RSocket + factory + .onClose() + .doFinally( + signalType -> { + logger.info("Factory {} closed", factory); + rSocket.dispose(); + }) + .subscribe(); + + // When the WeightedSocket is closed, close the RSocket + WeightedSocket.this + .onClose() + .doFinally( + signalType -> { + logger.info( + "WeightedSocket {} from factory {} closed", + WeightedSocket.this, + factory); + rSocket.dispose(); + }) + .subscribe(); + + /*synchronized (LoadBalancedRSocketMono.this) { + if (activeSockets.size() >= targetAperture) { + quickSlowestRS(); + pendingSockets -= 1; + } + }*/ + rSocketMono.onNext(rSocket); + availability = 1.0; + if (!WeightedSocket.this + .isDisposed()) { // May be already disposed because of retryBackoff delay + activeSockets.add(WeightedSocket.this); + logger.debug( + "Added WeightedSocket {} from factory {} to activeSockets", + WeightedSocket.this, + factory); + } + }); + } + + WeightedSocket(RSocketSupplier factory, Quantile lowerQuantile, Quantile higherQuantile) { + this(factory, lowerQuantile, higherQuantile, DEFAULT_INTER_ARRIVAL_FACTOR); + } + + @Override + public Mono requestResponse(Payload payload) { + return rSocketMono.flatMap( + source -> { + return Mono.from( + subscriber -> + source + .requestResponse(payload) + .subscribe(new LatencySubscriber<>(subscriber, this))); + }); + } + + @Override + public Flux requestStream(Payload payload) { + + return rSocketMono.flatMapMany( + source -> { + return Flux.from( + subscriber -> + source + .requestStream(payload) + .subscribe(new CountingSubscriber<>(subscriber, this))); + }); + } + + @Override + public Mono fireAndForget(Payload payload) { + + return rSocketMono.flatMap( + source -> { + return Mono.from( + subscriber -> + source + .fireAndForget(payload) + .subscribe(new CountingSubscriber<>(subscriber, this))); + }); + } + + @Override + public Mono metadataPush(Payload payload) { + return rSocketMono.flatMap( + source -> { + return Mono.from( + subscriber -> + source + .metadataPush(payload) + .subscribe(new CountingSubscriber<>(subscriber, this))); + }); + } + + @Override + public Flux requestChannel(Publisher payloads) { + + return rSocketMono.flatMapMany( + source -> { + return Flux.from( + subscriber -> + source + .requestChannel(payloads) + .subscribe(new CountingSubscriber<>(subscriber, this))); + }); + } + + synchronized double getPredictedLatency() { + long now = Clock.now(); + long elapsed = Math.max(now - stamp, 1L); + + double weight; + double prediction = median.estimation(); + + if (prediction == 0.0) { + if (pending == 0) { + weight = 0.0; // first request + } else { + // subsequent requests while we don't have any history + weight = STARTUP_PENALTY + pending; + } + } else if (pending == 0 && elapsed > inactivityFactor * interArrivalTime.value()) { + // if we did't see any data for a while, we decay the prediction by inserting + // artificial 0.0 into the median + median.insert(0.0); + weight = median.estimation(); + } else { + double predicted = prediction * pending; + double instant = instantaneous(now); + + if (predicted < instant) { // NB: (0.0 < 0.0) == false + weight = instant / pending; // NB: pending never equal 0 here + } else { + // we are under the predictions + weight = prediction; + } + } + + return weight; + } + + int getPending() { + return pending; + } + + private synchronized long instantaneous(long now) { + return duration + (now - stamp0) * pending; + } + + private synchronized long incr() { + long now = Clock.now(); + interArrivalTime.insert(now - stamp); + duration += Math.max(0, now - stamp0) * pending; + pending += 1; + stamp = now; + stamp0 = now; + return now; + } + + private synchronized long decr(long timestamp) { + long now = Clock.now(); + duration += Math.max(0, now - stamp0) * pending - (now - timestamp); + pending -= 1; + stamp0 = now; + return now; + } + + private synchronized void observe(double rtt) { + median.insert(rtt); + lowerQuantile.insert(rtt); + higherQuantile.insert(rtt); + } + + @Override + public double availability() { + return availability; + } + + @Override + public void dispose() { + onClose.onComplete(); + } + + @Override + public boolean isDisposed() { + return onClose.isDisposed(); + } + + @Override + public Mono onClose() { + return onClose; + } + + @Override + public String toString() { + return "WeightedSocket(" + + "median=" + + median.estimation() + + " quantile-low=" + + lowerQuantile.estimation() + + " quantile-high=" + + higherQuantile.estimation() + + " inter-arrival=" + + interArrivalTime.value() + + " duration/pending=" + + (pending == 0 ? 0 : (double) duration / pending) + + " pending=" + + pending + + " availability= " + + availability() + + ")->"; + } + + @Override + public double medianLatency() { + return median.estimation(); + } + + @Override + public double lowerQuantileLatency() { + return lowerQuantile.estimation(); + } + + @Override + public double higherQuantileLatency() { + return higherQuantile.estimation(); + } + + @Override + public double interArrivalTime() { + return interArrivalTime.value(); + } + + @Override + public int pending() { + return pending; + } + + @Override + public long lastTimeUsedMillis() { + return stamp0; + } + + /** + * Subscriber wrapper used for request/response interaction model, measure and collect latency + * information. + */ + private class LatencySubscriber implements Subscriber { + private final Subscriber child; + private final WeightedSocket socket; + private final AtomicBoolean done; + private long start; + + LatencySubscriber(Subscriber child, WeightedSocket socket) { + this.child = child; + this.socket = socket; + this.done = new AtomicBoolean(false); + } + @Override - public void subscribe(CoreSubscriber s) { - rSocketClient.source().subscribe(s); + public void onSubscribe(Subscription s) { + start = incr(); + child.onSubscribe( + new Subscription() { + @Override + public void request(long n) { + s.request(n); + } + + @Override + public void cancel() { + if (done.compareAndSet(false, true)) { + s.cancel(); + decr(start); + } + } + }); } - }; + + @Override + public void onNext(U u) { + child.onNext(u); + } + + @Override + public void onError(Throwable t) { + if (done.compareAndSet(false, true)) { + child.onError(t); + long now = decr(start); + if (t instanceof TransportException || t instanceof ClosedChannelException) { + socket.dispose(); + } else if (t instanceof TimeoutException) { + observe(now - start); + } + } + } + + @Override + public void onComplete() { + if (done.compareAndSet(false, true)) { + long now = decr(start); + observe(now - start); + child.onComplete(); + } + } + } + + /** + * Subscriber wrapper used for stream like interaction model, it only counts the number of + * active streams + */ + private class CountingSubscriber implements Subscriber { + private final Subscriber child; + private final WeightedSocket socket; + + CountingSubscriber(Subscriber child, WeightedSocket socket) { + this.child = child; + this.socket = socket; + } + + @Override + public void onSubscribe(Subscription s) { + socket.pendingStreams.incrementAndGet(); + child.onSubscribe(s); + } + + @Override + public void onNext(U u) { + child.onNext(u); + } + + @Override + public void onError(Throwable t) { + socket.pendingStreams.decrementAndGet(); + child.onError(t); + if (t instanceof TransportException || t instanceof ClosedChannelException) { + logger.debug("Disposing {} from activeSockets because of error {}", socket, t); + socket.dispose(); + } + } + + @Override + public void onComplete() { + socket.pendingStreams.decrementAndGet(); + child.onComplete(); + } + } } } From 13bb8185abd30c76d8adb28431cb59bcdbb04993 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 7 Sep 2020 09:58:12 +0100 Subject: [PATCH 013/183] RSocketClient related refactoring Signed-off-by: Rossen Stoyanchev --- .../main/java/io/rsocket/RSocketClient.java | 97 ------------- .../io/rsocket/core/DefaultRSocketClient.java | 22 ++- .../java/io/rsocket/core/RSocketClient.java | 137 ++++++++++++++++++ .../io/rsocket/core/RSocketClientAdapter.java | 3 +- .../io/rsocket/core/RSocketConnector.java | 63 ++------ .../java/io/rsocket/core/ReconnectMono.java | 4 + .../loadbalance/LoadbalanceRSocketClient.java | 2 +- .../core/DefaultRSocketClientTests.java | 1 - .../tcp/client/RSocketClientExample.java | 9 +- .../RoundRobinRSocketLoadbalancerExample.java | 2 +- 10 files changed, 178 insertions(+), 162 deletions(-) delete mode 100644 rsocket-core/src/main/java/io/rsocket/RSocketClient.java create mode 100644 rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java deleted file mode 100644 index db2304e12..000000000 --- a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket; - -import org.reactivestreams.Publisher; -import reactor.core.Disposable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.retry.Retry; - -/** - * Contract to perform RSocket requests from client to server, transparently connecting and ensuring - * a single, shared connection to make requests with. - * - *

    {@code RSocketClient} contains a {@code Mono} {@link #source() source}. It uses it to - * obtain a live, shared {@link RSocket} connection on the first request and on subsequent requests - * if the connection is lost. This eliminates the need to obtain a connection first, and makes it - * easy to pass a single {@code RSocketClient} to use from multiple places. - * - *

    Request methods of {@code RSocketClient} allow multiple subscriptions with each subscription - * performing a new request. Therefore request methods accept {@code Mono} rather than - * {@code Payload} as on {@link RSocket}. By contrast, {@link RSocket} request methods cannot be - * subscribed to more than once. - * - *

    Use {@link io.rsocket.core.RSocketConnector RSocketConnector} to create a client: - * - *

    {@code
    - * RSocketClient client =
    - *         RSocketConnector.create()
    - *                 .metadataMimeType("message/x.rsocket.composite-metadata.v0")
    - *                 .dataMimeType("application/cbor")
    - *                 .toRSocketClient(TcpClientTransport.create("localhost", 7000));
    - * }
    - * - *

    Use the {@link io.rsocket.core.RSocketConnector#reconnect(Retry) RSocketConnector#reconnect} - * method to configure the retry logic to use whenever a shared {@code RSocket} connection needs to - * be obtained: - * - *

    {@code
    - * RSocketClient client =
    - *         RSocketConnector.create()
    - *                 .metadataMimeType("message/x.rsocket.composite-metadata.v0")
    - *                 .dataMimeType("application/cbor")
    - *                 .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
    - *                 .toRSocketClient(TcpClientTransport.create("localhost", 7000));
    - * }
    - * - * @since 1.0.1 - */ -public interface RSocketClient extends Disposable { - - /** Return the underlying source used to obtain a shared {@link RSocket} connection. */ - Mono source(); - - /** - * Perform a Fire-and-Forget interaction via {@link RSocket#fireAndForget(Payload)}. Allows - * multiple subscriptions and performs a request per subscriber. - */ - Mono fireAndForget(Mono payloadMono); - - /** - * Perform a Request-Response interaction via {@link RSocket#requestResponse(Payload)}. Allows - * multiple subscriptions and performs a request per subscriber. - */ - Mono requestResponse(Mono payloadMono); - - /** - * Perform a Request-Stream interaction via {@link RSocket#requestStream(Payload)}. Allows - * multiple subscriptions and performs a request per subscriber. - */ - Flux requestStream(Mono payloadMono); - - /** - * Perform a Request-Channel interaction via {@link RSocket#requestChannel(Publisher)}. Allows - * multiple subscriptions and performs a request per subscriber. - */ - Flux requestChannel(Publisher payloads); - - /** - * Perform a Metadata Push via {@link RSocket#metadataPush(Payload)}. Allows multiple - * subscriptions and performs a request per subscriber. - */ - Mono metadataPush(Mono payloadMono); -} 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 24fa8f84c..4dc250158 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java @@ -1,10 +1,24 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.core; import io.netty.util.IllegalReferenceCountException; import io.netty.util.ReferenceCounted; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketClient; import io.rsocket.frame.FrameType; import java.util.AbstractMap; import java.util.Map; @@ -57,7 +71,11 @@ class DefaultRSocketClient extends ResolvingOperator AtomicReferenceFieldUpdater.newUpdater(DefaultRSocketClient.class, Subscription.class, "s"); DefaultRSocketClient(Mono source) { - this.source = source; + this.source = unwrapReconnectMono(source); + } + + private Mono unwrapReconnectMono(Mono source) { + return source instanceof ReconnectMono ? ((ReconnectMono) source).getSource() : source; } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java new file mode 100644 index 000000000..81392e661 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java @@ -0,0 +1,137 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.core; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import org.reactivestreams.Publisher; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Contract for performing RSocket requests. + * + *

    {@link RSocketClient} differs from {@link RSocket} in a number of ways: + * + *

      + *
    • {@code RSocket} represents a "live" connection that is transient and needs to be obtained + * typically from a {@code Mono} source via {@code flatMap} or block. By contrast, + * {@code RSocketClient} is a higher level layer that contains such a {@link #source() source} + * of connections and transparently obtains and re-obtains a shared connection as needed when + * requests are made concurrently. That means an {@code RSocketClient} can simply be created + * once, even before a connection is established, and shared as a singleton across multiple + * places as you would with any other client. + *
    • For request input {@code RSocket} accepts an instance of {@code Payload} and does not allow + * more than one subscription per request because there is no way to safely re-use that input. + * By contrast {@code RSocketClient} accepts {@code Publisher} and allow + * re-subscribing which repeats the request. + *
    • {@code RSocket} can be used for sending and it can also be implemented for receiving. By + * contrast {@code RSocketClient} is used only for sending, typically from the client side + * which allows obtaining and re-obtaining connections from a source as needed. However it can + * also be used from the server side by {@link #from(RSocket) wrapping} the "live" {@code + * RSocket} for a given connection. + *
    + * + *

    The example below shows how to create an {@code RSocketClient}: + * + *

    {@code
    + * Mono source =
    + *         RSocketConnector.create()
    + *                 .metadataMimeType("message/x.rsocket.composite-metadata.v0")
    + *                 .dataMimeType("application/cbor")
    + *                 .connect(TcpClientTransport.create("localhost", 7000));
    + *
    + * RSocketClient client = RSocketClient.from(source);
    + * }
    + * + *

    The below configures retry logic to use when a shared {@code RSocket} connection is obtained: + * + *

    {@code
    + * Mono source =
    + *         RSocketConnector.create()
    + *                 .metadataMimeType("message/x.rsocket.composite-metadata.v0")
    + *                 .dataMimeType("application/cbor")
    + *                 .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
    + *                 .connect(TcpClientTransport.create("localhost", 7000));
    + *
    + * RSocketClient client = RSocketClient.from(source);
    + * }
    + * + * @since 1.1 + * @see io.rsocket.loadbalance.LoadbalanceRSocketClient + */ +public interface RSocketClient extends Disposable { + + /** Return the underlying source used to obtain a shared {@link RSocket} connection. */ + Mono source(); + + /** + * Perform a Fire-and-Forget interaction via {@link RSocket#fireAndForget(Payload)}. Allows + * multiple subscriptions and performs a request per subscriber. + */ + Mono fireAndForget(Mono payloadMono); + + /** + * Perform a Request-Response interaction via {@link RSocket#requestResponse(Payload)}. Allows + * multiple subscriptions and performs a request per subscriber. + */ + Mono requestResponse(Mono payloadMono); + + /** + * Perform a Request-Stream interaction via {@link RSocket#requestStream(Payload)}. Allows + * multiple subscriptions and performs a request per subscriber. + */ + Flux requestStream(Mono payloadMono); + + /** + * Perform a Request-Channel interaction via {@link RSocket#requestChannel(Publisher)}. Allows + * multiple subscriptions and performs a request per subscriber. + */ + Flux requestChannel(Publisher payloads); + + /** + * Perform a Metadata Push via {@link RSocket#metadataPush(Payload)}. Allows multiple + * subscriptions and performs a request per subscriber. + */ + Mono metadataPush(Mono payloadMono); + + /** + * Create an {@link RSocketClient} that obtains shared connections as needed, when requests are + * made, from the given {@code Mono} source. + * + * @param source the source for connections, typically prepared via {@link RSocketConnector}. + * @return the created client instance + */ + static RSocketClient from(Mono source) { + return new DefaultRSocketClient(source); + } + + /** + * Adapt the given {@link RSocket} to use as {@link RSocketClient}. This is useful to wrap the + * sending {@code RSocket} in a server. + * + *

    Note: unlike an {@code RSocketClient} created via {@link + * RSocketClient#from(Mono)}, the instance returned from this factory method can only perform + * requests for as long as the given {@code RSocket} remains "live". + * + * @param rsocket the {@code RSocket} to perform requests with + * @return the created client instance + */ + static RSocketClient from(RSocket rsocket) { + return new RSocketClientAdapter(rsocket); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java index e54bf157d..cc94f4102 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java @@ -17,7 +17,6 @@ import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketClient; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -30,7 +29,7 @@ * * @since 1.1 */ -public class RSocketClientAdapter implements RSocketClient { +class RSocketClientAdapter implements RSocketClient { private final RSocket rsocket; 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 692aaca7d..0058106bc 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -25,7 +25,6 @@ import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketClient; import io.rsocket.SocketAcceptor; import io.rsocket.frame.SetupFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; @@ -120,15 +119,6 @@ public static Mono connectWith(ClientTransport transport) { return RSocketConnector.create().connect(() -> transport); } - /** - * @param transport - * @return - * @since 1.0.1 - */ - public static RSocketClient createRSocketClient(ClientTransport transport) { - return RSocketConnector.create().toRSocketClient(transport); - } - /** * Provide a {@code Mono} from which to obtain the {@code Payload} for the initial SETUP frame. * Data and metadata should be formatted according to the MIME types specified via {@link @@ -485,37 +475,6 @@ public RSocketConnector payloadDecoder(PayloadDecoder decoder) { return this; } - /** - * Create {@link RSocketClient} that will use {@link #connect(ClientTransport)} as its source to - * obtain a live, shared {@code RSocket} when the first request is made, and also on subsequent - * requests after the connection is lost. - * - *

    The following transports are available through additional RSocket Java modules: - * - *

      - *
    • {@link io.rsocket.transport.netty.client.TcpClientTransport TcpClientTransport} via - * {@code rsocket-transport-netty}. - *
    • {@link io.rsocket.transport.netty.client.WebsocketClientTransport - * WebsocketClientTransport} via {@code rsocket-transport-netty}. - *
    • {@link io.rsocket.transport.local.LocalClientTransport LocalClientTransport} via {@code - * rsocket-transport-local} - *
    - * - * @param transport the transport of choice to connect with - * @return a {@code RSocketClient} with not established connection. Note, connection will be - * established on the first request - * @since 1.0.1 - */ - public RSocketClient toRSocketClient(ClientTransport transport) { - Mono source = connect0(() -> transport); - - if (retrySpec != null) { - source = source.retryWhen(retrySpec); - } - - return new DefaultRSocketClient(source); - } - /** * Connect with the given transport and obtain a live {@link RSocket} to use for making requests. * Each subscriber to the returned {@code Mono} receives a new connection, if neither {@link @@ -549,19 +508,6 @@ public Mono connect(ClientTransport transport) { * @return a {@code Mono} with the connected RSocket */ public Mono connect(Supplier transportSupplier) { - return this.connect0(transportSupplier) - .as( - source -> { - if (retrySpec != null) { - return new ReconnectMono<>( - source.retryWhen(retrySpec), Disposable::dispose, INVALIDATE_FUNCTION); - } else { - return source; - } - }); - } - - private Mono connect0(Supplier transportSupplier) { return Mono.fromSupplier(transportSupplier) .flatMap( ct -> { @@ -692,6 +638,15 @@ private Mono connect0(Supplier transportSupplier) { }) .doFinally(signalType -> setup.release()); }); + }) + .as( + source -> { + if (retrySpec != null) { + return new ReconnectMono<>( + source.retryWhen(retrySpec), Disposable::dispose, INVALIDATE_FUNCTION); + } else { + return source; + } }); } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/ReconnectMono.java b/rsocket-core/src/main/java/io/rsocket/core/ReconnectMono.java index 44e4ffa81..afad6e0df 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ReconnectMono.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ReconnectMono.java @@ -48,6 +48,10 @@ final class ReconnectMono extends Mono implements Invalidatable, Disposabl this.resolvingInner = new ResolvingInner<>(this); } + public Mono getSource() { + return source; + } + @Override public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; 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 18600e633..2dd8ecc72 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java @@ -17,7 +17,7 @@ import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketClient; +import io.rsocket.core.RSocketClient; import io.rsocket.core.RSocketConnector; import java.util.List; import org.reactivestreams.Publisher; 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 d41ed16c1..d080b166d 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -26,7 +26,6 @@ import io.netty.util.ReferenceCounted; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketClient; import io.rsocket.frame.ErrorFrameCodec; import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/client/RSocketClientExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/client/RSocketClientExample.java index e1bf459b9..2d19b9ce4 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/client/RSocketClientExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/client/RSocketClientExample.java @@ -1,8 +1,9 @@ package io.rsocket.examples.transport.tcp.client; import io.rsocket.Payload; -import io.rsocket.RSocketClient; +import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketClient; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.transport.netty.client.TcpClientTransport; @@ -35,12 +36,12 @@ public static void main(String[] args) { .doOnNext(cc -> logger.info("Server started on the address : {}", cc.address())) .subscribe(); - RSocketClient rSocketClient = + Mono source = RSocketConnector.create() .reconnect(Retry.backoff(50, Duration.ofMillis(500))) - .toRSocketClient(TcpClientTransport.create("localhost", 7000)); + .connect(TcpClientTransport.create("localhost", 7000)); - rSocketClient + RSocketClient.from(source) .requestResponse(Mono.just(DefaultPayload.create("Test Request"))) .doOnSubscribe(s -> logger.info("Executing Request")) .doOnNext( diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java index 2c1d8c37c..27d10b472 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java @@ -15,8 +15,8 @@ */ package io.rsocket.examples.transport.tcp.loadbalancer; -import io.rsocket.RSocketClient; import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketClient; import io.rsocket.core.RSocketServer; import io.rsocket.loadbalance.LoadbalanceRSocketClient; import io.rsocket.loadbalance.LoadbalanceTarget; From 6959390a02e598d52135c10f6dfb84c6e3323d6f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 7 Sep 2020 14:11:47 +0100 Subject: [PATCH 014/183] Make LoadbalanceStrategy implementations public Signed-off-by: Rossen Stoyanchev --- .../loadbalance/LoadbalanceRSocketClient.java | 8 ++++---- .../RoundRobinLoadbalanceStrategy.java | 7 ++++++- .../WeightedLoadbalanceStrategy.java | 20 +++++++++---------- 3 files changed, 19 insertions(+), 16 deletions(-) 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 2dd8ecc72..89ae01f18 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java @@ -146,7 +146,7 @@ public Builder roundRobinLoadbalanceStrategy() { * Switch to using a strategy that assigns a weight to each pooled {@code RSocket} based on * actual usage stats, and uses that to make a choice. * - *

    By default this strategy is not used. + *

    By default, {@link RoundRobinLoadbalanceStrategy} is used. */ public Builder weightedLoadbalanceStrategy() { this.loadbalanceStrategy = new WeightedLoadbalanceStrategy(); @@ -154,11 +154,11 @@ public Builder weightedLoadbalanceStrategy() { } /** - * Switch to using a custom strategy for loadbalancing. + * Provide the {@link LoadbalanceStrategy} to use. * - * @see #roundRobinLoadbalanceStrategy() + *

    By default, {@link RoundRobinLoadbalanceStrategy} is used. */ - public Builder customLoadbalanceStrategy(LoadbalanceStrategy strategy) { + public Builder loadbalanceStrategy(LoadbalanceStrategy strategy) { this.loadbalanceStrategy = strategy; return this; } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java index 0e1c541f2..98c86d565 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategy.java @@ -19,7 +19,12 @@ import java.util.List; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -class RoundRobinLoadbalanceStrategy implements LoadbalanceStrategy { +/** + * Simple {@link LoadbalanceStrategy} that selects the {@code RSocket} to use in round-robin order. + * + * @since 1.1 + */ +public class RoundRobinLoadbalanceStrategy implements LoadbalanceStrategy { volatile int nextIndex; diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java index 44ffbd6f5..03bc0530d 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java @@ -20,18 +20,22 @@ import java.util.List; import java.util.SplittableRandom; import java.util.concurrent.ThreadLocalRandom; -import java.util.function.Supplier; import reactor.util.annotation.Nullable; -class WeightedLoadbalanceStrategy implements LoadbalanceStrategy { +/** + * {@link LoadbalanceStrategy} that assigns a weight to each {@code RSocket} based on usage + * statistics, and uses this weight to select the {@code RSocket} to use. + * + * @since 1.1 + */ +public class WeightedLoadbalanceStrategy implements LoadbalanceStrategy { private static final double EXP_FACTOR = 4.0; private static final int EFFORT = 5; - final SplittableRandom splittableRandom; final int effort; - final Supplier statsSupplier; + final SplittableRandom splittableRandom; public WeightedLoadbalanceStrategy() { this(EFFORT); @@ -42,14 +46,8 @@ public WeightedLoadbalanceStrategy(int effort) { } public WeightedLoadbalanceStrategy(int effort, SplittableRandom splittableRandom) { - this(effort, splittableRandom, Stats::create); - } - - public WeightedLoadbalanceStrategy( - int effort, SplittableRandom splittableRandom, Supplier statsSupplier) { - this.splittableRandom = splittableRandom; this.effort = effort; - this.statsSupplier = statsSupplier; + this.splittableRandom = splittableRandom; } @Override From 4606f25a09216cdce08582129c6e2305721b486f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 10 Sep 2020 18:01:30 +0100 Subject: [PATCH 015/183] DuplexConnection exposes remoteAddress() Closes gh-735 Signed-off-by: Rossen Stoyanchev --- .../java/io/rsocket/DuplexConnection.java | 14 +++++- .../core/ClientServerInputMultiplexer.java | 6 +++ .../ClientServerInputMultiplexer.java | 6 +++ .../resume/ResumableDuplexConnection.java | 8 +++- .../test/util/LocalDuplexConnection.java | 8 +++- .../test/util/TestDuplexConnection.java | 8 +++- .../test/util/TestLocalSocketAddress.java | 46 +++++++++++++++++++ .../MicrometerDuplexConnection.java | 8 +++- .../java/io/rsocket/test/TransportTest.java | 6 +++ .../transport/local/LocalClientTransport.java | 7 ++- .../local/LocalDuplexConnection.java | 12 ++++- .../transport/local/LocalSocketAddress.java | 17 +++---- .../transport/netty/TcpDuplexConnection.java | 6 +++ .../netty/WebsocketDuplexConnection.java | 8 +++- 14 files changed, 143 insertions(+), 17 deletions(-) create mode 100644 rsocket-core/src/test/java/io/rsocket/test/util/TestLocalSocketAddress.java diff --git a/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java index 6190d24e3..4cb35f022 100644 --- a/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import java.net.SocketAddress; import java.nio.channels.ClosedChannelException; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; @@ -86,6 +87,17 @@ default Mono sendOne(ByteBuf frame) { */ ByteBufAllocator alloc(); + /** + * Return the remote address that this connection is connected to. The returned {@link + * SocketAddress} varies by transport type and should be downcast to obtain more detailed + * information. For TCP and WebSocket, the address type is {@link java.net.InetSocketAddress}. For + * local transport, it is {@link io.rsocket.transport.local.LocalSocketAddress}. + * + * @return the address + * @since 1.1 + */ + SocketAddress remoteAddress(); + @Override default double availability() { return isDisposed() ? 0.0 : 1.0; 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 4b07f04c7..cf1cdac03 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java @@ -24,6 +24,7 @@ import io.rsocket.frame.FrameUtil; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; import io.rsocket.plugins.InitializingInterceptorRegistry; +import java.net.SocketAddress; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; @@ -372,6 +373,11 @@ public ByteBufAllocator alloc() { return source.alloc(); } + @Override + public SocketAddress remoteAddress() { + return source.remoteAddress(); + } + @Override public void dispose() { source.dispose(); diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java index 179a7a757..dd7b485f9 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java @@ -24,6 +24,7 @@ import io.rsocket.frame.FrameUtil; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; import io.rsocket.plugins.InitializingInterceptorRegistry; +import java.net.SocketAddress; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -221,6 +222,11 @@ public ByteBufAllocator alloc() { return source.alloc(); } + @Override + public SocketAddress remoteAddress() { + return source.remoteAddress(); + } + @Override public void dispose() { source.dispose(); diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java index 461d71228..7bd471583 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import io.rsocket.Closeable; import io.rsocket.DuplexConnection; import io.rsocket.frame.FrameHeaderCodec; +import java.net.SocketAddress; import java.nio.channels.ClosedChannelException; import java.time.Duration; import java.util.Queue; @@ -111,6 +112,11 @@ public ByteBufAllocator alloc() { return curConnection.alloc(); } + @Override + public SocketAddress remoteAddress() { + return curConnection.remoteAddress(); + } + public void disconnect() { DuplexConnection c = this.curConnection; if (c != null) { diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/LocalDuplexConnection.java b/rsocket-core/src/test/java/io/rsocket/test/util/LocalDuplexConnection.java index a2957c5a1..f455c8385 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/LocalDuplexConnection.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/LocalDuplexConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; +import java.net.SocketAddress; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; @@ -93,6 +94,11 @@ public ByteBufAllocator alloc() { return allocator; } + @Override + public SocketAddress remoteAddress() { + return new TestLocalSocketAddress(name); + } + @Override public void dispose() { onClose.onComplete(); diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java index abc15509b..91de8f0de 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; +import java.net.SocketAddress; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import org.reactivestreams.Publisher; @@ -111,6 +112,11 @@ public ByteBufAllocator alloc() { return allocator; } + @Override + public SocketAddress remoteAddress() { + return new TestLocalSocketAddress("TestDuplexConnection"); + } + @Override public double availability() { return availability; diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestLocalSocketAddress.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestLocalSocketAddress.java new file mode 100644 index 000000000..2dad2cc1f --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestLocalSocketAddress.java @@ -0,0 +1,46 @@ +/* + * 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.test.util; + +import java.net.SocketAddress; +import java.util.Objects; + +public final class TestLocalSocketAddress extends SocketAddress { + + private static final long serialVersionUID = 2608695156052100164L; + + private final String name; + + /** + * Creates a new instance. + * + * @param name the name representing the address + * @throws NullPointerException if {@code name} is {@code null} + */ + public TestLocalSocketAddress(String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); + } + + /** Return the name for this connection. */ + public String getName() { + return name; + } + + @Override + public String toString() { + return "[local address] " + name; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java index c8b22382a..4f00073eb 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; +import java.net.SocketAddress; import java.util.Objects; import java.util.function.Consumer; import org.reactivestreams.Publisher; @@ -88,6 +89,11 @@ public ByteBufAllocator alloc() { return delegate.alloc(); } + @Override + public SocketAddress remoteAddress() { + return delegate.remoteAddress(); + } + @Override public void dispose() { delegate.dispose(); 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 436550130..f55e08bd7 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -33,6 +33,7 @@ import io.rsocket.util.ByteBufPayload; import java.io.BufferedReader; import java.io.InputStreamReader; +import java.net.SocketAddress; import java.time.Duration; import java.util.concurrent.CancellationException; import java.util.concurrent.Executors; @@ -568,6 +569,11 @@ public ByteBufAllocator alloc() { return duplexConnection.alloc(); } + @Override + public SocketAddress remoteAddress() { + return duplexConnection.remoteAddress(); + } + @Override public Mono onClose() { return duplexConnection.onClose(); 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 b80fc2337..a87636365 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 @@ -82,10 +82,13 @@ public Mono connect() { UnboundedProcessor out = new UnboundedProcessor<>(); MonoProcessor closeNotifier = MonoProcessor.create(); - server.apply(new LocalDuplexConnection(allocator, out, in, closeNotifier)).subscribe(); + server + .apply(new LocalDuplexConnection(name, allocator, out, in, closeNotifier)) + .subscribe(); return Mono.just( - (DuplexConnection) new LocalDuplexConnection(allocator, in, out, closeNotifier)); + (DuplexConnection) + new LocalDuplexConnection(name, allocator, in, out, closeNotifier)); }); } } diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java index 026f30ced..40d09f4aa 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; +import java.net.SocketAddress; import java.util.Objects; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; @@ -33,6 +34,7 @@ /** An implementation of {@link DuplexConnection} that connects inside the same JVM. */ final class LocalDuplexConnection implements DuplexConnection { + private final LocalSocketAddress address; private final ByteBufAllocator allocator; private final Flux in; @@ -43,16 +45,19 @@ final class LocalDuplexConnection implements DuplexConnection { /** * Creates a new instance. * + * @param name the name assigned to this local connection * @param in the inbound {@link ByteBuf}s * @param out the outbound {@link ByteBuf}s * @param onClose the closing notifier * @throws NullPointerException if {@code in}, {@code out}, or {@code onClose} are {@code null} */ LocalDuplexConnection( + String name, ByteBufAllocator allocator, Flux in, Subscriber out, MonoProcessor onClose) { + this.address = new LocalSocketAddress(name); this.allocator = Objects.requireNonNull(allocator, "allocator must not be null"); this.in = Objects.requireNonNull(in, "in must not be null"); this.out = Objects.requireNonNull(out, "out must not be null"); @@ -100,6 +105,11 @@ public ByteBufAllocator alloc() { return allocator; } + @Override + public SocketAddress remoteAddress() { + return address; + } + static class ByteBufReleaserOperator implements CoreSubscriber, Subscription, Fuseable.QueueSubscription { diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalSocketAddress.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalSocketAddress.java index d04fd482e..4d0da126a 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalSocketAddress.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalSocketAddress.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import java.util.Objects; /** An implementation of {@link SocketAddress} representing a local connection. */ -final class LocalSocketAddress extends SocketAddress { +public final class LocalSocketAddress extends SocketAddress { private static final long serialVersionUID = -7513338854585475473L; @@ -32,16 +32,17 @@ final class LocalSocketAddress extends SocketAddress { * @param name the name representing the address * @throws NullPointerException if {@code name} is {@code null} */ - LocalSocketAddress(String name) { + public LocalSocketAddress(String name) { this.name = Objects.requireNonNull(name, "name must not be null"); } - @Override - public String toString() { - return "[local server] " + name; + /** Return the name for this connection. */ + public String getName() { + return name; } - String getName() { - return name; + @Override + public String toString() { + return "[local address] " + name; } } 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 80c8b8256..0dc766e6f 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 @@ -21,6 +21,7 @@ import io.rsocket.DuplexConnection; import io.rsocket.frame.FrameLengthCodec; import io.rsocket.internal.BaseDuplexConnection; +import java.net.SocketAddress; import java.util.Objects; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -54,6 +55,11 @@ public ByteBufAllocator alloc() { return connection.channel().alloc(); } + @Override + public SocketAddress remoteAddress() { + return connection.channel().remoteAddress(); + } + @Override protected void doOnClose() { if (!connection.isDisposed()) { diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java index a3745bd1f..208e78905 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.rsocket.DuplexConnection; import io.rsocket.internal.BaseDuplexConnection; +import java.net.SocketAddress; import java.util.Objects; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -59,6 +60,11 @@ public ByteBufAllocator alloc() { return connection.channel().alloc(); } + @Override + public SocketAddress remoteAddress() { + return connection.channel().remoteAddress(); + } + @Override protected void doOnClose() { if (!connection.isDisposed()) { From 78a747a1f7596825244e2ce2b0e0607d62fcd386 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 14 Sep 2020 17:05:36 +0300 Subject: [PATCH 016/183] improves DuplexConnection api and reworks Resumability (#923) Co-authored-by: Rossen Stoyanchev --- .../java/io/rsocket/DuplexConnection.java | 30 +- .../core/ClientServerInputMultiplexer.java | 127 +-- .../java/io/rsocket/core/ClientSetup.java | 32 + .../core/FireAndForgetRequesterMono.java | 11 +- .../rsocket/core/LoggingDuplexConnection.java | 72 ++ .../core/MetadataPushRequesterMono.java | 10 +- .../io/rsocket/core/RSocketConnector.java | 207 +++-- .../io/rsocket/core/RSocketRequester.java | 39 +- .../io/rsocket/core/RSocketResponder.java | 45 +- .../java/io/rsocket/core/RSocketServer.java | 90 +-- .../core/RequestChannelRequesterFlux.java | 29 +- .../RequestChannelResponderSubscriber.java | 53 +- .../core/RequestResponseRequesterMono.java | 14 +- .../RequestResponseResponderSubscriber.java | 32 +- .../core/RequestStreamRequesterFlux.java | 23 +- .../RequestStreamResponderSubscriber.java | 24 +- .../core/RequesterResponderSupport.java | 16 +- .../main/java/io/rsocket/core/SendUtils.java | 52 +- .../java/io/rsocket/core/ServerSetup.java | 100 ++- .../core/SetupHandlingDuplexConnection.java | 170 ++++ .../internal/BaseDuplexConnection.java | 14 +- .../ClientServerInputMultiplexer.java | 249 ------ .../rsocket/keepalive/KeepAliveHandler.java | 19 +- .../plugins/DuplexConnectionInterceptor.java | 2 + .../rsocket/resume/ClientRSocketSession.java | 311 +++++--- .../resume/InMemoryResumableFramesStore.java | 319 ++++---- .../io/rsocket/resume/RSocketSession.java | 33 +- .../io/rsocket/resume/RequestListener.java | 32 - .../resume/ResumableDuplexConnection.java | 549 +++++-------- .../rsocket/resume/ResumableFramesStore.java | 4 +- .../resume/ResumeFramesSubscriber.java | 88 --- .../rsocket/resume/ServerRSocketSession.java | 247 +++--- .../io/rsocket/resume/SessionManager.java | 23 +- .../resume/UpstreamFramesSubscriber.java | 159 ---- .../io/rsocket/core/AbstractSocketRule.java | 2 +- .../ClientServerInputMultiplexerTest.java | 126 +-- .../core/FireAndForgetRequesterMonoTest.java | 27 +- .../java/io/rsocket/core/KeepAliveTest.java | 747 +++++++++--------- .../io/rsocket/core/RSocketConnectorTest.java | 99 ++- .../io/rsocket/core/RSocketLeaseTest.java | 1 - .../io/rsocket/core/RSocketResponderTest.java | 6 +- .../io/rsocket/core/RSocketServerTest.java | 26 + .../core/RequestChannelRequesterFluxTest.java | 63 +- ...RequestChannelResponderSubscriberTest.java | 41 +- .../RequestResponseRequesterMonoTest.java | 41 +- .../core/RequestStreamRequesterFluxTest.java | 56 +- .../core/RequesterOperatorsRacingTest.java | 49 +- .../core/ResponderOperatorsCommonTest.java | 25 +- .../io/rsocket/core/SetupRejectionTest.java | 6 +- .../core/TestRequesterResponderSupport.java | 32 +- .../resume/InMemoryResumeStoreTest.java | 186 ++--- .../rsocket/resume/ResumeCalculatorTest.java | 114 +-- .../test/util/LocalDuplexConnection.java | 20 +- .../test/util/TestClientTransport.java | 5 +- .../test/util/TestDuplexConnection.java | 59 +- .../tcp/resume/ResumeFileTransfer.java | 6 +- .../MicrometerDuplexConnection.java | 12 +- .../MicrometerDuplexConnectionTest.java | 56 +- .../test/LeaksTrackingByteBufAllocator.java | 50 +- .../java/io/rsocket/test/TestRSocket.java | 2 +- .../java/io/rsocket/test/TransportTest.java | 206 ++++- .../local/LocalDuplexConnection.java | 27 +- .../local/LocalResumableTransportTest.java | 44 ++ .../transport/netty/TcpDuplexConnection.java | 42 +- .../netty/WebsocketDuplexConnection.java | 37 +- .../netty/TcpResumableTransportTest.java | 57 ++ 66 files changed, 2765 insertions(+), 2730 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/core/ClientSetup.java create mode 100644 rsocket-core/src/main/java/io/rsocket/core/LoggingDuplexConnection.java create mode 100644 rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/resume/RequestListener.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumeFramesSubscriber.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java rename rsocket-core/src/test/java/io/rsocket/{internal => core}/ClientServerInputMultiplexerTest.java (54%) create mode 100644 rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableTransportTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java index 4cb35f022..497edf123 100644 --- a/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java @@ -20,40 +20,28 @@ import io.netty.buffer.ByteBufAllocator; import java.net.SocketAddress; import java.nio.channels.ClosedChannelException; -import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; /** Represents a connection with input/output that the protocol uses. */ public interface DuplexConnection extends Availability, Closeable { /** - * Sends the source of Frames on this connection and returns the {@code Publisher} representing - * the result of this send. + * Delivers the given frame to the underlying transport connection. This method is non-blocking + * and can be safely executed from multiple threads. This method does not provide any flow-control + * mechanism. * - *

    Flow control - * - *

    The passed {@code Publisher} must - * - * @param frames Stream of {@code Frame}s to send on the connection. - * @return {@code Publisher} that completes when all the frames are written on the connection - * successfully and errors when it fails. - * @throws NullPointerException if {@code frames} is {@code null} + * @param streamId to which the given frame relates + * @param frame with the encoded content */ - Mono send(Publisher frames); + void sendFrame(int streamId, ByteBuf frame); /** - * Sends a single {@code Frame} on this connection and returns the {@code Publisher} representing - * the result of this send. + * Send an error frame and after it is successfully sent, close the connection. * - * @param frame {@code Frame} to send. - * @return {@code Publisher} that completes when the frame is written on the connection - * successfully and errors when it fails. + * @param errorException to encode in the error frame */ - default Mono sendOne(ByteBuf frame) { - return send(Mono.just(frame)); - } + void sendErrorAndClose(RSocketErrorException errorException); /** * Returns a stream of all {@code Frame}s received on this connection. 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 cf1cdac03..d6cb46d98 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java @@ -20,16 +20,13 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.Closeable; import io.rsocket.DuplexConnection; +import io.rsocket.RSocketErrorException; import io.rsocket.frame.FrameHeaderCodec; -import io.rsocket.frame.FrameUtil; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; import io.rsocket.plugins.InitializingInterceptorRegistry; import java.net.SocketAddress; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -50,21 +47,14 @@ */ class ClientServerInputMultiplexer implements CoreSubscriber, Closeable { - private static final Logger LOGGER = LoggerFactory.getLogger("io.rsocket.FrameLogger"); - private static final InitializingInterceptorRegistry emptyInterceptorRegistry = - new InitializingInterceptorRegistry(); - - private final InternalDuplexConnection setupReceiver; private final InternalDuplexConnection serverReceiver; private final InternalDuplexConnection clientReceiver; - private final DuplexConnection setupConnection; private final DuplexConnection serverConnection; private final DuplexConnection clientConnection; private final DuplexConnection source; private final boolean isClient; private Subscription s; - private boolean setupReceived; private Throwable t; @@ -72,45 +62,25 @@ class ClientServerInputMultiplexer implements CoreSubscriber, Closeable private static final AtomicIntegerFieldUpdater STATE = AtomicIntegerFieldUpdater.newUpdater(ClientServerInputMultiplexer.class, "state"); - public ClientServerInputMultiplexer(DuplexConnection source) { - this(source, emptyInterceptorRegistry, false); - } - public ClientServerInputMultiplexer( DuplexConnection source, InitializingInterceptorRegistry registry, boolean isClient) { this.source = source; this.isClient = isClient; - source = registry.initConnection(Type.SOURCE, source); - if (!isClient) { - setupReceiver = new InternalDuplexConnection(this, source); - setupConnection = registry.initConnection(Type.SETUP, setupReceiver); - } else { - setupReceiver = null; - setupConnection = null; - } - serverReceiver = new InternalDuplexConnection(this, source); - clientReceiver = new InternalDuplexConnection(this, source); - serverConnection = registry.initConnection(Type.SERVER, serverReceiver); - clientConnection = registry.initConnection(Type.CLIENT, clientReceiver); + this.serverReceiver = new InternalDuplexConnection(this, source); + this.clientReceiver = new InternalDuplexConnection(this, source); + this.serverConnection = registry.initConnection(Type.SERVER, serverReceiver); + this.clientConnection = registry.initConnection(Type.CLIENT, clientReceiver); } - public DuplexConnection asClientServerConnection() { - return source; - } - - public DuplexConnection asServerConnection() { + DuplexConnection asServerConnection() { return serverConnection; } - public DuplexConnection asClientConnection() { + DuplexConnection asClientConnection() { return clientConnection; } - public DuplexConnection asSetupConnection() { - return setupConnection; - } - @Override public void dispose() { source.dispose(); @@ -130,12 +100,7 @@ public Mono onClose() { public void onSubscribe(Subscription s) { if (Operators.validate(this.s, s)) { this.s = s; - if (isClient) { - s.request(Long.MAX_VALUE); - } else { - // request first SetupFrame - s.request(1); - } + s.request(Long.MAX_VALUE); } } @@ -145,12 +110,6 @@ public void onNext(ByteBuf frame) { final Type type; if (streamId == 0) { switch (FrameHeaderCodec.frameType(frame)) { - case SETUP: - case RESUME: - case RESUME_OK: - type = Type.SETUP; - setupReceived = true; - break; case LEASE: case KEEPALIVE: case ERROR: @@ -164,19 +123,8 @@ public void onNext(ByteBuf frame) { } else { type = Type.CLIENT; } - if (!isClient && type != Type.SETUP && !setupReceived) { - final IllegalStateException error = - new IllegalStateException("SETUP or LEASE frame must be received before any others."); - this.s.cancel(); - onError(error); - } switch (type) { - case SETUP: - final InternalDuplexConnection setupReceiver = this.setupReceiver; - setupReceiver.onNext(frame); - setupReceiver.onComplete(); - break; case CLIENT: clientReceiver.onNext(frame); break; @@ -193,16 +141,6 @@ public void onComplete() { return; } - if (!isClient) { - if (!setupReceived) { - setupReceiver.onComplete(); - } - - if (previousState == 1) { - return; - } - } - if (clientReceiver.isSubscribed()) { clientReceiver.onComplete(); } @@ -220,16 +158,6 @@ public void onError(Throwable t) { return; } - if (!isClient) { - if (!setupReceived) { - setupReceiver.onError(t); - } - - if (previousState == 1) { - return; - } - } - if (clientReceiver.isSubscribed()) { clientReceiver.onError(t); } @@ -244,17 +172,8 @@ boolean notifyRequested() { return false; } - if (isClient) { - if (currentState == 2) { - source.receive().subscribe(this); - } - } else { - if (currentState == 1) { - source.receive().subscribe(this); - } else if (currentState == 3) { - // means setup was consumed and we got request from client and server multiplexers - s.request(Long.MAX_VALUE); - } + if (currentState == 2) { + source.receive().subscribe(this); } return true; @@ -280,7 +199,6 @@ private static class InternalDuplexConnection extends Flux implements Subscription, DuplexConnection { private final ClientServerInputMultiplexer clientServerInputMultiplexer; private final DuplexConnection source; - private final boolean debugEnabled; private volatile int state; static final AtomicIntegerFieldUpdater STATE = @@ -292,7 +210,6 @@ public InternalDuplexConnection( ClientServerInputMultiplexer clientServerInputMultiplexer, DuplexConnection source) { this.clientServerInputMultiplexer = clientServerInputMultiplexer; this.source = source; - this.debugEnabled = LOGGER.isDebugEnabled(); } @Override @@ -340,32 +257,18 @@ void onError(Throwable t) { } @Override - public Mono send(Publisher frame) { - if (debugEnabled) { - return Flux.from(frame) - .doOnNext(f -> LOGGER.debug("sending -> " + FrameUtil.toString(f))) - .as(source::send); - } - - return source.send(frame); + public void sendFrame(int streamId, ByteBuf frame) { + source.sendFrame(streamId, frame); } @Override - public Mono sendOne(ByteBuf frame) { - if (debugEnabled) { - LOGGER.debug("sending -> " + FrameUtil.toString(frame)); - } - - return source.sendOne(frame); + public void sendErrorAndClose(RSocketErrorException e) { + source.sendErrorAndClose(e); } @Override public Flux receive() { - if (debugEnabled) { - return this.doOnNext(frame -> LOGGER.debug("receiving -> " + FrameUtil.toString(frame))); - } else { - return this; - } + return this; } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/core/ClientSetup.java b/rsocket-core/src/main/java/io/rsocket/core/ClientSetup.java new file mode 100644 index 000000000..725201fe7 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/ClientSetup.java @@ -0,0 +1,32 @@ +package io.rsocket.core; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.rsocket.DuplexConnection; +import java.nio.channels.ClosedChannelException; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +abstract class ClientSetup { + abstract Mono> init(DuplexConnection connection); +} + +class DefaultClientSetup extends ClientSetup { + + @Override + Mono> init(DuplexConnection connection) { + return Mono.create( + sink -> sink.onRequest(__ -> sink.success(Tuples.of(Unpooled.EMPTY_BUFFER, connection)))); + } +} + +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))); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java b/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java index 3d7a3dfa7..e51c3e75f 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java +++ b/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java @@ -20,12 +20,11 @@ import static io.rsocket.core.SendUtils.sendReleasingPayload; import static io.rsocket.core.StateUtils.*; -import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.IllegalReferenceCountException; +import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.frame.FrameType; -import io.rsocket.internal.UnboundedProcessor; import java.time.Duration; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Subscription; @@ -50,7 +49,7 @@ final class FireAndForgetRequesterMono extends Mono implements Subscriptio final int mtu; final int maxFrameLength; final RequesterResponderSupport requesterResponderSupport; - final UnboundedProcessor sendProcessor; + final DuplexConnection connection; FireAndForgetRequesterMono(Payload payload, RequesterResponderSupport requesterResponderSupport) { this.allocator = requesterResponderSupport.getAllocator(); @@ -58,7 +57,7 @@ final class FireAndForgetRequesterMono extends Mono implements Subscriptio this.mtu = requesterResponderSupport.getMtu(); this.maxFrameLength = requesterResponderSupport.getMaxFrameLength(); this.requesterResponderSupport = requesterResponderSupport; - this.sendProcessor = requesterResponderSupport.getSendProcessor(); + this.connection = requesterResponderSupport.getDuplexConnection(); } @Override @@ -106,7 +105,7 @@ public void subscribe(CoreSubscriber actual) { } sendReleasingPayload( - streamId, FrameType.REQUEST_FNF, mtu, p, this.sendProcessor, this.allocator, true); + streamId, FrameType.REQUEST_FNF, mtu, p, this.connection, this.allocator, true); } catch (Throwable e) { lazyTerminate(STATE, this); actual.onError(e); @@ -169,7 +168,7 @@ public Void block() { FrameType.REQUEST_FNF, this.mtu, this.payload, - this.sendProcessor, + this.connection, this.allocator, true); } catch (Throwable e) { diff --git a/rsocket-core/src/main/java/io/rsocket/core/LoggingDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/core/LoggingDuplexConnection.java new file mode 100644 index 000000000..7b5d8f6c2 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/LoggingDuplexConnection.java @@ -0,0 +1,72 @@ +package io.rsocket.core; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.RSocketErrorException; +import io.rsocket.frame.FrameUtil; +import java.net.SocketAddress; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +class LoggingDuplexConnection implements DuplexConnection { + + private static final Logger LOGGER = LoggerFactory.getLogger("io.rsocket.FrameLogger"); + + final DuplexConnection source; + + LoggingDuplexConnection(DuplexConnection source) { + this.source = source; + } + + @Override + public void dispose() { + source.dispose(); + } + + @Override + public Mono onClose() { + return source.onClose(); + } + + @Override + public void sendFrame(int streamId, ByteBuf frame) { + LOGGER.debug("sending -> " + FrameUtil.toString(frame)); + + source.sendFrame(streamId, frame); + } + + @Override + public void sendErrorAndClose(RSocketErrorException e) { + LOGGER.debug("sending -> " + e.getClass().getSimpleName() + ": " + e.getMessage()); + + source.sendErrorAndClose(e); + } + + @Override + public Flux receive() { + return source + .receive() + .doOnNext(frame -> LOGGER.debug("receiving -> " + FrameUtil.toString(frame))); + } + + @Override + public ByteBufAllocator alloc() { + return source.alloc(); + } + + @Override + public SocketAddress remoteAddress() { + return source.remoteAddress(); + } + + static DuplexConnection wrapIfEnabled(DuplexConnection source) { + if (LOGGER.isDebugEnabled()) { + return new LoggingDuplexConnection(source); + } + + return source; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/MetadataPushRequesterMono.java b/rsocket-core/src/main/java/io/rsocket/core/MetadataPushRequesterMono.java index 3a53b0ad8..226e9a0af 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/MetadataPushRequesterMono.java +++ b/rsocket-core/src/main/java/io/rsocket/core/MetadataPushRequesterMono.java @@ -22,9 +22,9 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.IllegalReferenceCountException; +import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.frame.MetadataPushFrameCodec; -import io.rsocket.internal.UnboundedProcessor; import java.time.Duration; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import reactor.core.CoreSubscriber; @@ -43,13 +43,13 @@ final class MetadataPushRequesterMono extends Mono implements Scannable { final ByteBufAllocator allocator; final Payload payload; final int maxFrameLength; - final UnboundedProcessor sendProcessor; + final DuplexConnection connection; MetadataPushRequesterMono(Payload payload, RequesterResponderSupport requesterResponderSupport) { this.allocator = requesterResponderSupport.getAllocator(); this.payload = payload; this.maxFrameLength = requesterResponderSupport.getMaxFrameLength(); - this.sendProcessor = requesterResponderSupport.getSendProcessor(); + this.connection = requesterResponderSupport.getDuplexConnection(); } @Override @@ -109,7 +109,7 @@ public void subscribe(CoreSubscriber actual) { final ByteBuf requestFrame = MetadataPushFrameCodec.encode(this.allocator, metadataRetainedSlice); - this.sendProcessor.onNext(requestFrame); + this.connection.sendFrame(0, requestFrame); Operators.complete(actual); } @@ -166,7 +166,7 @@ public Void block() { final ByteBuf requestFrame = MetadataPushFrameCodec.encode(this.allocator, metadataRetainedSlice); - this.sendProcessor.onNext(requestFrame); + this.connection.sendFrame(0, requestFrame); return null; } 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 0058106bc..4de9df1d1 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -33,9 +33,12 @@ import io.rsocket.lease.Leases; import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.plugins.DuplexConnectionInterceptor; import io.rsocket.plugins.InitializingInterceptorRegistry; import io.rsocket.plugins.InterceptorRegistry; import io.rsocket.resume.ClientRSocketSession; +import io.rsocket.resume.ResumableDuplexConnection; +import io.rsocket.resume.ResumableFramesStore; import io.rsocket.transport.ClientTransport; import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; @@ -519,7 +522,12 @@ public Mono connect(Supplier transportSupplier) { assertValidateSetup(maxFrameLength, maxInboundPayloadSize, mtu); return ct; }) - .flatMap(transport -> transport.connect()); + .flatMap(transport -> transport.connect()) + .map( + sourceConnection -> + interceptors.initConnection( + DuplexConnectionInterceptor.Type.SOURCE, sourceConnection)) + .map(source -> LoggingDuplexConnection.wrapIfEnabled(source)); return connectionMono .flatMap( @@ -530,65 +538,24 @@ public Mono connect(Supplier transportSupplier) { .doOnError(ex -> connection.dispose()) .doOnCancel(connection::dispose)) .flatMap( - tuple -> { - DuplexConnection connection = tuple.getT1(); - Payload setupPayload = tuple.getT2(); + tuple2 -> { + DuplexConnection sourceConnection = tuple2.getT1(); + Payload setupPayload = tuple2.getT2(); + boolean leaseEnabled = leasesSupplier != null; + boolean resumeEnabled = resume != null; + // TODO: add LeaseClientSetup + ClientSetup clientSetup = new DefaultClientSetup(); ByteBuf resumeToken; - KeepAliveHandler keepAliveHandler; - DuplexConnection wrappedConnection; - if (resume != null) { + if (resumeEnabled) { resumeToken = resume.getTokenSupplier().get(); - ClientRSocketSession session = - new ClientRSocketSession( - connection, - resume.getSessionDuration(), - resume.getRetry(), - resume.getStoreFactory(CLIENT_TAG).apply(resumeToken), - resume.getStreamTimeout(), - resume.isCleanupStoreOnKeepAlive()) - .continueWith(connectionMono) - .resumeToken(resumeToken); - keepAliveHandler = - new KeepAliveHandler.ResumableKeepAliveHandler( - session.resumableConnection()); - wrappedConnection = session.resumableConnection(); } else { resumeToken = Unpooled.EMPTY_BUFFER; - keepAliveHandler = - new KeepAliveHandler.DefaultKeepAliveHandler(connection); - wrappedConnection = connection; } - ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(wrappedConnection, interceptors, true); - - boolean leaseEnabled = leasesSupplier != null; - Leases leases = leaseEnabled ? leasesSupplier.get() : null; - RequesterLeaseHandler requesterLeaseHandler = - leaseEnabled - ? new RequesterLeaseHandler.Impl(CLIENT_TAG, leases.receiver()) - : RequesterLeaseHandler.None; - - RSocket rSocketRequester = - new RSocketRequester( - multiplexer.asClientConnection(), - payloadDecoder, - StreamIdSupplier.clientSupplier(), - mtu, - maxFrameLength, - maxInboundPayloadSize, - (int) keepAliveInterval.toMillis(), - (int) keepAliveMaxLifeTime.toMillis(), - keepAliveHandler, - requesterLeaseHandler); - - RSocket wrappedRSocketRequester = - interceptors.initRequester(rSocketRequester); - ByteBuf setupFrame = SetupFrameCodec.encode( - wrappedConnection.alloc(), + sourceConnection.alloc(), leaseEnabled, (int) keepAliveInterval.toMillis(), (int) keepAliveMaxLifeTime.toMillis(), @@ -597,46 +564,116 @@ public Mono connect(Supplier transportSupplier) { dataMimeType, setupPayload); - SocketAcceptor acceptor = - this.acceptor != null - ? this.acceptor - : SocketAcceptor.with(new RSocket() {}); - - ConnectionSetupPayload setup = - new DefaultConnectionSetupPayload(setupFrame); + sourceConnection.sendFrame(0, setupFrame.retain()); - return interceptors - .initSocketAcceptor(acceptor) - .accept(setup, wrappedRSocketRequester) + return clientSetup + .init(sourceConnection) .flatMap( - rSocketHandler -> { - RSocket wrappedRSocketHandler = - interceptors.initResponder(rSocketHandler); - - ResponderLeaseHandler responderLeaseHandler = + tuple -> { + // should be used if lease setup sequence; + // See: + // https://github.com/rsocket/rsocket/blob/master/Protocol.md#sequences-with-lease + ByteBuf serverResponse = tuple.getT1(); + DuplexConnection clientServerConnection = tuple.getT2(); + KeepAliveHandler keepAliveHandler; + DuplexConnection wrappedConnection; + + if (resumeEnabled) { + final ResumableFramesStore resumableFramesStore = + resume.getStoreFactory(CLIENT_TAG).apply(resumeToken); + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + clientServerConnection, resumableFramesStore); + final ResumableClientSetup resumableClientSetup = + new ResumableClientSetup(); + final ClientRSocketSession session = + new ClientRSocketSession( + resumeToken, + clientServerConnection, + resumableDuplexConnection, + connectionMono, + resumableClientSetup::init, + resumableFramesStore, + resume.getSessionDuration(), + resume.getRetry(), + resume.isCleanupStoreOnKeepAlive()); + keepAliveHandler = + new KeepAliveHandler.ResumableKeepAliveHandler( + resumableDuplexConnection, session, session); + wrappedConnection = resumableDuplexConnection; + } else { + keepAliveHandler = + new KeepAliveHandler.DefaultKeepAliveHandler( + clientServerConnection); + wrappedConnection = clientServerConnection; + } + + ClientServerInputMultiplexer multiplexer = + new ClientServerInputMultiplexer( + wrappedConnection, interceptors, true); + + Leases leases = leaseEnabled ? leasesSupplier.get() : null; + RequesterLeaseHandler requesterLeaseHandler = leaseEnabled - ? new ResponderLeaseHandler.Impl<>( - CLIENT_TAG, - wrappedConnection.alloc(), - leases.sender(), - leases.stats()) - : ResponderLeaseHandler.None; - - RSocket rSocketResponder = - new RSocketResponder( - multiplexer.asServerConnection(), - wrappedRSocketHandler, + ? new RequesterLeaseHandler.Impl( + CLIENT_TAG, leases.receiver()) + : RequesterLeaseHandler.None; + + RSocket rSocketRequester = + new RSocketRequester( + multiplexer.asClientConnection(), payloadDecoder, - responderLeaseHandler, + StreamIdSupplier.clientSupplier(), mtu, maxFrameLength, - maxInboundPayloadSize); - - return wrappedConnection - .sendOne(setupFrame.retain()) - .thenReturn(wrappedRSocketRequester); - }) - .doFinally(signalType -> setup.release()); + maxInboundPayloadSize, + (int) keepAliveInterval.toMillis(), + (int) keepAliveMaxLifeTime.toMillis(), + keepAliveHandler, + requesterLeaseHandler); + + RSocket wrappedRSocketRequester = + interceptors.initRequester(rSocketRequester); + + SocketAcceptor acceptor = + this.acceptor != null + ? this.acceptor + : SocketAcceptor.with(new RSocket() {}); + + ConnectionSetupPayload setup = + new DefaultConnectionSetupPayload(setupFrame); + + return interceptors + .initSocketAcceptor(acceptor) + .accept(setup, wrappedRSocketRequester) + .map( + rSocketHandler -> { + RSocket wrappedRSocketHandler = + interceptors.initResponder(rSocketHandler); + + ResponderLeaseHandler responderLeaseHandler = + leaseEnabled + ? new ResponderLeaseHandler.Impl<>( + CLIENT_TAG, + wrappedConnection.alloc(), + leases.sender(), + leases.stats()) + : ResponderLeaseHandler.None; + + RSocket rSocketResponder = + new RSocketResponder( + multiplexer.asServerConnection(), + wrappedRSocketHandler, + payloadDecoder, + responderLeaseHandler, + mtu, + maxFrameLength, + maxInboundPayloadSize); + + return wrappedRSocketRequester; + }) + .doFinally(signalType -> setup.release()); + }); }); }) .as( 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 66e2c60ec..044204225 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -29,7 +29,6 @@ import io.rsocket.frame.FrameType; import io.rsocket.frame.RequestNFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.UnboundedProcessor; import io.rsocket.keepalive.KeepAliveFramesAcceptor; import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.keepalive.KeepAliveSupport; @@ -62,7 +61,6 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { AtomicReferenceFieldUpdater.newUpdater( RSocketRequester.class, Throwable.class, "terminationError"); - private final DuplexConnection connection; private final RequesterLeaseHandler leaseHandler; private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; private final MonoProcessor onClose; @@ -78,23 +76,13 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { int keepAliveAckTimeout, @Nullable KeepAliveHandler keepAliveHandler, RequesterLeaseHandler leaseHandler) { - super( - mtu, - maxFrameLength, - maxInboundPayloadSize, - payloadDecoder, - connection.alloc(), - streamIdSupplier); - - this.connection = connection; + super(mtu, maxFrameLength, maxInboundPayloadSize, payloadDecoder, connection, streamIdSupplier); + this.leaseHandler = leaseHandler; this.onClose = MonoProcessor.create(); - UnboundedProcessor sendProcessor = super.getSendProcessor(); - // DO NOT Change the order here. The Send processor must be subscribed to before receiving connection.onClose().subscribe(null, this::tryTerminateOnConnectionError, this::tryShutdown); - connection.send(sendProcessor).subscribe(null, this::handleSendProcessorError); connection.receive().subscribe(this::handleIncomingFrames, e -> {}); @@ -103,7 +91,9 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { new ClientKeepAliveSupport(this.getAllocator(), keepAliveTickPeriod, keepAliveAckTimeout); this.keepAliveFramesAcceptor = keepAliveHandler.start( - keepAliveSupport, sendProcessor::onNextPrioritized, this::tryTerminateOnKeepAlive); + keepAliveSupport, + (keepAliveFrame) -> connection.sendFrame(0, keepAliveFrame), + this::tryTerminateOnKeepAlive); } else { keepAliveFramesAcceptor = null; } @@ -177,7 +167,7 @@ public int addAndGetNextStreamId(FrameHandler frameHandler) { @Override public double availability() { - return Math.min(connection.availability(), leaseHandler.availability()); + return Math.min(getDuplexConnection().availability(), leaseHandler.availability()); } @Override @@ -206,13 +196,9 @@ private void handleIncomingFrames(ByteBuf frame) { } } catch (Throwable t) { LOGGER.error("Unexpected error during frame handling", t); - super.getSendProcessor() - .onNext( - ErrorFrameCodec.encode( - super.getAllocator(), - 0, - new ConnectionErrorException("Unexpected error during frame handling", t))); - this.tryTerminateOnConnectionError(t); + final ConnectionErrorException error = + new ConnectionErrorException("Unexpected error during frame handling", t); + getDuplexConnection().sendErrorAndClose(error); } } @@ -332,7 +318,7 @@ private void terminate(Throwable e) { if (keepAliveFramesAcceptor != null) { keepAliveFramesAcceptor.dispose(); } - connection.dispose(); + getDuplexConnection().dispose(); leaseHandler.dispose(); synchronized (this) { @@ -347,15 +333,10 @@ private void terminate(Throwable e) { }); } - this.getSendProcessor().dispose(); if (e == CLOSED_CHANNEL_EXCEPTION) { onClose.onComplete(); } else { onClose.onError(e); } } - - private void handleSendProcessorError(Throwable t) { - connection.dispose(); - } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index 2368445c9..a4f4c9ef9 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -28,7 +28,6 @@ import io.rsocket.frame.RequestNFrameCodec; import io.rsocket.frame.RequestStreamFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.UnboundedProcessor; import io.rsocket.lease.ResponderLeaseHandler; import java.nio.channels.ClosedChannelException; import java.util.concurrent.CancellationException; @@ -48,7 +47,6 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { private static final Exception CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException(); - private final DuplexConnection connection; private final RSocket requestHandler; private final ResponderLeaseHandler leaseHandler; @@ -67,8 +65,7 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { int mtu, int maxFrameLength, int maxInboundPayloadSize) { - super(mtu, maxFrameLength, maxInboundPayloadSize, payloadDecoder, connection.alloc(), null); - this.connection = connection; + super(mtu, maxFrameLength, maxInboundPayloadSize, payloadDecoder, connection, null); this.requestHandler = requestHandler; @@ -76,24 +73,14 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { // DO NOT Change the order here. The Send processor must be subscribed to before receiving // connections - UnboundedProcessor sendProcessor = super.getSendProcessor(); - - connection.send(sendProcessor).subscribe(null, this::handleSendProcessorError); - connection.receive().subscribe(this::handleFrame, e -> {}); - leaseHandlerDisposable = leaseHandler.send(sendProcessor::onNextPrioritized); + leaseHandlerDisposable = leaseHandler.send(leaseFrame -> connection.sendFrame(0, leaseFrame)); - this.connection + connection .onClose() .subscribe(null, this::tryTerminateOnConnectionError, this::tryTerminateOnConnectionClose); } - private void handleSendProcessorError(Throwable t) { - for (FrameHandler frameHandler : activeStreams.values()) { - frameHandler.handleError(t); - } - } - private void tryTerminateOnConnectionError(Throwable e) { tryTerminate(() -> e); } @@ -106,7 +93,7 @@ private void tryTerminate(Supplier errorSupplier) { if (terminationError == null) { Throwable e = errorSupplier.get(); if (TERMINATION_ERROR.compareAndSet(this, null, e)) { - cleanup(e); + cleanup(); } } } @@ -195,21 +182,20 @@ public void dispose() { @Override public boolean isDisposed() { - return connection.isDisposed(); + return getDuplexConnection().isDisposed(); } @Override public Mono onClose() { - return connection.onClose(); + return getDuplexConnection().onClose(); } - private void cleanup(Throwable e) { + private void cleanup() { cleanUpSendingSubscriptions(); - connection.dispose(); + getDuplexConnection().dispose(); leaseHandlerDisposable.dispose(); requestHandler.dispose(); - super.getSendProcessor().dispose(); } private synchronized void cleanUpSendingSubscriptions() { @@ -282,8 +268,9 @@ private void handleFrame(ByteBuf frame) { } break; case SETUP: - super.getSendProcessor() - .onNext( + getDuplexConnection() + .sendFrame( + streamId, ErrorFrameCodec.encode( super.getAllocator(), streamId, @@ -291,8 +278,9 @@ private void handleFrame(ByteBuf frame) { break; case LEASE: default: - super.getSendProcessor() - .onNext( + getDuplexConnection() + .sendFrame( + streamId, ErrorFrameCodec.encode( super.getAllocator(), streamId, @@ -302,8 +290,9 @@ private void handleFrame(ByteBuf frame) { } } catch (Throwable t) { LOGGER.error("Unexpected error during frame handling", t); - super.getSendProcessor() - .onNext( + getDuplexConnection() + .sendFrame( + 0, ErrorFrameCodec.encode( super.getAllocator(), 0, 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 5a411e464..258306cd2 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -27,6 +27,7 @@ import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.RSocket; +import io.rsocket.RSocketErrorException; import io.rsocket.SocketAcceptor; import io.rsocket.exceptions.InvalidSetupException; import io.rsocket.exceptions.RejectedSetupException; @@ -36,6 +37,7 @@ import io.rsocket.lease.Leases; import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.plugins.DuplexConnectionInterceptor; import io.rsocket.plugins.InitializingInterceptorRegistry; import io.rsocket.plugins.InterceptorRegistry; import io.rsocket.resume.SessionManager; @@ -333,75 +335,73 @@ public Mono apply(DuplexConnection connection) { } private Mono acceptor( - ServerSetup serverSetup, DuplexConnection connection, int maxFrameLength) { + ServerSetup serverSetup, DuplexConnection sourceConnection, int maxFrameLength) { - ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(connection, interceptors, false); + final DuplexConnection interceptedConnection = + interceptors.initConnection(DuplexConnectionInterceptor.Type.SOURCE, sourceConnection); - return multiplexer - .asSetupConnection() - .receive() - .next() - .flatMap(startFrame -> accept(serverSetup, startFrame, multiplexer, maxFrameLength)); + return serverSetup + .init(LoggingDuplexConnection.wrapIfEnabled(interceptedConnection)) + .flatMap( + tuple2 -> { + final ByteBuf startFrame = tuple2.getT1(); + final DuplexConnection clientServerConnection = tuple2.getT2(); + + return accept(serverSetup, startFrame, clientServerConnection, maxFrameLength); + }); } private Mono acceptResume( - ServerSetup serverSetup, ByteBuf resumeFrame, ClientServerInputMultiplexer multiplexer) { - return serverSetup.acceptRSocketResume(resumeFrame, multiplexer); + ServerSetup serverSetup, ByteBuf resumeFrame, DuplexConnection clientServerConnection) { + return serverSetup.acceptRSocketResume(resumeFrame, clientServerConnection); } private Mono accept( ServerSetup serverSetup, ByteBuf startFrame, - ClientServerInputMultiplexer multiplexer, + DuplexConnection clientServerConnection, int maxFrameLength) { switch (FrameHeaderCodec.frameType(startFrame)) { case SETUP: - return acceptSetup(serverSetup, startFrame, multiplexer, maxFrameLength); + return acceptSetup(serverSetup, startFrame, clientServerConnection, maxFrameLength); case RESUME: - return acceptResume(serverSetup, startFrame, multiplexer); + return acceptResume(serverSetup, startFrame, clientServerConnection); default: - return serverSetup - .sendError( - multiplexer, - new InvalidSetupException( - "invalid setup frame: " + FrameHeaderCodec.frameType(startFrame))) - .doFinally( - signalType -> { - startFrame.release(); - multiplexer.dispose(); - }); + serverSetup.sendError( + clientServerConnection, + new InvalidSetupException("SETUP or RESUME frame must be received before any others")); + return clientServerConnection.onClose(); } } private Mono acceptSetup( ServerSetup serverSetup, ByteBuf setupFrame, - ClientServerInputMultiplexer multiplexer, + DuplexConnection clientServerConnection, int maxFrameLength) { if (!SetupFrameCodec.isSupportedVersion(setupFrame)) { - return serverSetup - .sendError( - multiplexer, - new InvalidSetupException( - "Unsupported version: " + SetupFrameCodec.humanReadableVersion(setupFrame))) - .doFinally(signalType -> multiplexer.dispose()); + serverSetup.sendError( + clientServerConnection, + new InvalidSetupException( + "Unsupported version: " + SetupFrameCodec.humanReadableVersion(setupFrame))); } boolean leaseEnabled = leasesSupplier != null; if (SetupFrameCodec.honorLease(setupFrame) && !leaseEnabled) { - return serverSetup - .sendError(multiplexer, new InvalidSetupException("lease is not supported")) - .doFinally(signalType -> multiplexer.dispose()); + serverSetup.sendError( + clientServerConnection, new InvalidSetupException("lease is not supported")); + return Mono.empty(); } return serverSetup.acceptRSocketSetup( setupFrame, - multiplexer, - (keepAliveHandler, wrappedMultiplexer) -> { + clientServerConnection, + (keepAliveHandler, wrappedDuplexConnection) -> { ConnectionSetupPayload setupPayload = new DefaultConnectionSetupPayload(setupFrame.retain()); + final ClientServerInputMultiplexer multiplexer = + new ClientServerInputMultiplexer(wrappedDuplexConnection, interceptors, false); Leases leases = leaseEnabled ? leasesSupplier.get() : null; RequesterLeaseHandler requesterLeaseHandler = @@ -411,7 +411,7 @@ private Mono acceptSetup( RSocket rSocketRequester = new RSocketRequester( - wrappedMultiplexer.asServerConnection(), + multiplexer.asServerConnection(), payloadDecoder, StreamIdSupplier.serverSupplier(), mtu, @@ -427,25 +427,25 @@ private Mono acceptSetup( return interceptors .initSocketAcceptor(acceptor) .accept(setupPayload, wrappedRSocketRequester) - .onErrorResume( - err -> - serverSetup - .sendError(multiplexer, rejectedSetupError(err)) - .then(Mono.error(err))) + .doOnError( + err -> serverSetup.sendError(wrappedDuplexConnection, rejectedSetupError(err))) .doOnNext( rSocketHandler -> { RSocket wrappedRSocketHandler = interceptors.initResponder(rSocketHandler); - DuplexConnection connection = wrappedMultiplexer.asClientConnection(); + DuplexConnection clientConnection = multiplexer.asClientConnection(); ResponderLeaseHandler responderLeaseHandler = leaseEnabled ? new ResponderLeaseHandler.Impl<>( - SERVER_TAG, connection.alloc(), leases.sender(), leases.stats()) + SERVER_TAG, + clientConnection.alloc(), + leases.sender(), + leases.stats()) : ResponderLeaseHandler.None; RSocket rSocketResponder = new RSocketResponder( - connection, + clientConnection, wrappedRSocketHandler, payloadDecoder, responderLeaseHandler, @@ -471,7 +471,7 @@ ServerSetup createSetup() { resume.isCleanupStoreOnKeepAlive()); } - private Exception rejectedSetupError(Throwable err) { + private RSocketErrorException rejectedSetupError(Throwable err) { String msg = err.getMessage(); return new RejectedSetupException(msg == null ? "rejected by server acceptor" : msg); } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java index 8355022c9..722a7c2c5 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java @@ -26,6 +26,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.util.IllegalReferenceCountException; +import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.frame.CancelFrameCodec; import io.rsocket.frame.ErrorFrameCodec; @@ -33,7 +34,6 @@ import io.rsocket.frame.PayloadFrameCodec; import io.rsocket.frame.RequestNFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.UnboundedProcessor; import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicLongFieldUpdater; @@ -54,7 +54,7 @@ final class RequestChannelRequesterFlux extends Flux final int maxFrameLength; final int maxInboundPayloadSize; final RequesterResponderSupport requesterResponderSupport; - final UnboundedProcessor sendProcessor; + final DuplexConnection connection; final PayloadDecoder payloadDecoder; final Publisher payloadsPublisher; @@ -84,7 +84,7 @@ final class RequestChannelRequesterFlux extends Flux this.maxFrameLength = requesterResponderSupport.getMaxFrameLength(); this.maxInboundPayloadSize = requesterResponderSupport.getMaxInboundPayloadSize(); this.requesterResponderSupport = requesterResponderSupport; - this.sendProcessor = requesterResponderSupport.getSendProcessor(); + this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); } @@ -125,8 +125,9 @@ public final void request(long n) { if (hasRequested(previousState)) { if (isFirstFrameSent(previousState) && !isMaxAllowedRequestN(extractRequestN(previousState))) { - final ByteBuf requestNFrame = RequestNFrameCodec.encode(this.allocator, this.streamId, n); - this.sendProcessor.onNext(requestNFrame); + final int streamId = this.streamId; + final ByteBuf requestNFrame = RequestNFrameCodec.encode(this.allocator, streamId, n); + this.connection.sendFrame(streamId, requestNFrame); } return; } @@ -182,7 +183,7 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { } final RequesterResponderSupport sm = this.requesterResponderSupport; - final UnboundedProcessor sender = this.sendProcessor; + final DuplexConnection connection = this.connection; final ByteBufAllocator allocator = this.allocator; final int streamId; @@ -209,7 +210,7 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { initialRequestN, mtu, firstPayload, - sender, + connection, allocator, // TODO: Should be a different flag in case of the scalar // source or if we know in advance upstream is mono @@ -236,7 +237,7 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { ReassemblyUtils.synchronizedRelease(this, previousState); final ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, streamId); - sender.onNext(cancelFrame); + connection.sendFrame(streamId, cancelFrame); return; } @@ -248,14 +249,14 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { long requestN = extractRequestN(previousState); if (isMaxAllowedRequestN(requestN)) { final ByteBuf requestNFrame = RequestNFrameCodec.encode(allocator, streamId, requestN); - sender.onNext(requestNFrame); + connection.sendFrame(streamId, requestNFrame); return; } if (requestN > initialRequestN) { final ByteBuf requestNFrame = RequestNFrameCodec.encode(allocator, streamId, requestN - initialRequestN); - sender.onNext(requestNFrame); + connection.sendFrame(streamId, requestNFrame); } } @@ -292,7 +293,7 @@ final void sendFollowingPayload(Payload followingPayload) { FrameType.NEXT, mtu, followingPayload, - this.sendProcessor, + this.connection, allocator, true); } catch (Throwable e) { @@ -339,7 +340,7 @@ public final void cancel() { ReassemblyUtils.synchronizedRelease(this, previousState); final ByteBuf cancelFrame = CancelFrameCodec.encode(this.allocator, streamId); - this.sendProcessor.onNext(cancelFrame); + this.connection.sendFrame(streamId, cancelFrame); } @Override @@ -369,7 +370,7 @@ public void onError(Throwable t) { this.requesterResponderSupport.remove(streamId, this); // propagates error to remote responder final ByteBuf errorFrame = ErrorFrameCodec.encode(this.allocator, streamId, t); - this.sendProcessor.onNext(errorFrame); + this.connection.sendFrame(streamId, errorFrame); if (!isInboundTerminated(previousState)) { // FIXME: must be scheduled on the connection event-loop to achieve serial @@ -409,7 +410,7 @@ public void onComplete() { } final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(this.allocator, streamId); - this.sendProcessor.onNext(completeFrame); + this.connection.sendFrame(streamId, completeFrame); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java index 0c2258950..67816407c 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java @@ -26,6 +26,7 @@ import io.netty.buffer.CompositeByteBuf; import io.netty.util.IllegalReferenceCountException; import io.netty.util.ReferenceCountUtil; +import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.exceptions.CanceledException; @@ -35,7 +36,6 @@ import io.rsocket.frame.PayloadFrameCodec; import io.rsocket.frame.RequestNFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.UnboundedProcessor; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; @@ -60,7 +60,7 @@ final class RequestChannelResponderSubscriber extends Flux final int maxFrameLength; final int maxInboundPayloadSize; final RequesterResponderSupport requesterResponderSupport; - final UnboundedProcessor sendProcessor; + final DuplexConnection connection; final long firstRequest; final RSocket handler; @@ -97,7 +97,7 @@ public RequestChannelResponderSubscriber( this.maxFrameLength = requesterResponderSupport.getMaxFrameLength(); this.maxInboundPayloadSize = requesterResponderSupport.getMaxInboundPayloadSize(); this.requesterResponderSupport = requesterResponderSupport; - this.sendProcessor = requesterResponderSupport.getSendProcessor(); + this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); this.handler = handler; this.firstRequest = firstRequestN; @@ -119,7 +119,7 @@ public RequestChannelResponderSubscriber( this.maxFrameLength = requesterResponderSupport.getMaxFrameLength(); this.maxInboundPayloadSize = requesterResponderSupport.getMaxInboundPayloadSize(); this.requesterResponderSupport = requesterResponderSupport; - this.sendProcessor = requesterResponderSupport.getSendProcessor(); + this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); this.firstRequest = firstRequestN; this.firstPayload = firstPayload; @@ -221,8 +221,9 @@ public void request(long n) { if (hasRequested(previousState)) { if (isFirstFrameSent(previousState) && !isMaxAllowedRequestN(StateUtils.extractRequestN(previousState))) { - final ByteBuf requestNFrame = RequestNFrameCodec.encode(this.allocator, this.streamId, n); - this.sendProcessor.onNext(requestNFrame); + final int streamId = this.streamId; + final ByteBuf requestNFrame = RequestNFrameCodec.encode(this.allocator, streamId, n); + this.connection.sendFrame(streamId, requestNFrame); } return; } @@ -262,14 +263,16 @@ public void request(long n) { long requestN = StateUtils.extractRequestN(previousState); if (isMaxAllowedRequestN(requestN)) { + final int streamId = this.streamId; final ByteBuf requestNFrame = RequestNFrameCodec.encode(allocator, streamId, requestN); - this.sendProcessor.onNext(requestNFrame); + this.connection.sendFrame(streamId, requestNFrame); } else { long firstRequestN = requestN - 1; if (firstRequestN > 0) { + final int streamId = this.streamId; final ByteBuf requestNFrame = - RequestNFrameCodec.encode(this.allocator, this.streamId, firstRequestN); - this.sendProcessor.onNext(requestNFrame); + RequestNFrameCodec.encode(this.allocator, streamId, firstRequestN); + this.connection.sendFrame(streamId, requestNFrame); } } } @@ -295,7 +298,7 @@ public void cancel() { } final ByteBuf cancelFrame = CancelFrameCodec.encode(this.allocator, streamId); - this.sendProcessor.onNext(cancelFrame); + this.connection.sendFrame(streamId, cancelFrame); } @Override @@ -472,10 +475,10 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) this.outboundDone = true; // send error to terminate interaction + final int streamId = this.streamId; final ByteBuf errorFrame = - ErrorFrameCodec.encode( - this.allocator, this.streamId, new CanceledException(t.getMessage())); - this.sendProcessor.onNext(errorFrame); + ErrorFrameCodec.encode(this.allocator, streamId, new CanceledException(t.getMessage())); + this.connection.sendFrame(streamId, errorFrame); return; } @@ -518,12 +521,13 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) this.outboundDone = true; // send error to terminate interaction + final int streamId = this.streamId; final ByteBuf errorFrame = ErrorFrameCodec.encode( this.allocator, - this.streamId, + streamId, new CanceledException("Failed to reassemble payload. Cause: " + e.getMessage())); - this.sendProcessor.onNext(errorFrame); + this.connection.sendFrame(streamId, errorFrame); return; } @@ -551,12 +555,13 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) } // send error to terminate interaction + final int streamId = this.streamId; final ByteBuf errorFrame = ErrorFrameCodec.encode( this.allocator, - this.streamId, + streamId, new CanceledException("Failed to reassemble payload. Cause: " + t.getMessage())); - this.sendProcessor.onNext(errorFrame); + this.connection.sendFrame(streamId, errorFrame); return; } @@ -583,12 +588,12 @@ public void onNext(Payload p) { } final int streamId = this.streamId; - final UnboundedProcessor sender = this.sendProcessor; + final DuplexConnection connection = this.connection; final ByteBufAllocator allocator = this.allocator; if (p == null) { final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(allocator, streamId); - sender.onNext(completeFrame); + connection.sendFrame(streamId, completeFrame); return; } @@ -614,7 +619,7 @@ public void onNext(Payload p) { streamId, new CanceledException( String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength))); - sender.onNext(errorFrame); + connection.sendFrame(streamId, errorFrame); return; } } catch (IllegalReferenceCountException e) { @@ -632,12 +637,12 @@ public void onNext(Payload p) { allocator, streamId, new CanceledException("Failed to validate payload. Cause:" + e.getMessage())); - sender.onNext(errorFrame); + connection.sendFrame(streamId, errorFrame); return; } try { - sendReleasingPayload(streamId, FrameType.NEXT, mtu, p, sender, allocator, false); + sendReleasingPayload(streamId, FrameType.NEXT, mtu, p, connection, allocator, false); } catch (Throwable t) { // FIXME: must be scheduled on the connection event-loop to achieve serial // behaviour on the inbound subscriber @@ -699,7 +704,7 @@ public void onError(Throwable t) { } final ByteBuf errorFrame = ErrorFrameCodec.encode(this.allocator, streamId, t); - this.sendProcessor.onNext(errorFrame); + this.connection.sendFrame(streamId, errorFrame); } @Override @@ -722,7 +727,7 @@ public void onComplete() { } final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(this.allocator, streamId); - this.sendProcessor.onNext(completeFrame); + this.connection.sendFrame(streamId, completeFrame); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java index 0ce91725b..1706ece32 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java @@ -25,11 +25,11 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.util.IllegalReferenceCountException; +import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.frame.CancelFrameCodec; import io.rsocket.frame.FrameType; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.UnboundedProcessor; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; @@ -49,7 +49,7 @@ final class RequestResponseRequesterMono extends Mono final int maxFrameLength; final int maxInboundPayloadSize; final RequesterResponderSupport requesterResponderSupport; - final UnboundedProcessor sendProcessor; + final DuplexConnection connection; final PayloadDecoder payloadDecoder; volatile long state; @@ -70,7 +70,7 @@ final class RequestResponseRequesterMono extends Mono this.maxFrameLength = requesterResponderSupport.getMaxFrameLength(); this.maxInboundPayloadSize = requesterResponderSupport.getMaxInboundPayloadSize(); this.requesterResponderSupport = requesterResponderSupport; - this.sendProcessor = requesterResponderSupport.getSendProcessor(); + this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); } @@ -122,7 +122,7 @@ public final void request(long n) { void sendFirstPayload(Payload payload, long initialRequestN) { final RequesterResponderSupport sm = this.requesterResponderSupport; - final UnboundedProcessor sender = this.sendProcessor; + final DuplexConnection connection = this.connection; final ByteBufAllocator allocator = this.allocator; final int streamId; @@ -143,7 +143,7 @@ void sendFirstPayload(Payload payload, long initialRequestN) { try { sendReleasingPayload( - streamId, FrameType.REQUEST_RESPONSE, this.mtu, payload, sender, allocator, true); + streamId, FrameType.REQUEST_RESPONSE, this.mtu, payload, connection, allocator, true); } catch (Throwable e) { this.done = true; lazyTerminate(STATE, this); @@ -163,7 +163,7 @@ void sendFirstPayload(Payload payload, long initialRequestN) { sm.remove(streamId, this); final ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, streamId); - sender.onNext(cancelFrame); + connection.sendFrame(streamId, cancelFrame); } } @@ -180,7 +180,7 @@ public final void cancel() { ReassemblyUtils.synchronizedRelease(this, previousState); - this.sendProcessor.onNext(CancelFrameCodec.encode(this.allocator, streamId)); + this.connection.sendFrame(streamId, CancelFrameCodec.encode(this.allocator, streamId)); } else if (!hasRequested(previousState)) { this.payload.release(); } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java index 36177e217..f36211c7d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java @@ -24,6 +24,7 @@ import io.netty.buffer.CompositeByteBuf; import io.netty.util.IllegalReferenceCountException; import io.netty.util.ReferenceCountUtil; +import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.exceptions.CanceledException; @@ -31,7 +32,6 @@ import io.rsocket.frame.FrameType; import io.rsocket.frame.PayloadFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.UnboundedProcessor; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.reactivestreams.Subscription; import org.slf4j.Logger; @@ -54,7 +54,7 @@ final class RequestResponseResponderSubscriber final int maxFrameLength; final int maxInboundPayloadSize; final RequesterResponderSupport requesterResponderSupport; - final UnboundedProcessor sendProcessor; + final DuplexConnection connection; final RSocket handler; @@ -77,7 +77,7 @@ public RequestResponseResponderSubscriber( this.maxFrameLength = requesterResponderSupport.getMaxFrameLength(); this.maxInboundPayloadSize = requesterResponderSupport.getMaxInboundPayloadSize(); this.requesterResponderSupport = requesterResponderSupport; - this.sendProcessor = requesterResponderSupport.getSendProcessor(); + this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); this.handler = handler; this.frames = @@ -93,7 +93,7 @@ public RequestResponseResponderSubscriber( this.maxFrameLength = requesterResponderSupport.getMaxFrameLength(); this.maxInboundPayloadSize = requesterResponderSupport.getMaxInboundPayloadSize(); this.requesterResponderSupport = requesterResponderSupport; - this.sendProcessor = requesterResponderSupport.getSendProcessor(); + this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = null; this.handler = null; @@ -129,14 +129,14 @@ public void onNext(@Nullable Payload p) { this.done = true; final int streamId = this.streamId; - final UnboundedProcessor sender = this.sendProcessor; + final DuplexConnection connection = this.connection; final ByteBufAllocator allocator = this.allocator; this.requesterResponderSupport.remove(streamId, this); if (p == null) { final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(allocator, streamId); - sender.onNext(completeFrame); + connection.sendFrame(streamId, completeFrame); return; } @@ -153,7 +153,7 @@ public void onNext(@Nullable Payload p) { streamId, new CanceledException( String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength))); - sender.onNext(errorFrame); + connection.sendFrame(streamId, errorFrame); return; } } catch (IllegalReferenceCountException e) { @@ -164,12 +164,12 @@ public void onNext(@Nullable Payload p) { allocator, streamId, new CanceledException("Failed to validate payload. Cause" + e.getMessage())); - sender.onNext(errorFrame); + connection.sendFrame(streamId, errorFrame); return; } try { - sendReleasingPayload(streamId, FrameType.NEXT_COMPLETE, mtu, p, sender, allocator, false); + sendReleasingPayload(streamId, FrameType.NEXT_COMPLETE, mtu, p, connection, allocator, false); } catch (Throwable ignored) { currentSubscription.cancel(); } @@ -196,7 +196,7 @@ public void onError(Throwable t) { this.requesterResponderSupport.remove(streamId, this); final ByteBuf errorFrame = ErrorFrameCodec.encode(this.allocator, streamId, t); - this.sendProcessor.onNext(errorFrame); + this.connection.sendFrame(streamId, errorFrame); } @Override @@ -256,12 +256,13 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) logger.debug("Reassembly has failed", t); // sends error frame from the responder side to tell that something went wrong + final int streamId = this.streamId; final ByteBuf errorFrame = ErrorFrameCodec.encode( this.allocator, - this.streamId, + streamId, new CanceledException("Failed to reassemble payload. Cause: " + t.getMessage())); - this.sendProcessor.onNext(errorFrame); + this.connection.sendFrame(streamId, errorFrame); return; } @@ -274,7 +275,8 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) } catch (Throwable t) { S.lazySet(this, Operators.cancelledSubscription()); - this.requesterResponderSupport.remove(this.streamId, this); + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); ReferenceCountUtil.safeRelease(frames); @@ -284,9 +286,9 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) final ByteBuf errorFrame = ErrorFrameCodec.encode( this.allocator, - this.streamId, + streamId, new CanceledException("Failed to reassemble payload. Cause: " + t.getMessage())); - this.sendProcessor.onNext(errorFrame); + this.connection.sendFrame(streamId, errorFrame); return; } 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 cf70109ea..a3107d4d6 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java @@ -25,12 +25,12 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.util.IllegalReferenceCountException; +import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.frame.CancelFrameCodec; import io.rsocket.frame.FrameType; import io.rsocket.frame.RequestNFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.UnboundedProcessor; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; @@ -50,7 +50,7 @@ final class RequestStreamRequesterFlux extends Flux final int maxFrameLength; final int maxInboundPayloadSize; final RequesterResponderSupport requesterResponderSupport; - final UnboundedProcessor sendProcessor; + final DuplexConnection connection; final PayloadDecoder payloadDecoder; volatile long state; @@ -69,7 +69,7 @@ final class RequestStreamRequesterFlux extends Flux this.maxFrameLength = requesterResponderSupport.getMaxFrameLength(); this.maxInboundPayloadSize = requesterResponderSupport.getMaxInboundPayloadSize(); this.requesterResponderSupport = requesterResponderSupport; - this.sendProcessor = requesterResponderSupport.getSendProcessor(); + this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); } @@ -117,8 +117,9 @@ public final void request(long n) { if (hasRequested(previousState)) { if (isFirstFrameSent(previousState) && !isMaxAllowedRequestN(extractRequestN(previousState))) { - final ByteBuf requestNFrame = RequestNFrameCodec.encode(this.allocator, this.streamId, n); - this.sendProcessor.onNext(requestNFrame); + final int streamId = this.streamId; + final ByteBuf requestNFrame = RequestNFrameCodec.encode(this.allocator, streamId, n); + this.connection.sendFrame(streamId, requestNFrame); } return; } @@ -129,7 +130,7 @@ public final void request(long n) { void sendFirstPayload(Payload payload, long initialRequestN) { final RequesterResponderSupport sm = this.requesterResponderSupport; - final UnboundedProcessor sender = this.sendProcessor; + final DuplexConnection connection = this.connection; final ByteBufAllocator allocator = this.allocator; final int streamId; @@ -155,7 +156,7 @@ void sendFirstPayload(Payload payload, long initialRequestN) { initialRequestN, this.mtu, payload, - sender, + connection, allocator, false); } catch (Throwable e) { @@ -177,7 +178,7 @@ void sendFirstPayload(Payload payload, long initialRequestN) { sm.remove(streamId, this); final ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, streamId); - sender.onNext(cancelFrame); + connection.sendFrame(streamId, cancelFrame); return; } @@ -189,14 +190,14 @@ void sendFirstPayload(Payload payload, long initialRequestN) { long requestN = extractRequestN(previousState); if (isMaxAllowedRequestN(requestN)) { final ByteBuf requestNFrame = RequestNFrameCodec.encode(allocator, streamId, requestN); - sender.onNext(requestNFrame); + connection.sendFrame(streamId, requestNFrame); return; } if (requestN > initialRequestN) { final ByteBuf requestNFrame = RequestNFrameCodec.encode(allocator, streamId, requestN - initialRequestN); - sender.onNext(requestNFrame); + connection.sendFrame(streamId, requestNFrame); } } @@ -213,7 +214,7 @@ public final void cancel() { ReassemblyUtils.synchronizedRelease(this, previousState); - this.sendProcessor.onNext(CancelFrameCodec.encode(this.allocator, streamId)); + this.connection.sendFrame(streamId, CancelFrameCodec.encode(this.allocator, streamId)); } else if (!hasRequested(previousState)) { // no need to send anything, since the first request has not happened this.payload.release(); 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 8486d9b24..620638d9c 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamResponderSubscriber.java @@ -24,6 +24,7 @@ import io.netty.buffer.CompositeByteBuf; import io.netty.util.IllegalReferenceCountException; import io.netty.util.ReferenceCountUtil; +import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.exceptions.CanceledException; @@ -31,7 +32,6 @@ import io.rsocket.frame.FrameType; import io.rsocket.frame.PayloadFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.UnboundedProcessor; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.reactivestreams.Subscription; import org.slf4j.Logger; @@ -54,7 +54,7 @@ final class RequestStreamResponderSubscriber final int maxFrameLength; final int maxInboundPayloadSize; final RequesterResponderSupport requesterResponderSupport; - final UnboundedProcessor sendProcessor; + final DuplexConnection connection; final RSocket handler; @@ -79,7 +79,7 @@ public RequestStreamResponderSubscriber( this.maxFrameLength = requesterResponderSupport.getMaxFrameLength(); this.maxInboundPayloadSize = requesterResponderSupport.getMaxInboundPayloadSize(); this.requesterResponderSupport = requesterResponderSupport; - this.sendProcessor = requesterResponderSupport.getSendProcessor(); + this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); this.handler = handler; this.frames = @@ -96,7 +96,7 @@ public RequestStreamResponderSubscriber( this.maxFrameLength = requesterResponderSupport.getMaxFrameLength(); this.maxInboundPayloadSize = requesterResponderSupport.getMaxInboundPayloadSize(); this.requesterResponderSupport = requesterResponderSupport; - this.sendProcessor = requesterResponderSupport.getSendProcessor(); + this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = null; this.handler = null; @@ -120,12 +120,12 @@ public void onNext(Payload p) { } final int streamId = this.streamId; - final UnboundedProcessor sender = this.sendProcessor; + final DuplexConnection sender = this.connection; final ByteBufAllocator allocator = this.allocator; if (p == null) { final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(allocator, streamId); - sender.onNext(completeFrame); + sender.sendFrame(streamId, completeFrame); return; } @@ -143,7 +143,7 @@ public void onNext(Payload p) { streamId, new CanceledException( String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength))); - sender.onNext(errorFrame); + sender.sendFrame(streamId, errorFrame); return; } } catch (IllegalReferenceCountException e) { @@ -154,7 +154,7 @@ public void onNext(Payload p) { allocator, streamId, new CanceledException("Failed to validate payload. Cause" + e.getMessage())); - sender.onNext(errorFrame); + sender.sendFrame(streamId, errorFrame); return; } @@ -190,7 +190,7 @@ public void onError(Throwable t) { this.requesterResponderSupport.remove(streamId, this); final ByteBuf errorFrame = ErrorFrameCodec.encode(this.allocator, streamId, t); - this.sendProcessor.onNext(errorFrame); + this.connection.sendFrame(streamId, errorFrame); } @Override @@ -210,7 +210,7 @@ public void onComplete() { this.requesterResponderSupport.remove(streamId, this); final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(this.allocator, streamId); - this.sendProcessor.onNext(completeFrame); + this.connection.sendFrame(streamId, completeFrame); } @Override @@ -278,7 +278,7 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas this.allocator, this.streamId, new CanceledException("Failed to reassemble payload. Cause: " + t.getMessage())); - this.sendProcessor.onNext(errorFrame); + this.connection.sendFrame(streamId, errorFrame); return; } @@ -304,7 +304,7 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas this.allocator, this.streamId, new CanceledException("Failed to reassemble payload. Cause: " + t.getMessage())); - this.sendProcessor.onNext(errorFrame); + this.connection.sendFrame(streamId, errorFrame); return; } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java b/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java index f5ddb199c..e3f70cede 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java @@ -1,11 +1,10 @@ package io.rsocket.core; -import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.collection.IntObjectHashMap; import io.netty.util.collection.IntObjectMap; +import io.rsocket.DuplexConnection; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.UnboundedProcessor; import reactor.util.annotation.Nullable; class RequesterResponderSupport { @@ -15,18 +14,17 @@ class RequesterResponderSupport { private final int maxInboundPayloadSize; private final PayloadDecoder payloadDecoder; private final ByteBufAllocator allocator; + private final DuplexConnection connection; @Nullable final StreamIdSupplier streamIdSupplier; final IntObjectMap activeStreams; - private final UnboundedProcessor sendProcessor; - public RequesterResponderSupport( int mtu, int maxFrameLength, int maxInboundPayloadSize, PayloadDecoder payloadDecoder, - ByteBufAllocator allocator, + DuplexConnection connection, @Nullable StreamIdSupplier streamIdSupplier) { this.activeStreams = new IntObjectHashMap<>(); @@ -34,9 +32,9 @@ public RequesterResponderSupport( this.maxFrameLength = maxFrameLength; this.maxInboundPayloadSize = maxInboundPayloadSize; this.payloadDecoder = payloadDecoder; - this.allocator = allocator; + this.allocator = connection.alloc(); this.streamIdSupplier = streamIdSupplier; - this.sendProcessor = new UnboundedProcessor<>(); + this.connection = connection; } public int getMtu() { @@ -59,8 +57,8 @@ public ByteBufAllocator getAllocator() { return allocator; } - public UnboundedProcessor getSendProcessor() { - return sendProcessor; + public DuplexConnection getDuplexConnection() { + return connection; } /** diff --git a/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java b/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java index 3e8cf34f5..53d222605 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java +++ b/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java @@ -22,6 +22,7 @@ import io.netty.buffer.Unpooled; import io.netty.util.IllegalReferenceCountException; import io.netty.util.ReferenceCounted; +import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.exceptions.CanceledException; import io.rsocket.frame.CancelFrameCodec; @@ -32,7 +33,6 @@ import io.rsocket.frame.RequestFireAndForgetFrameCodec; import io.rsocket.frame.RequestResponseFrameCodec; import io.rsocket.frame.RequestStreamFrameCodec; -import io.rsocket.internal.UnboundedProcessor; import java.util.function.Consumer; import reactor.core.publisher.Operators; import reactor.util.context.Context; @@ -55,7 +55,7 @@ static void sendReleasingPayload( FrameType frameType, int mtu, Payload payload, - UnboundedProcessor sendProcessor, + DuplexConnection connection, ByteBufAllocator allocator, boolean requester) { @@ -67,7 +67,7 @@ static void sendReleasingPayload( try { fragmentable = isFragmentable(mtu, data, metadata, false); } catch (IllegalReferenceCountException | NullPointerException e) { - sendTerminalFrame(streamId, frameType, sendProcessor, allocator, requester, false, e); + sendTerminalFrame(streamId, frameType, connection, allocator, requester, false, e); throw e; } @@ -81,11 +81,11 @@ static void sendReleasingPayload( FragmentationUtils.encodeFirstFragment( allocator, mtu, frameType, streamId, hasMetadata, slicedMetadata, slicedData); } catch (IllegalReferenceCountException e) { - sendTerminalFrame(streamId, frameType, sendProcessor, allocator, requester, false, e); + sendTerminalFrame(streamId, frameType, connection, allocator, requester, false, e); throw e; } - sendProcessor.onNext(first); + connection.sendFrame(streamId, first); boolean complete = frameType == FrameType.NEXT_COMPLETE; while (slicedData.isReadable() || slicedMetadata.isReadable()) { @@ -95,16 +95,16 @@ static void sendReleasingPayload( FragmentationUtils.encodeFollowsFragment( allocator, mtu, streamId, complete, slicedMetadata, slicedData); } catch (IllegalReferenceCountException e) { - sendTerminalFrame(streamId, frameType, sendProcessor, allocator, requester, true, e); + sendTerminalFrame(streamId, frameType, connection, allocator, requester, true, e); throw e; } - sendProcessor.onNext(following); + connection.sendFrame(streamId, following); } try { payload.release(); } catch (IllegalReferenceCountException e) { - sendTerminalFrame(streamId, frameType, sendProcessor, allocator, true, true, e); + sendTerminalFrame(streamId, frameType, connection, allocator, true, true, e); throw e; } } else { @@ -116,7 +116,7 @@ static void sendReleasingPayload( } catch (IllegalReferenceCountException e) { dataRetainedSlice.release(); - sendTerminalFrame(streamId, frameType, sendProcessor, allocator, requester, false, e); + sendTerminalFrame(streamId, frameType, connection, allocator, requester, false, e); throw e; } @@ -128,7 +128,7 @@ static void sendReleasingPayload( metadataRetainedSlice.release(); } - sendTerminalFrame(streamId, frameType, sendProcessor, allocator, requester, false, e); + sendTerminalFrame(streamId, frameType, connection, allocator, requester, false, e); throw e; } @@ -161,7 +161,7 @@ static void sendReleasingPayload( throw new IllegalArgumentException("Unsupported frame type " + frameType); } - sendProcessor.onNext(requestFrame); + connection.sendFrame(streamId, requestFrame); } } @@ -171,7 +171,7 @@ static void sendReleasingPayload( long initialRequestN, int mtu, Payload payload, - UnboundedProcessor sendProcessor, + DuplexConnection connection, ByteBufAllocator allocator, boolean complete) { @@ -183,7 +183,7 @@ static void sendReleasingPayload( try { fragmentable = isFragmentable(mtu, data, metadata, true); } catch (IllegalReferenceCountException | NullPointerException e) { - sendTerminalFrame(streamId, frameType, sendProcessor, allocator, true, false, e); + sendTerminalFrame(streamId, frameType, connection, allocator, true, false, e); throw e; } @@ -204,11 +204,11 @@ static void sendReleasingPayload( slicedMetadata, slicedData); } catch (IllegalReferenceCountException e) { - sendTerminalFrame(streamId, frameType, sendProcessor, allocator, true, false, e); + sendTerminalFrame(streamId, frameType, connection, allocator, true, false, e); throw e; } - sendProcessor.onNext(first); + connection.sendFrame(streamId, first); while (slicedData.isReadable() || slicedMetadata.isReadable()) { final ByteBuf following; @@ -217,16 +217,16 @@ static void sendReleasingPayload( FragmentationUtils.encodeFollowsFragment( allocator, mtu, streamId, complete, slicedMetadata, slicedData); } catch (IllegalReferenceCountException e) { - sendTerminalFrame(streamId, frameType, sendProcessor, allocator, true, true, e); + sendTerminalFrame(streamId, frameType, connection, allocator, true, true, e); throw e; } - sendProcessor.onNext(following); + connection.sendFrame(streamId, following); } try { payload.release(); } catch (IllegalReferenceCountException e) { - sendTerminalFrame(streamId, frameType, sendProcessor, allocator, true, true, e); + sendTerminalFrame(streamId, frameType, connection, allocator, true, true, e); throw e; } } else { @@ -238,7 +238,7 @@ static void sendReleasingPayload( } catch (IllegalReferenceCountException e) { dataRetainedSlice.release(); - sendTerminalFrame(streamId, frameType, sendProcessor, allocator, true, false, e); + sendTerminalFrame(streamId, frameType, connection, allocator, true, false, e); throw e; } @@ -250,7 +250,7 @@ static void sendReleasingPayload( metadataRetainedSlice.release(); } - sendTerminalFrame(streamId, frameType, sendProcessor, allocator, true, false, e); + sendTerminalFrame(streamId, frameType, connection, allocator, true, false, e); throw e; } @@ -281,14 +281,14 @@ static void sendReleasingPayload( throw new IllegalArgumentException("Unsupported frame type " + frameType); } - sendProcessor.onNext(requestFrame); + connection.sendFrame(streamId, requestFrame); } } static void sendTerminalFrame( int streamId, FrameType frameType, - UnboundedProcessor sendProcessor, + DuplexConnection connection, ByteBufAllocator allocator, boolean requester, boolean onFollowingFrame, @@ -297,7 +297,7 @@ static void sendTerminalFrame( if (onFollowingFrame) { if (requester) { final ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, streamId); - sendProcessor.onNext(cancelFrame); + connection.sendFrame(streamId, cancelFrame); } else { final ByteBuf errorFrame = ErrorFrameCodec.encode( @@ -308,7 +308,7 @@ static void sendTerminalFrame( + frameType + " frame. Cause: " + t.getMessage())); - sendProcessor.onNext(errorFrame); + connection.sendFrame(streamId, errorFrame); } } else { switch (frameType) { @@ -317,7 +317,7 @@ static void sendTerminalFrame( case PAYLOAD: if (requester) { final ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, streamId); - sendProcessor.onNext(cancelFrame); + connection.sendFrame(streamId, cancelFrame); } else { final ByteBuf errorFrame = ErrorFrameCodec.encode( @@ -325,7 +325,7 @@ static void sendTerminalFrame( streamId, new CanceledException( "Failed to encode " + frameType + " frame. Cause: " + t.getMessage())); - sendProcessor.onNext(errorFrame); + connection.sendFrame(streamId, errorFrame); } } } 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 eb86c6734..25dae6084 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java @@ -20,34 +20,39 @@ import io.netty.buffer.ByteBuf; import io.rsocket.DuplexConnection; +import io.rsocket.RSocketErrorException; import io.rsocket.exceptions.RejectedResumeException; import io.rsocket.exceptions.UnsupportedSetupException; -import io.rsocket.frame.ErrorFrameCodec; import io.rsocket.frame.ResumeFrameCodec; import io.rsocket.frame.SetupFrameCodec; import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.resume.*; +import java.nio.channels.ClosedChannelException; import java.time.Duration; import java.util.function.BiFunction; import java.util.function.Function; import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; abstract class ServerSetup { + Mono> init(DuplexConnection connection) { + return Mono.>create( + sink -> sink.onRequest(__ -> new SetupHandlingDuplexConnection(connection, sink))) + .or(connection.onClose().then(Mono.error(ClosedChannelException::new))); + } + abstract Mono acceptRSocketSetup( ByteBuf frame, - ClientServerInputMultiplexer multiplexer, - BiFunction> then); + DuplexConnection clientServerConnection, + BiFunction> then); - abstract Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer); + abstract Mono acceptRSocketResume(ByteBuf frame, DuplexConnection connection); void dispose() {} - Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { - DuplexConnection duplexConnection = multiplexer.asSetupConnection(); - return duplexConnection - .sendOne(ErrorFrameCodec.encode(duplexConnection.alloc(), 0, exception)) - .onErrorResume(err -> Mono.empty()); + void sendError(DuplexConnection duplexConnection, RSocketErrorException exception) { + duplexConnection.sendErrorAndClose(exception); } static class DefaultServerSetup extends ServerSetup { @@ -55,30 +60,21 @@ static class DefaultServerSetup extends ServerSetup { @Override public Mono acceptRSocketSetup( ByteBuf frame, - ClientServerInputMultiplexer multiplexer, - BiFunction> then) { + DuplexConnection duplexConnection, + BiFunction> then) { if (SetupFrameCodec.resumeEnabled(frame)) { - return sendError(multiplexer, new UnsupportedSetupException("resume not supported")) - .doFinally( - signalType -> { - frame.release(); - multiplexer.dispose(); - }); + sendError(duplexConnection, new UnsupportedSetupException("resume not supported")); + return Mono.empty(); } else { - return then.apply(new DefaultKeepAliveHandler(multiplexer), multiplexer); + return then.apply(new DefaultKeepAliveHandler(duplexConnection), duplexConnection); } } @Override - public Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer) { - - return sendError(multiplexer, new RejectedResumeException("resume not supported")) - .doFinally( - signalType -> { - frame.release(); - multiplexer.dispose(); - }); + public Mono acceptRSocketResume(ByteBuf frame, DuplexConnection duplexConnection) { + sendError(duplexConnection, new RejectedResumeException("resume not supported")); + return duplexConnection.onClose(); } } @@ -105,47 +101,43 @@ static class ResumableServerSetup extends ServerSetup { @Override public Mono acceptRSocketSetup( ByteBuf frame, - ClientServerInputMultiplexer multiplexer, - BiFunction> then) { + DuplexConnection duplexConnection, + BiFunction> then) { if (SetupFrameCodec.resumeEnabled(frame)) { ByteBuf resumeToken = SetupFrameCodec.resumeToken(frame); - ResumableDuplexConnection connection = - sessionManager - .save( - new ServerRSocketSession( - multiplexer.asClientServerConnection(), - resumeSessionDuration, - resumeStreamTimeout, - resumeStoreFactory, - resumeToken, - cleanupStoreOnKeepAlive)) - .resumableConnection(); + final ResumableFramesStore resumableFramesStore = resumeStoreFactory.apply(resumeToken); + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection(duplexConnection, resumableFramesStore); + final ServerRSocketSession serverRSocketSession = + new ServerRSocketSession( + resumeToken, + duplexConnection, + resumableDuplexConnection, + resumeSessionDuration, + resumableFramesStore, + cleanupStoreOnKeepAlive); + + sessionManager.save(serverRSocketSession, resumeToken); + return then.apply( - new ResumableKeepAliveHandler(connection), - new ClientServerInputMultiplexer(connection)); + new ResumableKeepAliveHandler( + resumableDuplexConnection, serverRSocketSession, serverRSocketSession), + resumableDuplexConnection); } else { - return then.apply(new DefaultKeepAliveHandler(multiplexer), multiplexer); + return then.apply(new DefaultKeepAliveHandler(duplexConnection), duplexConnection); } } @Override - public Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer) { + public Mono acceptRSocketResume(ByteBuf frame, DuplexConnection duplexConnection) { ServerRSocketSession session = sessionManager.get(ResumeFrameCodec.token(frame)); if (session != null) { - return session - .continueWith(multiplexer.asClientServerConnection()) - .resumeWith(frame) - .onClose() - .then(); + return session.resumeWith(frame, duplexConnection); } else { - return sendError(multiplexer, new RejectedResumeException("unknown resume token")) - .doFinally( - s -> { - frame.release(); - multiplexer.dispose(); - }); + sendError(duplexConnection, new RejectedResumeException("unknown resume token")); + return duplexConnection.onClose(); } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java new file mode 100644 index 000000000..b6bc87513 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java @@ -0,0 +1,170 @@ +package io.rsocket.core; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.RSocketErrorException; +import java.net.SocketAddress; +import java.nio.channels.ClosedChannelException; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoSink; +import reactor.core.publisher.Operators; +import reactor.util.context.Context; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +class SetupHandlingDuplexConnection extends Flux + implements DuplexConnection, CoreSubscriber, Subscription { + + final DuplexConnection source; + final MonoSink> sink; + + Subscription s; + boolean firstFrameReceived = false; + + CoreSubscriber actual; + + boolean done; + Throwable t; + + SetupHandlingDuplexConnection( + DuplexConnection source, MonoSink> sink) { + this.source = source; + this.sink = sink; + + source.receive().subscribe(this); + } + + @Override + public void dispose() { + source.dispose(); + } + + @Override + public boolean isDisposed() { + return source.isDisposed(); + } + + @Override + public Mono onClose() { + return source.onClose(); + } + + @Override + public void sendFrame(int streamId, ByteBuf frame) { + source.sendFrame(streamId, frame); + } + + @Override + public Flux receive() { + return this; + } + + @Override + public SocketAddress remoteAddress() { + return source.remoteAddress(); + } + + @Override + public void subscribe(CoreSubscriber actual) { + if (done) { + final Throwable t = this.t; + if (t == null) { + Operators.complete(actual); + } else { + Operators.error(actual, t); + } + return; + } + + this.actual = actual; + actual.onSubscribe(this); + } + + @Override + public void request(long n) { + if (n != Long.MAX_VALUE) { + actual.onError(new IllegalArgumentException("Only unbounded request is allowed")); + return; + } + + s.request(Long.MAX_VALUE); + } + + @Override + public void cancel() { + s.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (Operators.validate(this.s, s)) { + this.s = s; + s.request(1); + } + } + + @Override + public void onNext(ByteBuf frame) { + if (!firstFrameReceived) { + firstFrameReceived = true; + sink.success(Tuples.of(frame, this)); + return; + } + + actual.onNext(frame); + } + + @Override + public void onError(Throwable t) { + if (done) { + Operators.onErrorDropped(t, Context.empty()); + return; + } + + this.done = true; + this.t = t; + + if (!firstFrameReceived) { + sink.error(t); + return; + } + + final CoreSubscriber actual = this.actual; + if (actual != null) { + actual.onError(t); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + + this.done = true; + + if (!firstFrameReceived) { + sink.error(new ClosedChannelException()); + return; + } + + final CoreSubscriber actual = this.actual; + if (actual != null) { + actual.onComplete(); + } + } + + @Override + public void sendErrorAndClose(RSocketErrorException e) { + source.sendErrorAndClose(e); + } + + @Override + public ByteBufAllocator alloc() { + return source.alloc(); + } +} 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 9668e5e18..acbcfcf39 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java @@ -1,16 +1,28 @@ package io.rsocket.internal; +import io.netty.buffer.ByteBuf; import io.rsocket.DuplexConnection; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; public abstract class BaseDuplexConnection implements DuplexConnection { - private MonoProcessor onClose = MonoProcessor.create(); + protected MonoProcessor onClose = MonoProcessor.create(); + + protected UnboundedProcessor sender = new UnboundedProcessor<>(); public BaseDuplexConnection() { onClose.doFinally(s -> doOnClose()).subscribe(); } + @Override + public void sendFrame(int streamId, ByteBuf frame) { + if (streamId == 0) { + sender.onNextPrioritized(frame); + } else { + sender.onNext(frame); + } + } + protected abstract void doOnClose(); @Override diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java index dd7b485f9..8b1378917 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java @@ -1,250 +1 @@ -/* - * 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.internal; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.rsocket.Closeable; -import io.rsocket.DuplexConnection; -import io.rsocket.frame.FrameHeaderCodec; -import io.rsocket.frame.FrameUtil; -import io.rsocket.plugins.DuplexConnectionInterceptor.Type; -import io.rsocket.plugins.InitializingInterceptorRegistry; -import java.net.SocketAddress; -import org.reactivestreams.Publisher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; - -/** - * {@link DuplexConnection#receive()} is a single stream on which the following type of frames - * arrive: - * - *

      - *
    • Frames for streams initiated by the initiator of the connection (client). - *
    • Frames for streams initiated by the acceptor of the connection (server). - *
    - * - *

    The only way to differentiate these two frames is determining whether the stream Id is odd or - * even. Even IDs are for the streams initiated by server and odds are for streams initiated by the - * client. - * - * @deprecated since 1.1.0-M1 in favor of package-private {@link - * io.rsocket.core.ClientServerInputMultiplexer} - */ -@Deprecated -public class ClientServerInputMultiplexer implements Closeable { - private static final Logger LOGGER = LoggerFactory.getLogger("io.rsocket.FrameLogger"); - private static final InitializingInterceptorRegistry emptyInterceptorRegistry = - new InitializingInterceptorRegistry(); - - private final DuplexConnection setupConnection; - private final DuplexConnection serverConnection; - private final DuplexConnection clientConnection; - private final DuplexConnection source; - private final DuplexConnection clientServerConnection; - - private boolean setupReceived; - - public ClientServerInputMultiplexer(DuplexConnection source) { - this(source, emptyInterceptorRegistry, false); - } - - public ClientServerInputMultiplexer( - DuplexConnection source, InitializingInterceptorRegistry registry, boolean isClient) { - this.source = source; - final MonoProcessor> setup = MonoProcessor.create(); - final MonoProcessor> server = MonoProcessor.create(); - final MonoProcessor> client = MonoProcessor.create(); - - source = registry.initConnection(Type.SOURCE, source); - setupConnection = - registry.initConnection(Type.SETUP, new InternalDuplexConnection(source, setup)); - serverConnection = - registry.initConnection(Type.SERVER, new InternalDuplexConnection(source, server)); - clientConnection = - registry.initConnection(Type.CLIENT, new InternalDuplexConnection(source, client)); - clientServerConnection = new InternalDuplexConnection(source, client, server); - - source - .receive() - .groupBy( - frame -> { - int streamId = FrameHeaderCodec.streamId(frame); - final Type type; - if (streamId == 0) { - switch (FrameHeaderCodec.frameType(frame)) { - case SETUP: - case RESUME: - case RESUME_OK: - type = Type.SETUP; - setupReceived = true; - break; - case LEASE: - case KEEPALIVE: - case ERROR: - type = isClient ? Type.CLIENT : Type.SERVER; - break; - default: - type = isClient ? Type.SERVER : Type.CLIENT; - } - } else if ((streamId & 0b1) == 0) { - type = Type.SERVER; - } else { - type = Type.CLIENT; - } - if (!isClient && type != Type.SETUP && !setupReceived) { - frame.release(); - throw new IllegalStateException( - "SETUP or LEASE frame must be received before any others."); - } - return type; - }) - .subscribe( - group -> { - switch (group.key()) { - case SETUP: - setup.onNext(group); - break; - - case SERVER: - server.onNext(group); - break; - - case CLIENT: - client.onNext(group); - break; - } - }, - ex -> { - setup.onError(ex); - server.onError(ex); - client.onError(ex); - }); - } - - public DuplexConnection asClientServerConnection() { - return clientServerConnection; - } - - public DuplexConnection asServerConnection() { - return serverConnection; - } - - public DuplexConnection asClientConnection() { - return clientConnection; - } - - public DuplexConnection asSetupConnection() { - return setupConnection; - } - - @Override - public void dispose() { - source.dispose(); - } - - @Override - public boolean isDisposed() { - return source.isDisposed(); - } - - @Override - public Mono onClose() { - return source.onClose(); - } - - private static class InternalDuplexConnection implements DuplexConnection { - private final DuplexConnection source; - private final MonoProcessor>[] processors; - private final boolean debugEnabled; - - @SafeVarargs - public InternalDuplexConnection( - DuplexConnection source, MonoProcessor>... processors) { - this.source = source; - this.processors = processors; - this.debugEnabled = LOGGER.isDebugEnabled(); - } - - @Override - public Mono send(Publisher frame) { - if (debugEnabled) { - frame = Flux.from(frame).doOnNext(f -> LOGGER.debug("sending -> " + FrameUtil.toString(f))); - } - - return source.send(frame); - } - - @Override - public Mono sendOne(ByteBuf frame) { - if (debugEnabled) { - LOGGER.debug("sending -> " + FrameUtil.toString(frame)); - } - - return source.sendOne(frame); - } - - @Override - public Flux receive() { - return Flux.fromArray(processors) - .flatMap( - p -> - p.flatMapMany( - f -> { - if (debugEnabled) { - return f.doOnNext( - frame -> LOGGER.debug("receiving -> " + FrameUtil.toString(frame))); - } else { - return f; - } - })); - } - - @Override - public ByteBufAllocator alloc() { - return source.alloc(); - } - - @Override - public SocketAddress remoteAddress() { - return source.remoteAddress(); - } - - @Override - public void dispose() { - source.dispose(); - } - - @Override - public boolean isDisposed() { - return source.isDisposed(); - } - - @Override - public Mono onClose() { - return source.onClose(); - } - - @Override - public double availability() { - return source.availability(); - } - } -} 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 2535c342b..b92c25f46 100644 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java @@ -3,7 +3,9 @@ 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; +import io.rsocket.resume.ResumeStateHolder; import java.util.function.Consumer; public interface KeepAliveHandler { @@ -34,10 +36,18 @@ public KeepAliveFramesAcceptor start( } class ResumableKeepAliveHandler implements KeepAliveHandler { + private final ResumableDuplexConnection resumableDuplexConnection; + private final RSocketSession rSocketSession; + private final ResumeStateHolder resumeStateHolder; - public ResumableKeepAliveHandler(ResumableDuplexConnection resumableDuplexConnection) { + public ResumableKeepAliveHandler( + ResumableDuplexConnection resumableDuplexConnection, + RSocketSession rSocketSession, + ResumeStateHolder resumeStateHolder) { this.resumableDuplexConnection = resumableDuplexConnection; + this.rSocketSession = rSocketSession; + this.resumeStateHolder = resumeStateHolder; } @Override @@ -45,10 +55,11 @@ public KeepAliveFramesAcceptor start( KeepAliveSupport keepAliveSupport, Consumer onSendKeepAliveFrame, Consumer onTimeout) { - resumableDuplexConnection.onResume(keepAliveSupport::start); - resumableDuplexConnection.onDisconnect(keepAliveSupport::stop); + + rSocketSession.setKeepAliveSupport(keepAliveSupport); + return keepAliveSupport - .resumeState(resumableDuplexConnection) + .resumeState(resumeStateHolder) .onSendKeepAliveFrame(onSendKeepAliveFrame) .onTimeout(keepAlive -> resumableDuplexConnection.disconnect()) .start(); diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java index 6b2a7a71b..5d3a43b03 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java @@ -27,6 +27,8 @@ extends BiFunction { enum Type { + /** @deprecated since 1.1.0-M2. Will be removed in 1.2 */ + @Deprecated SETUP, CLIENT, SERVER, 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 ed9450357..f5463a6e9 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -20,175 +20,232 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.exceptions.ConnectionErrorException; -import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.exceptions.Exceptions; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; import io.rsocket.frame.ResumeFrameCodec; import io.rsocket.frame.ResumeOkFrameCodec; -import io.rsocket.internal.ClientServerInputMultiplexer; +import io.rsocket.keepalive.KeepAliveSupport; import java.time.Duration; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.Function; +import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; +import reactor.core.CoreSubscriber; import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.util.function.Tuple2; import reactor.util.retry.Retry; -public class ClientRSocketSession implements RSocketSession> { +public class ClientRSocketSession + implements RSocketSession, + ResumeStateHolder, + CoreSubscriber> { + private static final Logger logger = LoggerFactory.getLogger(ClientRSocketSession.class); - private final ResumableDuplexConnection resumableConnection; - private volatile Mono newConnection; - private volatile ByteBuf resumeToken; - private final ByteBufAllocator allocator; + final ResumableDuplexConnection resumableConnection; + final Mono> connectionFactory; + final ResumableFramesStore resumableFramesStore; + + final ByteBufAllocator allocator; + final Duration resumeSessionDuration; + final Retry retry; + final boolean cleanupStoreOnKeepAlive; + final ByteBuf resumeToken; + + volatile Subscription s; + static final AtomicReferenceFieldUpdater S = + AtomicReferenceFieldUpdater.newUpdater(ClientRSocketSession.class, Subscription.class, "s"); + + KeepAliveSupport keepAliveSupport; public ClientRSocketSession( - DuplexConnection duplexConnection, + ByteBuf resumeToken, + DuplexConnection initialDuplexConnection, + ResumableDuplexConnection resumableDuplexConnection, + Mono connectionFactory, + Function>> connectionTransformer, + ResumableFramesStore resumableFramesStore, Duration resumeSessionDuration, Retry retry, - ResumableFramesStore resumableFramesStore, - Duration resumeStreamTimeout, boolean cleanupStoreOnKeepAlive) { - this.allocator = duplexConnection.alloc(); - this.resumableConnection = - new ResumableDuplexConnection( - "client", - duplexConnection, - resumableFramesStore, - resumeStreamTimeout, - cleanupStoreOnKeepAlive); - - /*session completed: release token initially retained in resumeToken(ByteBuf)*/ - onClose().doFinally(s -> resumeToken.release()).subscribe(); - - resumableConnection - .connectionErrors() - .flatMap( - err -> { - logger.debug("Client session connection error. Starting new connection"); - AtomicBoolean once = new AtomicBoolean(); - return newConnection - .delaySubscription( - once.compareAndSet(false, true) - ? retry.generateCompanion(Flux.just(new RetrySignal(err))) - : Mono.empty()) - .retryWhen(retry) - .timeout(resumeSessionDuration); - }) - .map(ClientServerInputMultiplexer::new) - .subscribe( - multiplexer -> { - /*reconnect resumable connection*/ - reconnect(multiplexer.asClientServerConnection()); - long impliedPosition = resumableConnection.impliedPosition(); - long position = resumableConnection.position(); - logger.debug( - "Client ResumableConnection reconnected. Sending RESUME frame with state: [impliedPos: {}, pos: {}]", - impliedPosition, - position); - /*Connection is established again: send RESUME frame to server, listen for RESUME_OK*/ - sendFrame( - ResumeFrameCodec.encode( - allocator, - /*retain so token is not released once sent as part of resume frame*/ - resumeToken.retain(), - impliedPosition, - position)) - .then(multiplexer.asSetupConnection().receive().next()) - .subscribe(this::resumeWith); - }, - err -> { - logger.debug("Client ResumableConnection reconnect timeout"); - resumableConnection.dispose(); + this.resumeToken = resumeToken; + this.connectionFactory = + connectionFactory.flatMap( + dc -> { + dc.sendFrame( + 0, + ResumeFrameCodec.encode( + dc.alloc(), + resumeToken.retain(), + resumableFramesStore.frameImpliedPosition(), // observed on the client side + resumableFramesStore.framePosition() // sent from the client sent + )); + logger.debug("Resume Frame has been sent"); + + return connectionTransformer.apply(dc); }); - } + this.resumableFramesStore = resumableFramesStore; + this.allocator = resumableDuplexConnection.alloc(); + this.resumeSessionDuration = resumeSessionDuration; + this.retry = retry; + this.cleanupStoreOnKeepAlive = cleanupStoreOnKeepAlive; + this.resumableConnection = resumableDuplexConnection; - @Override - public ClientRSocketSession continueWith(Mono connectionFactory) { - this.newConnection = connectionFactory; - return this; - } + resumableDuplexConnection.onClose().doFinally(__ -> dispose()).subscribe(); - @Override - public ClientRSocketSession resumeWith(ByteBuf resumeOkFrame) { - logger.debug("ResumeOK FRAME received"); - long remotePos = remotePos(resumeOkFrame); - long remoteImpliedPos = remoteImpliedPos(resumeOkFrame); - resumeOkFrame.release(); - - resumableConnection.resume( - remotePos, - remoteImpliedPos, - pos -> - pos.then() - /*Resumption is impossible: send CONNECTION_ERROR*/ - .onErrorResume( - err -> - sendFrame( - ErrorFrameCodec.encode( - allocator, 0, errorFrameThrowable(remoteImpliedPos))) - .then(Mono.fromRunnable(resumableConnection::dispose)) - /*Resumption is impossible: no need to return control to ResumableConnection*/ - .then(Mono.never()))); - return this; - } + observeDisconnection(initialDuplexConnection); - public ClientRSocketSession resumeToken(ByteBuf resumeToken) { - /*retain so token is not released once sent as part of setup frame*/ - this.resumeToken = resumeToken.retain(); - return this; + S.lazySet(this, Operators.cancelledSubscription()); } - @Override - public void reconnect(DuplexConnection connection) { - resumableConnection.reconnect(connection); + void reconnect() { + if (this.s == Operators.cancelledSubscription() + && S.compareAndSet(this, Operators.cancelledSubscription(), null)) { + keepAliveSupport.stop(); + connectionFactory.retryWhen(retry).timeout(resumeSessionDuration).subscribe(this); + logger.debug("Connection is lost. Reconnecting..."); + } } - @Override - public ResumableDuplexConnection resumableConnection() { - return resumableConnection; + void observeDisconnection(DuplexConnection activeConnection) { + activeConnection.onClose().subscribe(null, e -> reconnect(), () -> reconnect()); } @Override - public ByteBuf token() { - return resumeToken; + public long impliedPosition() { + return resumableFramesStore.frameImpliedPosition(); } - private Mono sendFrame(ByteBuf frame) { - return resumableConnection.sendOne(frame).onErrorResume(err -> Mono.empty()); + @Override + public void onImpliedPosition(long remoteImpliedPos) { + if (cleanupStoreOnKeepAlive) { + try { + resumableFramesStore.releaseFrames(remoteImpliedPos); + } catch (Throwable e) { + resumableConnection.sendErrorAndClose(new ConnectionErrorException(e.getMessage(), e)); + } + } } - private static long remoteImpliedPos(ByteBuf resumeOkFrame) { - return ResumeOkFrameCodec.lastReceivedClientPos(resumeOkFrame); + @Override + public void dispose() { + if (Operators.terminate(S, this)) { + resumableFramesStore.dispose(); + resumableConnection.dispose(); + resumeToken.release(); + } } - private static long remotePos(ByteBuf resumeOkFrame) { - return -1; + @Override + public boolean isDisposed() { + return resumableConnection.isDisposed(); } - private static ConnectionErrorException errorFrameThrowable(long impliedPos) { - return new ConnectionErrorException("resumption_server_pos=[" + impliedPos + "]"); + @Override + public void onSubscribe(Subscription s) { + if (Operators.setOnce(S, this, s)) { + s.request(Long.MAX_VALUE); + } } - private static class RetrySignal implements Retry.RetrySignal { - - private final Throwable ex; - - RetrySignal(Throwable ex) { - this.ex = ex; + @Override + public synchronized void onNext(Tuple2 tuple2) { + ByteBuf frame = tuple2.getT1(); + DuplexConnection nextDuplexConnection = tuple2.getT2(); + + if (!Operators.terminate(S, this)) { + logger.debug("Session has already been expired. Terminating received connection"); + final ConnectionErrorException connectionErrorException = + new ConnectionErrorException("resumption_server=[Session Expired]"); + nextDuplexConnection.sendErrorAndClose(connectionErrorException); + return; } - @Override - public long totalRetries() { - return 0; + final FrameType frameType = FrameHeaderCodec.nativeFrameType(frame); + final int streamId = FrameHeaderCodec.streamId(frame); + + if (streamId != 0) { + logger.debug( + "Illegal first frame received. RESUME_OK frame must be received before any others. Terminating received connection"); + resumableConnection.dispose(); + final ConnectionErrorException connectionErrorException = + new ConnectionErrorException("RESUME_OK frame must be received before any others"); + nextDuplexConnection.sendErrorAndClose(connectionErrorException); + return; } - @Override - public long totalRetriesInARow() { - return 0; + if (frameType == FrameType.RESUME_OK) { + long remoteImpliedPos = ResumeOkFrameCodec.lastReceivedClientPos(frame); + final long position = resumableFramesStore.framePosition(); + final long impliedPosition = resumableFramesStore.frameImpliedPosition(); + logger.debug( + "ResumeOK FRAME received. ServerResumeState{observedFramesPosition[{}]}. ClientResumeState{observedFramesPosition[{}], sentFramesPosition[{}]}", + remoteImpliedPos, + impliedPosition, + position); + if (position <= remoteImpliedPos) { + try { + if (position != remoteImpliedPos) { + resumableFramesStore.releaseFrames(remoteImpliedPos); + } + } catch (IllegalStateException e) { + logger.debug("Exception occurred while releasing frames in the frameStore", e); + resumableConnection.dispose(); + final ConnectionErrorException t = new ConnectionErrorException(e.getMessage(), e); + nextDuplexConnection.sendErrorAndClose(t); + return; + } + + if (resumableConnection.connect(nextDuplexConnection)) { + observeDisconnection(nextDuplexConnection); + keepAliveSupport.start(); + logger.debug("Session has been resumed successfully"); + } else { + logger.debug("Session has already been expired. Terminating received connection"); + final ConnectionErrorException connectionErrorException = + new ConnectionErrorException("resumption_server_pos=[Session Expired]"); + nextDuplexConnection.sendErrorAndClose(connectionErrorException); + } + } else { + logger.debug( + "Mismatching remote and local state. Expected RemoteImpliedPosition[{}] to be greater or equal to the LocalPosition[{}]. Terminating received connection", + remoteImpliedPos, + position); + resumableConnection.dispose(); + final ConnectionErrorException connectionErrorException = + new ConnectionErrorException("resumption_server_pos=[" + remoteImpliedPos + "]"); + nextDuplexConnection.sendErrorAndClose(connectionErrorException); + } + } else if (frameType == FrameType.ERROR) { + final RuntimeException exception = Exceptions.from(0, frame); + logger.debug("Received error frame. Terminating received connection", exception); + resumableConnection.dispose(); + } else { + logger.debug( + "Illegal first frame received. RESUME_OK frame must be received before any others. Terminating received connection"); + resumableConnection.dispose(); + final ConnectionErrorException connectionErrorException = + new ConnectionErrorException("RESUME_OK frame must be received before any others"); + nextDuplexConnection.sendErrorAndClose(connectionErrorException); } + } - @Override - public Throwable failure() { - return ex; + @Override + public void onError(Throwable t) { + if (!Operators.terminate(S, this)) { + Operators.onErrorDropped(t, currentContext()); } + + resumableConnection.dispose(); + } + + @Override + public void onComplete() {} + + public void setKeepAliveSupport(KeepAliveSupport keepAliveSupport) { + this.keepAliveSupport = keepAliveSupport; } } 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 1875b7eac..a6148bd08 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java @@ -16,42 +16,70 @@ package io.rsocket.resume; +import static io.rsocket.resume.ResumableDuplexConnection.isResumableFrame; + import io.netty.buffer.ByteBuf; -import java.util.Queue; -import org.reactivestreams.Subscriber; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; -import reactor.util.concurrent.Queues; +import reactor.core.publisher.Operators; + +/** + * writes - n (where n is frequent, primary operation) reads - m (where m == KeepAliveFrequency) + * skip - k -> 0 (where k is the rare operation which happens after disconnection + */ +public class InMemoryResumableFramesStore extends Flux + implements CoreSubscriber, ResumableFramesStore, Subscription { -public class InMemoryResumableFramesStore implements ResumableFramesStore { private static final Logger logger = LoggerFactory.getLogger(InMemoryResumableFramesStore.class); - private static final long SAVE_REQUEST_SIZE = Long.MAX_VALUE; - private final MonoProcessor disposed = MonoProcessor.create(); - volatile long position; + final MonoProcessor disposed = MonoProcessor.create(); + final ArrayList cachedFrames; + final String tag; + final int cacheLimit; + volatile long impliedPosition; + static final AtomicLongFieldUpdater IMPLIED_POSITION = + AtomicLongFieldUpdater.newUpdater(InMemoryResumableFramesStore.class, "impliedPosition"); + + volatile long position; + static final AtomicLongFieldUpdater POSITION = + AtomicLongFieldUpdater.newUpdater(InMemoryResumableFramesStore.class, "position"); + volatile int cacheSize; - final Queue cachedFrames; - private final String tag; - private final int cacheLimit; - private volatile int upstreamFrameRefCnt; + static final AtomicIntegerFieldUpdater CACHE_SIZE = + AtomicIntegerFieldUpdater.newUpdater(InMemoryResumableFramesStore.class, "cacheSize"); + + CoreSubscriber saveFramesSubscriber; + + CoreSubscriber actual; + + volatile int state; + static final AtomicIntegerFieldUpdater STATE = + AtomicIntegerFieldUpdater.newUpdater(InMemoryResumableFramesStore.class, "state"); public InMemoryResumableFramesStore(String tag, int cacheSizeBytes) { this.tag = tag; this.cacheLimit = cacheSizeBytes; - this.cachedFrames = cachedFramesQueue(cacheSizeBytes); + this.cachedFrames = new ArrayList<>(); } public Mono saveFrames(Flux frames) { - MonoProcessor completed = MonoProcessor.create(); - frames - .doFinally(s -> completed.onComplete()) - .subscribe(new FramesSubscriber(SAVE_REQUEST_SIZE)); - return completed; + return frames + .transform( + Operators.lift( + (__, actual) -> { + this.saveFramesSubscriber = actual; + return this; + })) + .then(); } @Override @@ -59,48 +87,42 @@ public void releaseFrames(long remoteImpliedPos) { long pos = position; logger.debug( "{} Removing frames for local: {}, remote implied: {}", tag, pos, remoteImpliedPos); - long removeSize = Math.max(0, remoteImpliedPos - pos); - while (removeSize > 0) { - ByteBuf cachedFrame = cachedFrames.poll(); - if (cachedFrame != null) { - removeSize -= releaseTailFrame(cachedFrame); - } else { - break; + long toRemoveBytes = Math.max(0, remoteImpliedPos - pos); + int removedBytes = 0; + final ArrayList frames = cachedFrames; + synchronized (this) { + while (toRemoveBytes > removedBytes && frames.size() > 0) { + ByteBuf cachedFrame = frames.remove(0); + int frameSize = cachedFrame.readableBytes(); + // logger.debug( + // "{} Removing frame {}", tag, + // cachedFrame.toString(CharsetUtil.UTF_8)); + cachedFrame.release(); + removedBytes += frameSize; } } - if (removeSize > 0) { + + if (toRemoveBytes > removedBytes) { throw new IllegalStateException( String.format( "Local and remote state disagreement: " + "need to remove additional %d bytes, but cache is empty", - removeSize)); - } else if (removeSize < 0) { + toRemoveBytes)); + } else if (toRemoveBytes < removedBytes) { throw new IllegalStateException( "Local and remote state disagreement: " + "local and remote frame sizes are not equal"); } else { - logger.debug("{} Removed frames. Current cache size: {}", tag, cacheSize); + POSITION.addAndGet(this, removedBytes); + if (cacheLimit != Integer.MAX_VALUE) { + CACHE_SIZE.addAndGet(this, -removedBytes); + logger.debug("{} Removed frames. Current cache size: {}", tag, cacheSize); + } } } @Override public Flux resumeStream() { - return Flux.generate( - () -> new ResumeStreamState(cachedFrames.size(), upstreamFrameRefCnt), - (state, sink) -> { - if (state.next()) { - /*spsc queue has no iterator - iterating by consuming*/ - ByteBuf frame = cachedFrames.poll(); - if (state.shouldRetain(frame)) { - frame.retain(); - } - cachedFrames.offer(frame); - sink.next(frame); - } else { - sink.complete(); - logger.debug("{} Resuming stream completed", tag); - } - return state; - }); + return this; } @Override @@ -110,13 +132,46 @@ public long framePosition() { @Override public long frameImpliedPosition() { - return impliedPosition; + return impliedPosition & Long.MAX_VALUE; } @Override - public void resumableFrameReceived(ByteBuf frame) { - /*called on transport thread so non-atomic on volatile is safe*/ - impliedPosition += frame.readableBytes(); + public boolean resumableFrameReceived(ByteBuf frame) { + final int frameSize = frame.readableBytes(); + for (; ; ) { + final long impliedPosition = this.impliedPosition; + + if (impliedPosition < 0) { + return false; + } + + if (IMPLIED_POSITION.compareAndSet(this, impliedPosition, impliedPosition + frameSize)) { + return true; + } + } + } + + void pauseImplied() { + for (; ; ) { + final long impliedPosition = this.impliedPosition; + + if (IMPLIED_POSITION.compareAndSet(this, impliedPosition, impliedPosition | Long.MIN_VALUE)) { + logger.debug("Tag {}. Paused at position[{}]", tag, impliedPosition); + return; + } + } + } + + void resumeImplied() { + for (; ; ) { + final long impliedPosition = this.impliedPosition; + + final long restoredImpliedPosition = impliedPosition & Long.MAX_VALUE; + if (IMPLIED_POSITION.compareAndSet(this, impliedPosition, restoredImpliedPosition)) { + logger.debug("Tag {}. Resumed at position[{}]", tag, restoredImpliedPosition); + return; + } + } } @Override @@ -126,13 +181,18 @@ public Mono onClose() { @Override public void dispose() { - cacheSize = 0; - ByteBuf frame = cachedFrames.poll(); - while (frame != null) { - frame.release(); - frame = cachedFrames.poll(); + if (STATE.getAndSet(this, 2) != 2) { + cacheSize = 0; + synchronized (this) { + for (ByteBuf frame : cachedFrames) { + if (frame != null) { + frame.release(); + } + } + cachedFrames.clear(); + } + disposed.onComplete(); } - disposed.onComplete(); } @Override @@ -140,101 +200,92 @@ public boolean isDisposed() { return disposed.isTerminated(); } - /* this method and saveFrame() won't be called concurrently, - * so non-atomic on volatile is safe*/ - private int releaseTailFrame(ByteBuf content) { - int frameSize = content.readableBytes(); - cacheSize -= frameSize; - position += frameSize; - content.release(); - return frameSize; + @Override + public void onSubscribe(Subscription s) { + saveFramesSubscriber.onSubscribe(Operators.emptySubscription()); + s.request(Long.MAX_VALUE); } - /*this method and releaseTailFrame() won't be called concurrently, - * so non-atomic on volatile is safe*/ - void saveFrame(ByteBuf frame) { - if (upstreamFrameRefCnt == 0) { - upstreamFrameRefCnt = frame.refCnt(); - } - - int frameSize = frame.readableBytes(); - long availableSize = cacheLimit - cacheSize; - while (availableSize < frameSize) { - ByteBuf cachedFrame = cachedFrames.poll(); - if (cachedFrame != null) { - availableSize += releaseTailFrame(cachedFrame); - } else { - break; - } - } - if (availableSize >= frameSize) { - cachedFrames.offer(frame.retain()); - cacheSize += frameSize; - } else { - position += frameSize; - } + @Override + public void onError(Throwable t) { + saveFramesSubscriber.onError(t); } - static class ResumeStreamState { - private final int cacheSize; - private final int expectedRefCnt; - private int cacheCounter; + @Override + public void onComplete() { + saveFramesSubscriber.onComplete(); + } - public ResumeStreamState(int cacheSize, int expectedRefCnt) { - this.cacheSize = cacheSize; - this.expectedRefCnt = expectedRefCnt; - } + @Override + public void onNext(ByteBuf frame) { + final boolean isResumable = isResumableFrame(frame); + if (isResumable) { + final ArrayList frames = cachedFrames; + int incomingFrameSize = frame.readableBytes(); + final int cacheLimit = this.cacheLimit; + if (cacheLimit != Integer.MAX_VALUE) { + long availableSize = cacheLimit - cacheSize; + if (availableSize < incomingFrameSize) { + int removedBytes = 0; + synchronized (this) { + while (availableSize < incomingFrameSize) { + if (frames.size() == 0) { + break; + } + ByteBuf cachedFrame; + cachedFrame = frames.remove(0); + final int frameSize = cachedFrame.readableBytes(); + availableSize += frameSize; + removedBytes += frameSize; + cachedFrame.release(); + } + } + CACHE_SIZE.addAndGet(this, -removedBytes); + POSITION.addAndGet(this, removedBytes); + } + } - public boolean next() { - if (cacheCounter < cacheSize) { - cacheCounter++; - return true; - } else { - return false; + synchronized (this) { + frames.add(frame); + } + if (cacheLimit != Integer.MAX_VALUE) { + CACHE_SIZE.addAndGet(this, incomingFrameSize); } } - public boolean shouldRetain(ByteBuf frame) { - return frame.refCnt() == expectedRefCnt; + final int state = this.state; + final CoreSubscriber actual = this.actual; + if (state == 1) { + actual.onNext(frame.retain()); + } else if (!isResumable) { + frame.release(); } } - static Queue cachedFramesQueue(int size) { - return Queues.get(size).get(); - } - - class FramesSubscriber implements Subscriber { - private final long firstRequestSize; - private final long refillSize; - private int received; - private Subscription s; - - public FramesSubscriber(long requestSize) { - this.firstRequestSize = requestSize; - this.refillSize = firstRequestSize / 2; - } + @Override + public void request(long n) {} - @Override - public void onSubscribe(Subscription s) { - this.s = s; - s.request(firstRequestSize); - } + @Override + public void cancel() { + pauseImplied(); + state = 0; + } - @Override - public void onNext(ByteBuf byteBuf) { - saveFrame(byteBuf); - if (firstRequestSize != Long.MAX_VALUE && ++received == refillSize) { - received = 0; - s.request(refillSize); + @Override + public void subscribe(CoreSubscriber actual) { + final int state = this.state; + logger.debug("Tag: {}. Subscribed State[{}]", tag, state); + actual.onSubscribe(this); + if (state != 2) { + synchronized (this) { + for (final ByteBuf frame : cachedFrames) { + actual.onNext(frame.retain()); + } } - } - @Override - public void onError(Throwable t) { - logger.info("unexpected onError signal: {}, {}", t.getClass(), t.getMessage()); + this.actual = actual; + resumeImplied(); + STATE.compareAndSet(this, 0, 1); } - - @Override - public void onComplete() {} } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java index 7ec0abaee..6dd3d5f4d 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java @@ -16,35 +16,10 @@ package io.rsocket.resume; -import io.netty.buffer.ByteBuf; -import io.rsocket.Closeable; -import io.rsocket.DuplexConnection; -import reactor.core.publisher.Mono; +import io.rsocket.keepalive.KeepAliveSupport; +import reactor.core.Disposable; -public interface RSocketSession extends Closeable { +public interface RSocketSession extends Disposable { - ByteBuf token(); - - ResumableDuplexConnection resumableConnection(); - - RSocketSession continueWith(T ConnectionFactory); - - RSocketSession resumeWith(ByteBuf resumeFrame); - - void reconnect(DuplexConnection connection); - - @Override - default Mono onClose() { - return resumableConnection().onClose(); - } - - @Override - default void dispose() { - resumableConnection().dispose(); - } - - @Override - default boolean isDisposed() { - return resumableConnection().isDisposed(); - } + void setKeepAliveSupport(KeepAliveSupport keepAliveSupport); } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/RequestListener.java b/rsocket-core/src/main/java/io/rsocket/resume/RequestListener.java deleted file mode 100644 index 6553e5ec5..000000000 --- a/rsocket-core/src/main/java/io/rsocket/resume/RequestListener.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2015-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.resume; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.ReplayProcessor; - -class RequestListener { - private final ReplayProcessor requests = ReplayProcessor.create(1); - - public Flux apply(Flux flux) { - return flux.doOnRequest(requests::onNext); - } - - public Flux requests() { - return requests; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java index 7bd471583..60484f9d1 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-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. @@ -18,439 +18,300 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.rsocket.Closeable; import io.rsocket.DuplexConnection; +import io.rsocket.RSocketErrorException; import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.internal.UnboundedProcessor; import java.net.SocketAddress; -import java.nio.channels.ClosedChannelException; -import java.time.Duration; -import java.util.Queue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; -import org.reactivestreams.Publisher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; import reactor.core.Disposable; -import reactor.core.Disposables; -import reactor.core.publisher.*; -import reactor.util.concurrent.Queues; - -public class ResumableDuplexConnection implements DuplexConnection, ResumeStateHolder { - private static final Logger logger = LoggerFactory.getLogger(ResumableDuplexConnection.class); - private static final Throwable closedChannelException = new ClosedChannelException(); - - private final String tag; - private final ResumableFramesStore resumableFramesStore; - private final Duration resumeStreamTimeout; - private final boolean cleanupOnKeepAlive; - - private final ReplayProcessor connections = ReplayProcessor.create(1); - private final EmitterProcessor connectionErrors = EmitterProcessor.create(); - private volatile DuplexConnection curConnection; - /*used instead of EmitterProcessor because its autocancel=false capability had no expected effect*/ - private final FluxProcessor downStreamFrames = ReplayProcessor.create(0); - private final FluxProcessor resumeSaveFrames = EmitterProcessor.create(); - private final MonoProcessor resumeSaveCompleted = MonoProcessor.create(); - private final Queue actions = Queues.unboundedMultiproducer().get(); - private final AtomicInteger actionsWip = new AtomicInteger(); - private final AtomicBoolean disposed = new AtomicBoolean(); - - private final Mono framesSent; - private final RequestListener downStreamRequestListener = new RequestListener(); - private final RequestListener resumeSaveStreamRequestListener = new RequestListener(); - private final UnicastProcessor> upstreams = UnicastProcessor.create(); - private final UpstreamFramesSubscriber upstreamSubscriber = - new UpstreamFramesSubscriber( - Queues.SMALL_BUFFER_SIZE, - downStreamRequestListener.requests(), - resumeSaveStreamRequestListener.requests(), - this::dispatch); - - private volatile Runnable onResume; - private volatile Runnable onDisconnect; - private volatile int state; - private volatile Disposable resumedStreamDisposable = Disposables.disposed(); +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Operators; + +public class ResumableDuplexConnection extends Flux + implements DuplexConnection, Subscription { + + final ResumableFramesStore resumableFramesStore; + + final UnboundedProcessor savableFramesSender; + final Disposable framesSaverDisposable; + final MonoProcessor onClose; + final SocketAddress remoteAddress; + + CoreSubscriber receiveSubscriber; + FrameReceivingSubscriber activeReceivingSubscriber; + + volatile int state; + static final AtomicIntegerFieldUpdater STATE = + AtomicIntegerFieldUpdater.newUpdater(ResumableDuplexConnection.class, "state"); + + volatile DuplexConnection activeConnection; + static final AtomicReferenceFieldUpdater + ACTIVE_CONNECTION = + AtomicReferenceFieldUpdater.newUpdater( + ResumableDuplexConnection.class, DuplexConnection.class, "activeConnection"); public ResumableDuplexConnection( - String tag, - DuplexConnection duplexConnection, - ResumableFramesStore resumableFramesStore, - Duration resumeStreamTimeout, - boolean cleanupOnKeepAlive) { - this.tag = tag; + DuplexConnection initialConnection, ResumableFramesStore resumableFramesStore) { this.resumableFramesStore = resumableFramesStore; - this.resumeStreamTimeout = resumeStreamTimeout; - this.cleanupOnKeepAlive = cleanupOnKeepAlive; - - resumableFramesStore - .saveFrames(resumeSaveStreamRequestListener.apply(resumeSaveFrames)) - .subscribe(resumeSaveCompleted); - - upstreams.flatMap(Function.identity()).subscribe(upstreamSubscriber); - - framesSent = - connections - .switchMap( - c -> { - logger.debug("Switching transport: {}", tag); - return c.send(downStreamRequestListener.apply(downStreamFrames)) - .doFinally( - s -> - logger.debug( - "{} Transport send completed: {}, {}", tag, s, c.toString())) - .onErrorResume(err -> Mono.never()); - }) - .then() - .cache(); - - reconnect(duplexConnection); - } + this.savableFramesSender = new UnboundedProcessor<>(); + this.framesSaverDisposable = resumableFramesStore.saveFrames(savableFramesSender).subscribe(); + this.onClose = MonoProcessor.create(); + this.remoteAddress = initialConnection.remoteAddress(); - @Override - public ByteBufAllocator alloc() { - return curConnection.alloc(); + ACTIVE_CONNECTION.lazySet(this, initialConnection); } - @Override - public SocketAddress remoteAddress() { - return curConnection.remoteAddress(); - } + public boolean connect(DuplexConnection nextConnection) { + final DuplexConnection activeConnection = this.activeConnection; + if (activeConnection != DisposedConnection.INSTANCE + && ACTIVE_CONNECTION.compareAndSet(this, activeConnection, nextConnection)) { - public void disconnect() { - DuplexConnection c = this.curConnection; - if (c != null) { - disconnect(c); - } - } + activeConnection.dispose(); - public void onDisconnect(Runnable onDisconnectAction) { - this.onDisconnect = onDisconnectAction; - } - - public void onResume(Runnable onResumeAction) { - this.onResume = onResumeAction; - } + initConnection(nextConnection); - /*reconnected by session after error. After this downstream can receive frames, - * but sending in suppressed until resume() is called*/ - public void reconnect(DuplexConnection connection) { - if (curConnection == null) { - logger.debug("{} Resumable duplex connection started with connection: {}", tag, connection); - state = State.CONNECTED; - onNewConnection(connection); + return true; } else { - logger.debug( - "{} Resumable duplex connection reconnected with connection: {}", tag, connection); - /*race between sendFrame and doResumeStart may lead to ongoing upstream frames - written before resume complete*/ - dispatch(new ResumeStart(connection)); + return false; } } - /*after receiving RESUME (Server) or RESUME_OK (Client) - calculate and send resume frames */ - public void resume( - long remotePos, long remoteImpliedPos, Function, Mono> resumeFrameSent) { - /*race between sendFrame and doResume may lead to duplicate frames on resume store*/ - dispatch(new Resume(remotePos, remoteImpliedPos, resumeFrameSent)); + void initConnection(DuplexConnection nextConnection) { + final FrameReceivingSubscriber frameReceivingSubscriber = + new FrameReceivingSubscriber(resumableFramesStore, receiveSubscriber); + this.activeReceivingSubscriber = frameReceivingSubscriber; + final Disposable disposable = + resumableFramesStore + .resumeStream() + .subscribe(f -> nextConnection.sendFrame(FrameHeaderCodec.streamId(f), f)); + nextConnection.receive().subscribe(frameReceivingSubscriber); + nextConnection + .onClose() + .doFinally( + __ -> { + frameReceivingSubscriber.dispose(); + disposable.dispose(); + }) + .subscribe(); } - @Override - public Mono sendOne(ByteBuf frame) { - return curConnection.sendOne(frame); + public void disconnect() { + final DuplexConnection activeConnection = this.activeConnection; + if (activeConnection != DisposedConnection.INSTANCE) { + activeConnection.dispose(); + } } @Override - public Mono send(Publisher frames) { - upstreams.onNext(Flux.from(frames)); - return framesSent; + public void sendFrame(int streamId, ByteBuf frame) { + if (streamId == 0) { + savableFramesSender.onNextPrioritized(frame); + } else { + savableFramesSender.onNext(frame); + } } @Override - public Flux receive() { - return connections.switchMap( - c -> - c.receive() - .doOnNext( - f -> { - if (isResumableFrame(f)) { - resumableFramesStore.resumableFrameReceived(f); - } - }) - .onErrorResume(err -> Mono.never())); - } + public void sendErrorAndClose(RSocketErrorException rSocketErrorException) { + final DuplexConnection activeConnection = + ACTIVE_CONNECTION.getAndSet(this, DisposedConnection.INSTANCE); + if (activeConnection == DisposedConnection.INSTANCE) { + return; + } - public long position() { - return resumableFramesStore.framePosition(); + activeConnection.sendErrorAndClose(rSocketErrorException); + activeConnection + .onClose() + .subscribe( + null, + t -> { + framesSaverDisposable.dispose(); + savableFramesSender.dispose(); + onClose.onError(t); + }, + () -> { + framesSaverDisposable.dispose(); + savableFramesSender.dispose(); + final Throwable cause = rSocketErrorException.getCause(); + if (cause == null) { + onClose.onComplete(); + } else { + onClose.onError(cause); + } + }); } @Override - public long impliedPosition() { - return resumableFramesStore.frameImpliedPosition(); + public Flux receive() { + return this; } @Override - public void onImpliedPosition(long remoteImpliedPos) { - logger.debug("Got remote position from keep-alive: {}", remoteImpliedPos); - if (cleanupOnKeepAlive) { - dispatch(new ReleaseFrames(remoteImpliedPos)); - } + public ByteBufAllocator alloc() { + return activeConnection.alloc(); } @Override public Mono onClose() { - return Flux.merge(connections.last().flatMap(Closeable::onClose), resumeSaveCompleted).then(); + return onClose; } @Override public void dispose() { - if (disposed.compareAndSet(false, true)) { - logger.debug("Resumable connection disposed: {}, {}", tag, this); - upstreams.onComplete(); - connections.onComplete(); - connectionErrors.onComplete(); - resumeSaveFrames.onComplete(); - curConnection.dispose(); - upstreamSubscriber.dispose(); - resumedStreamDisposable.dispose(); - resumableFramesStore.dispose(); + final DuplexConnection activeConnection = + ACTIVE_CONNECTION.getAndSet(this, DisposedConnection.INSTANCE); + if (activeConnection == DisposedConnection.INSTANCE) { + return; } + + if (activeConnection != null) { + activeConnection.dispose(); + } + + framesSaverDisposable.dispose(); + activeReceivingSubscriber.dispose(); + savableFramesSender.dispose(); + onClose.onComplete(); } @Override - public double availability() { - return curConnection.availability(); + public boolean isDisposed() { + return onClose.isDisposed(); } @Override - public boolean isDisposed() { - return disposed.get(); + public SocketAddress remoteAddress() { + return remoteAddress; } - private void sendFrame(ByteBuf f) { - if (disposed.get()) { - f.release(); - return; - } - /*resuming from store so no need to save again*/ - if (state != State.RESUME && isResumableFrame(f)) { - resumeSaveFrames.onNext(f); - } - /*filter frames coming from upstream before actual resumption began, - * to preserve frames ordering*/ - if (state != State.RESUME_STARTED) { - downStreamFrames.onNext(f); + @Override + public void request(long n) { + if (state == 1 && STATE.compareAndSet(this, 1, 2)) { + initConnection(this.activeConnection); } } - Flux connectionErrors() { - return connectionErrors; + @Override + public void cancel() { + dispose(); } - private void dispatch(Object action) { - actions.offer(action); - if (actionsWip.getAndIncrement() == 0) { - do { - Object a = actions.poll(); - if (a instanceof ByteBuf) { - sendFrame((ByteBuf) a); - } else { - ((Runnable) a).run(); - } - } while (actionsWip.decrementAndGet() != 0); + @Override + public void subscribe(CoreSubscriber receiverSubscriber) { + if (state == 0 && STATE.compareAndSet(this, 0, 1)) { + receiveSubscriber = receiverSubscriber; + receiverSubscriber.onSubscribe(this); } } - private void doResumeStart(DuplexConnection connection) { - state = State.RESUME_STARTED; - resumedStreamDisposable.dispose(); - upstreamSubscriber.resumeStart(); - onNewConnection(connection); + static boolean isResumableFrame(ByteBuf frame) { + return FrameHeaderCodec.streamId(frame) != 0; } - private void doResume( - long remotePosition, - long remoteImpliedPosition, - Function, Mono> sendResumeFrame) { - long localPosition = position(); - long localImpliedPosition = impliedPosition(); - - logger.debug("Resumption start"); - logger.debug( - "Resumption states. local: [pos: {}, impliedPos: {}], remote: [pos: {}, impliedPos: {}]", - localPosition, - localImpliedPosition, - remotePosition, - remoteImpliedPosition); - - long remoteImpliedPos = - calculateRemoteImpliedPos( - localPosition, localImpliedPosition, - remotePosition, remoteImpliedPosition); - - Mono impliedPositionOrError; - if (remoteImpliedPos >= 0) { - state = State.RESUME; - releaseFramesToPosition(remoteImpliedPos); - impliedPositionOrError = Mono.just(localImpliedPosition); - } else { - impliedPositionOrError = - Mono.error( - new ResumeStateException( - localPosition, localImpliedPosition, - remotePosition, remoteImpliedPosition)); - } + private static final class DisposedConnection implements DuplexConnection { - sendResumeFrame - .apply(impliedPositionOrError) - .doOnSuccess( - v -> { - Runnable r = this.onResume; - if (r != null) { - r.run(); - } - }) - .then( - streamResumedFrames( - resumableFramesStore - .resumeStream() - .timeout(resumeStreamTimeout) - .doFinally(s -> dispatch(new ResumeComplete()))) - .doOnError(err -> dispose())) - .onErrorResume(err -> Mono.empty()) - .subscribe(); - } + static final DisposedConnection INSTANCE = new DisposedConnection(); - static long calculateRemoteImpliedPos( - long pos, long impliedPos, long remotePos, long remoteImpliedPos) { - if (remotePos <= impliedPos && pos <= remoteImpliedPos) { - return remoteImpliedPos; - } else { - return -1L; - } - } + private DisposedConnection() {} - private void doResumeComplete() { - logger.debug("Completing resumption"); - state = State.RESUME_COMPLETED; - upstreamSubscriber.resumeComplete(); - } + @Override + public void dispose() {} - private Mono streamResumedFrames(Flux frames) { - return Mono.create( - s -> { - ResumeFramesSubscriber subscriber = - new ResumeFramesSubscriber( - downStreamRequestListener.requests(), this::dispatch, s::error, s::success); - s.onDispose(subscriber); - resumedStreamDisposable = subscriber; - frames.subscribe(subscriber); - }); - } + @Override + public Mono onClose() { + return Mono.never(); + } - private void onNewConnection(DuplexConnection connection) { - curConnection = connection; - connection.onClose().doFinally(v -> disconnect(connection)).subscribe(); - connections.onNext(connection); - } + @Override + public void sendFrame(int streamId, ByteBuf frame) {} - private void disconnect(DuplexConnection connection) { - /*do not report late disconnects on old connection if new one is available*/ - if (curConnection == connection && state != State.DISCONNECTED) { - connection.dispose(); - state = State.DISCONNECTED; - logger.debug( - "{} Inner connection disconnected: {}", - tag, - closedChannelException.getClass().getSimpleName()); - connectionErrors.onNext(closedChannelException); - Runnable r = this.onDisconnect; - if (r != null) { - r.run(); - } + @Override + public Flux receive() { + return Flux.never(); } - } - /*remove frames confirmed by implied pos, - set current pos accordingly*/ - private void releaseFramesToPosition(long remoteImpliedPos) { - resumableFramesStore.releaseFrames(remoteImpliedPos); - } + @Override + public void sendErrorAndClose(RSocketErrorException e) {} - static boolean isResumableFrame(ByteBuf frame) { - switch (FrameHeaderCodec.nativeFrameType(frame)) { - case REQUEST_CHANNEL: - case REQUEST_STREAM: - case REQUEST_RESPONSE: - case REQUEST_FNF: - case REQUEST_N: - case CANCEL: - case ERROR: - case PAYLOAD: - return true; - default: - return false; + @Override + public ByteBufAllocator alloc() { + return ByteBufAllocator.DEFAULT; } - } - static class State { - static int CONNECTED = 0; - static int RESUME_STARTED = 1; - static int RESUME = 2; - static int RESUME_COMPLETED = 3; - static int DISCONNECTED = 4; + @Override + @SuppressWarnings("ConstantConditions") + public SocketAddress remoteAddress() { + return null; + } } - class ResumeStart implements Runnable { - private final DuplexConnection connection; + private static final class FrameReceivingSubscriber + implements CoreSubscriber, Disposable { + + final ResumableFramesStore resumableFramesStore; + final CoreSubscriber actual; + + volatile Subscription s; + static final AtomicReferenceFieldUpdater S = + AtomicReferenceFieldUpdater.newUpdater( + FrameReceivingSubscriber.class, Subscription.class, "s"); - public ResumeStart(DuplexConnection connection) { - this.connection = connection; + boolean cancelled; + + private FrameReceivingSubscriber( + ResumableFramesStore store, CoreSubscriber actual) { + this.resumableFramesStore = store; + this.actual = actual; } @Override - public void run() { - doResumeStart(connection); + public void onSubscribe(Subscription s) { + if (Operators.setOnce(S, this, s)) { + s.request(Long.MAX_VALUE); + } } - } - class Resume implements Runnable { - private final long remotePos; - private final long remoteImpliedPos; - private final Function, Mono> resumeFrameSent; + @Override + public void onNext(ByteBuf frame) { + if (cancelled || s == Operators.cancelledSubscription()) { + return; + } + + if (isResumableFrame(frame)) { + if (resumableFramesStore.resumableFrameReceived(frame)) { + actual.onNext(frame); + } + return; + } - public Resume( - long remotePos, long remoteImpliedPos, Function, Mono> resumeFrameSent) { - this.remotePos = remotePos; - this.remoteImpliedPos = remoteImpliedPos; - this.resumeFrameSent = resumeFrameSent; + actual.onNext(frame); } @Override - public void run() { - doResume(remotePos, remoteImpliedPos, resumeFrameSent); + public void onError(Throwable t) { + Operators.set(S, this, Operators.cancelledSubscription()); } - } - - private class ResumeComplete implements Runnable { @Override - public void run() { - doResumeComplete(); + public void onComplete() { + Operators.set(S, this, Operators.cancelledSubscription()); } - } - private class ReleaseFrames implements Runnable { - private final long remoteImpliedPos; - - public ReleaseFrames(long remoteImpliedPos) { - this.remoteImpliedPos = remoteImpliedPos; + @Override + public void dispose() { + cancelled = true; + Operators.terminate(S, this); } @Override - public void run() { - releaseFramesToPosition(remoteImpliedPos); + public boolean isDisposed() { + return cancelled || s == Operators.cancelledSubscription(); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java index 3a30544b6..80d9a36dd 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java @@ -50,6 +50,8 @@ public interface ResumableFramesStore extends Closeable { /** * Received resumable frame as defined by RSocket protocol. Implementation must increment frame * implied position + * + * @return {@code true} if information about the frame has been stored */ - void resumableFrameReceived(ByteBuf frame); + boolean resumableFrameReceived(ByteBuf frame); } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeFramesSubscriber.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeFramesSubscriber.java deleted file mode 100644 index 4facdd3c1..000000000 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumeFramesSubscriber.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2015-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.resume; - -import io.netty.buffer.ByteBuf; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.Disposable; -import reactor.core.publisher.Flux; - -class ResumeFramesSubscriber implements Subscriber, Disposable { - private final Flux requests; - private final Consumer onNext; - private final Consumer onError; - private final Runnable onComplete; - private final AtomicBoolean disposed = new AtomicBoolean(); - private volatile Disposable requestsDisposable; - private volatile Subscription subscription; - - public ResumeFramesSubscriber( - Flux requests, - Consumer onNext, - Consumer onError, - Runnable onComplete) { - this.requests = requests; - this.onNext = onNext; - this.onError = onError; - this.onComplete = onComplete; - } - - @Override - public void onSubscribe(Subscription s) { - if (isDisposed()) { - s.cancel(); - } else { - this.subscription = s; - this.requestsDisposable = requests.subscribe(s::request); - } - } - - @Override - public void onNext(ByteBuf frame) { - this.onNext.accept(frame); - } - - @Override - public void onError(Throwable t) { - this.onError.accept(t); - requestsDisposable.dispose(); - } - - @Override - public void onComplete() { - this.onComplete.run(); - requestsDisposable.dispose(); - } - - @Override - public void dispose() { - if (disposed.compareAndSet(false, true)) { - if (subscription != null) { - subscription.cancel(); - requestsDisposable.dispose(); - } - } - } - - @Override - public boolean isDisposed() { - return disposed.get(); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java index b54ce644f..ad405afc0 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -18,143 +18,190 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.CharsetUtil; import io.rsocket.DuplexConnection; +import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.exceptions.RejectedResumeException; -import io.rsocket.frame.ErrorFrameCodec; import io.rsocket.frame.ResumeFrameCodec; import io.rsocket.frame.ResumeOkFrameCodec; +import io.rsocket.keepalive.KeepAliveSupport; import java.time.Duration; -import java.util.function.Function; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import reactor.core.publisher.FluxProcessor; +import reactor.core.CoreSubscriber; import reactor.core.publisher.Mono; -import reactor.core.publisher.ReplayProcessor; +import reactor.core.publisher.Operators; -public class ServerRSocketSession implements RSocketSession { +public class ServerRSocketSession + implements RSocketSession, ResumeStateHolder, CoreSubscriber { private static final Logger logger = LoggerFactory.getLogger(ServerRSocketSession.class); - private final ResumableDuplexConnection resumableConnection; - /*used instead of EmitterProcessor because its autocancel=false capability had no expected effect*/ - private final FluxProcessor newConnections = - ReplayProcessor.create(0); - private final ByteBufAllocator allocator; - private final ByteBuf resumeToken; + final ResumableDuplexConnection resumableConnection; + final Duration resumeSessionDuration; + final ResumableFramesStore resumableFramesStore; + final String resumeToken; + final ByteBufAllocator allocator; + final boolean cleanupStoreOnKeepAlive; + + volatile Subscription s; + static final AtomicReferenceFieldUpdater S = + AtomicReferenceFieldUpdater.newUpdater(ServerRSocketSession.class, Subscription.class, "s"); + + KeepAliveSupport keepAliveSupport; public ServerRSocketSession( - DuplexConnection duplexConnection, - Duration resumeSessionDuration, - Duration resumeStreamTimeout, - Function resumeStoreFactory, ByteBuf resumeToken, + DuplexConnection initialDuplexConnection, + ResumableDuplexConnection resumableDuplexConnection, + Duration resumeSessionDuration, + ResumableFramesStore resumableFramesStore, boolean cleanupStoreOnKeepAlive) { - this.allocator = duplexConnection.alloc(); - this.resumeToken = resumeToken; - this.resumableConnection = - new ResumableDuplexConnection( - "server", - duplexConnection, - resumeStoreFactory.apply(resumeToken), - resumeStreamTimeout, - cleanupStoreOnKeepAlive); - - Mono timeout = - resumableConnection - .connectionErrors() - .flatMap( - err -> { - logger.debug("Starting session timeout due to error", err); - return newConnections - .next() - .doOnNext(c -> logger.debug("Connection after error: {}", c)) - .timeout(resumeSessionDuration); - }) - .then() - .cast(DuplexConnection.class); - - newConnections - .mergeWith(timeout) - .subscribe( - connection -> { - reconnect(connection); - logger.debug("Server ResumableConnection reconnected: {}", connection); - }, - err -> { - logger.debug("Server ResumableConnection reconnect timeout"); - resumableConnection.dispose(); - }); + this.resumeToken = resumeToken.toString(CharsetUtil.UTF_8); + this.allocator = initialDuplexConnection.alloc(); + this.resumeSessionDuration = resumeSessionDuration; + this.resumableFramesStore = resumableFramesStore; + this.cleanupStoreOnKeepAlive = cleanupStoreOnKeepAlive; + this.resumableConnection = resumableDuplexConnection; + + resumableDuplexConnection.onClose().doFinally(__ -> dispose()).subscribe(); + + observeDisconnection(initialDuplexConnection); } - @Override - public ServerRSocketSession continueWith(DuplexConnection connectionFactory) { - logger.debug("Server continued with connection: {}", connectionFactory); - newConnections.onNext(connectionFactory); - return this; + void observeDisconnection(DuplexConnection activeConnection) { + activeConnection.onClose().subscribe(null, e -> tryTimeoutSession(), () -> tryTimeoutSession()); } - @Override - public ServerRSocketSession resumeWith(ByteBuf resumeFrame) { - logger.debug("Resume FRAME received"); - long remotePos = remotePos(resumeFrame); - long remoteImpliedPos = remoteImpliedPos(resumeFrame); - resumeFrame.release(); + void tryTimeoutSession() { + keepAliveSupport.stop(); + Mono.delay(resumeSessionDuration).subscribe(this); + logger.debug("Connection is lost. Trying to timeout the active session[{}]", resumeToken); + } - resumableConnection.resume( - remotePos, + public synchronized Mono resumeWith( + ByteBuf resumeFrame, DuplexConnection nextDuplexConnection) { + long remotePos = ResumeFrameCodec.firstAvailableClientPos(resumeFrame); + long remoteImpliedPos = ResumeFrameCodec.lastReceivedServerPos(resumeFrame); + long impliedPosition = resumableFramesStore.frameImpliedPosition(); + long position = resumableFramesStore.framePosition(); + + logger.debug( + "Resume FRAME received. ClientResumeState{observedFramesPosition[{}], sentFramesPosition[{}]}, ServerResumeState{observedFramesPosition[{}], sentFramesPosition[{}]}", remoteImpliedPos, - pos -> - pos.flatMap(impliedPos -> sendFrame(ResumeOkFrameCodec.encode(allocator, impliedPos))) - .onErrorResume( - err -> - sendFrame(ErrorFrameCodec.encode(allocator, 0, errorFrameThrowable(err))) - .then(Mono.fromRunnable(resumableConnection::dispose)) - /*Resumption is impossible: no need to return control to ResumableConnection*/ - .then(Mono.never()))); - return this; + remotePos, + impliedPosition, + position); + + for (; ; ) { + final Subscription subscription = this.s; + + if (subscription == Operators.cancelledSubscription()) { + logger.debug("Session has already been expired. Terminating received connection"); + final RejectedResumeException rejectedResumeException = + new RejectedResumeException("resume_internal_error: Session Expired"); + nextDuplexConnection.sendErrorAndClose(rejectedResumeException); + return nextDuplexConnection.onClose(); + } + + if (S.compareAndSet(this, subscription, null)) { + subscription.cancel(); + break; + } + } + + if (remotePos <= impliedPosition && position <= remoteImpliedPos) { + try { + if (position != remoteImpliedPos) { + resumableFramesStore.releaseFrames(remoteImpliedPos); + } + nextDuplexConnection.sendFrame( + 0, ResumeOkFrameCodec.encode(allocator, resumableFramesStore.frameImpliedPosition())); + logger.debug("ResumeOK Frame has been sent"); + } catch (Throwable t) { + logger.debug("Exception occurred while releasing frames in the frameStore", t); + resumableConnection.dispose(); + nextDuplexConnection.sendErrorAndClose(new RejectedResumeException(t.getMessage(), t)); + return nextDuplexConnection.onClose(); + } + if (resumableConnection.connect(nextDuplexConnection)) { + observeDisconnection(nextDuplexConnection); + keepAliveSupport.start(); + logger.debug("Session[{}] has been resumed successfully", resumeToken); + } else { + logger.debug("Session has already been expired. Terminating received connection"); + final ConnectionErrorException connectionErrorException = + new ConnectionErrorException("resume_internal_error: Session Expired"); + nextDuplexConnection.sendErrorAndClose(connectionErrorException); + } + } else { + logger.debug( + "Mismatching remote and local state. Expected RemoteImpliedPosition[{}] to be greater or equal to the LocalPosition[{}] and RemotePosition[{}] to be less or equal to LocalImpliedPosition[{}]. Terminating received connection", + remoteImpliedPos, + position, + remotePos, + impliedPosition); + resumableConnection.dispose(); + final RejectedResumeException rejectedResumeException = + new RejectedResumeException( + String.format( + "resumption_pos=[ remote: { pos: %d, impliedPos: %d }, local: { pos: %d, impliedPos: %d }]", + remotePos, remoteImpliedPos, position, impliedPosition)); + nextDuplexConnection.sendErrorAndClose(rejectedResumeException); + } + + return nextDuplexConnection.onClose(); } @Override - public void reconnect(DuplexConnection connection) { - resumableConnection.reconnect(connection); + public long impliedPosition() { + return resumableFramesStore.frameImpliedPosition(); } @Override - public ResumableDuplexConnection resumableConnection() { - return resumableConnection; + public void onImpliedPosition(long remoteImpliedPos) { + if (cleanupStoreOnKeepAlive) { + resumableFramesStore.releaseFrames(remoteImpliedPos); + } } @Override - public ByteBuf token() { - return resumeToken; + public void onSubscribe(Subscription s) { + if (Operators.setOnce(S, this, s)) { + s.request(Long.MAX_VALUE); + } } - private Mono sendFrame(ByteBuf frame) { - logger.debug("Sending Resume frame: {}", frame); - return resumableConnection.sendOne(frame).onErrorResume(e -> Mono.empty()); - } + @Override + public void onNext(Long aLong) { + if (!Operators.terminate(S, this)) { + return; + } - private static long remotePos(ByteBuf resumeFrame) { - return ResumeFrameCodec.firstAvailableClientPos(resumeFrame); + resumableConnection.dispose(); } - private static long remoteImpliedPos(ByteBuf resumeFrame) { - return ResumeFrameCodec.lastReceivedServerPos(resumeFrame); + @Override + public void onComplete() {} + + @Override + public void onError(Throwable t) {} + + public void setKeepAliveSupport(KeepAliveSupport keepAliveSupport) { + this.keepAliveSupport = keepAliveSupport; } - private static RejectedResumeException errorFrameThrowable(Throwable err) { - String msg; - if (err instanceof ResumeStateException) { - ResumeStateException resumeException = ((ResumeStateException) err); - msg = - String.format( - "resumption_pos=[ remote: { pos: %d, impliedPos: %d }, local: { pos: %d, impliedPos: %d }]", - resumeException.getRemotePos(), - resumeException.getRemoteImpliedPos(), - resumeException.getLocalPos(), - resumeException.getLocalImpliedPos()); - } else { - msg = String.format("resume_internal_error: %s", err.getMessage()); + @Override + public void dispose() { + if (Operators.terminate(S, this)) { + resumableFramesStore.dispose(); + resumableConnection.dispose(); } - return new RejectedResumeException(msg); + } + + @Override + public boolean isDisposed() { + return resumableConnection.isDisposed(); } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java b/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java index 1d5c23bd6..736d7c77c 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java @@ -17,27 +17,36 @@ package io.rsocket.resume; import io.netty.buffer.ByteBuf; +import io.netty.util.CharsetUtil; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.util.annotation.Nullable; public class SessionManager { + static final Logger logger = LoggerFactory.getLogger(SessionManager.class); + private volatile boolean isDisposed; - private final Map sessions = new ConcurrentHashMap<>(); + private final Map sessions = new ConcurrentHashMap<>(); - public ServerRSocketSession save(ServerRSocketSession session) { + public ServerRSocketSession save(ServerRSocketSession session, ByteBuf resumeToken) { if (isDisposed) { session.dispose(); } else { - ByteBuf token = session.token().retain(); + final String token = resumeToken.toString(CharsetUtil.UTF_8); session + .resumableConnection .onClose() - .doOnSuccess( - v -> { + .doFinally( + __ -> { + logger.debug( + "ResumableConnection has been closed. Removing associated session {" + + token + + "}"); if (isDisposed || sessions.get(token) == session) { sessions.remove(token); } - token.release(); }) .subscribe(); ServerRSocketSession prevSession = sessions.remove(token); @@ -51,7 +60,7 @@ public ServerRSocketSession save(ServerRSocketSession session) { @Nullable public ServerRSocketSession get(ByteBuf resumeToken) { - return sessions.get(resumeToken); + return sessions.get(resumeToken.toString(CharsetUtil.UTF_8)); } public void dispose() { diff --git a/rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java b/rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java deleted file mode 100644 index f010a05bd..000000000 --- a/rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2015-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.resume; - -import io.netty.buffer.ByteBuf; -import java.util.Queue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.Disposable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Operators; -import reactor.util.concurrent.Queues; - -class UpstreamFramesSubscriber implements Subscriber, Disposable { - private static final Logger logger = LoggerFactory.getLogger(UpstreamFramesSubscriber.class); - - private final AtomicBoolean disposed = new AtomicBoolean(); - private final Consumer itemConsumer; - private final Disposable downstreamRequestDisposable; - private final Disposable resumeSaveStreamDisposable; - - private volatile Subscription subs; - private volatile boolean resumeStarted; - private final Queue framesCache; - private long request; - private long downStreamRequestN; - private long resumeSaveStreamRequestN; - - UpstreamFramesSubscriber( - int estimatedDownstreamRequest, - Flux downstreamRequests, - Flux resumeSaveStreamRequests, - Consumer itemConsumer) { - this.itemConsumer = itemConsumer; - this.framesCache = Queues.unbounded(estimatedDownstreamRequest).get(); - - downstreamRequestDisposable = downstreamRequests.subscribe(requestN -> requestN(0, requestN)); - - resumeSaveStreamDisposable = - resumeSaveStreamRequests.subscribe(requestN -> requestN(requestN, 0)); - } - - @Override - public void onSubscribe(Subscription s) { - this.subs = s; - if (!isDisposed()) { - doRequest(); - } else { - s.cancel(); - } - } - - @Override - public void onNext(ByteBuf item) { - processFrame(item); - } - - @Override - public void onError(Throwable t) { - dispose(); - } - - @Override - public void onComplete() { - dispose(); - } - - public void resumeStart() { - resumeStarted = true; - } - - public void resumeComplete() { - ByteBuf frame = framesCache.poll(); - while (frame != null) { - itemConsumer.accept(frame); - frame = framesCache.poll(); - } - resumeStarted = false; - doRequest(); - } - - @Override - public void dispose() { - if (disposed.compareAndSet(false, true)) { - releaseCache(); - if (subs != null) { - subs.cancel(); - } - resumeSaveStreamDisposable.dispose(); - downstreamRequestDisposable.dispose(); - } - } - - @Override - public boolean isDisposed() { - return disposed.get(); - } - - private void requestN(long resumeStreamRequest, long downStreamRequest) { - synchronized (this) { - downStreamRequestN = Operators.addCap(downStreamRequestN, downStreamRequest); - resumeSaveStreamRequestN = Operators.addCap(resumeSaveStreamRequestN, resumeStreamRequest); - - long requests = Math.min(downStreamRequestN, resumeSaveStreamRequestN); - if (requests > 0) { - downStreamRequestN -= requests; - resumeSaveStreamRequestN -= requests; - logger.debug("Upstream subscriber requestN: {}", requests); - request = Operators.addCap(request, requests); - } - } - doRequest(); - } - - private void doRequest() { - if (subs != null && !resumeStarted) { - synchronized (this) { - long r = request; - if (r > 0) { - subs.request(r); - request = 0; - } - } - } - } - - private void releaseCache() { - ByteBuf frame = framesCache.poll(); - while (frame != null && frame.refCnt() > 0) { - frame.release(); - } - } - - private void processFrame(ByteBuf item) { - if (resumeStarted) { - framesCache.offer(item); - } else { - itemConsumer.accept(item); - } - } -} 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 84b46ea69..9f431d0d4 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java +++ b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java @@ -73,7 +73,7 @@ public void setMaxFrameLength(int maxFrameLength) { protected abstract T newRSocket(); - public ByteBufAllocator alloc() { + public LeaksTrackingByteBufAllocator alloc() { return allocator; } diff --git a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java b/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java similarity index 54% rename from rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java rename to rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java index fb951eb8a..fdf312bce 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package io.rsocket.internal; +package io.rsocket.core; -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import io.netty.buffer.ByteBuf; @@ -27,16 +26,11 @@ import io.rsocket.frame.KeepAliveFrameCodec; import io.rsocket.frame.LeaseFrameCodec; import io.rsocket.frame.MetadataPushFrameCodec; -import io.rsocket.frame.ResumeFrameCodec; -import io.rsocket.frame.ResumeOkFrameCodec; -import io.rsocket.frame.SetupFrameCodec; import io.rsocket.plugins.InitializingInterceptorRegistry; import io.rsocket.test.util.TestDuplexConnection; -import io.rsocket.util.DefaultPayload; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class ClientServerInputMultiplexerTest { private TestDuplexConnection source; @@ -45,7 +39,7 @@ public class ClientServerInputMultiplexerTest { LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); private ClientServerInputMultiplexer serverMultiplexer; - @Before + @BeforeEach public void setup() { source = new TestDuplexConnection(allocator); clientMultiplexer = @@ -58,7 +52,6 @@ public void setup() { public void clientSplits() { AtomicInteger clientFrames = new AtomicInteger(); AtomicInteger serverFrames = new AtomicInteger(); - AtomicInteger setupFrames = new AtomicInteger(); clientMultiplexer .asClientConnection() @@ -70,68 +63,40 @@ public void clientSplits() { .receive() .doOnNext(f -> serverFrames.incrementAndGet()) .subscribe(); - clientMultiplexer - .asSetupConnection() - .receive() - .doOnNext(f -> setupFrames.incrementAndGet()) - .subscribe(); - - source.addToReceivedBuffer(setupFrame().retain()); - assertEquals(0, clientFrames.get()); - assertEquals(0, serverFrames.get()); - assertEquals(1, setupFrames.get()); source.addToReceivedBuffer(errorFrame(1).retain()); assertEquals(1, clientFrames.get()); assertEquals(0, serverFrames.get()); - assertEquals(1, setupFrames.get()); source.addToReceivedBuffer(errorFrame(1).retain()); assertEquals(2, clientFrames.get()); assertEquals(0, serverFrames.get()); - assertEquals(1, setupFrames.get()); source.addToReceivedBuffer(leaseFrame().retain()); assertEquals(3, clientFrames.get()); assertEquals(0, serverFrames.get()); - assertEquals(1, setupFrames.get()); source.addToReceivedBuffer(keepAliveFrame().retain()); assertEquals(4, clientFrames.get()); assertEquals(0, serverFrames.get()); - assertEquals(1, setupFrames.get()); source.addToReceivedBuffer(errorFrame(2).retain()); assertEquals(4, clientFrames.get()); assertEquals(1, serverFrames.get()); - assertEquals(1, setupFrames.get()); source.addToReceivedBuffer(errorFrame(0).retain()); assertEquals(5, clientFrames.get()); assertEquals(1, serverFrames.get()); - assertEquals(1, setupFrames.get()); source.addToReceivedBuffer(metadataPushFrame().retain()); assertEquals(5, clientFrames.get()); assertEquals(2, serverFrames.get()); - assertEquals(1, setupFrames.get()); - - source.addToReceivedBuffer(resumeFrame().retain()); - assertEquals(5, clientFrames.get()); - assertEquals(2, serverFrames.get()); - assertEquals(2, setupFrames.get()); - - source.addToReceivedBuffer(resumeOkFrame().retain()); - assertEquals(5, clientFrames.get()); - assertEquals(2, serverFrames.get()); - assertEquals(3, setupFrames.get()); } @Test public void serverSplits() { AtomicInteger clientFrames = new AtomicInteger(); AtomicInteger serverFrames = new AtomicInteger(); - AtomicInteger setupFrames = new AtomicInteger(); serverMultiplexer .asClientConnection() @@ -143,113 +108,34 @@ public void serverSplits() { .receive() .doOnNext(f -> serverFrames.incrementAndGet()) .subscribe(); - serverMultiplexer - .asSetupConnection() - .receive() - .doOnNext(f -> setupFrames.incrementAndGet()) - .subscribe(); - - source.addToReceivedBuffer(setupFrame().retain()); - assertEquals(0, clientFrames.get()); - assertEquals(0, serverFrames.get()); - assertEquals(1, setupFrames.get()); source.addToReceivedBuffer(errorFrame(1).retain()); assertEquals(1, clientFrames.get()); assertEquals(0, serverFrames.get()); - assertEquals(1, setupFrames.get()); source.addToReceivedBuffer(errorFrame(1).retain()); assertEquals(2, clientFrames.get()); assertEquals(0, serverFrames.get()); - assertEquals(1, setupFrames.get()); source.addToReceivedBuffer(leaseFrame().retain()); assertEquals(2, clientFrames.get()); assertEquals(1, serverFrames.get()); - assertEquals(1, setupFrames.get()); source.addToReceivedBuffer(keepAliveFrame().retain()); assertEquals(2, clientFrames.get()); assertEquals(2, serverFrames.get()); - assertEquals(1, setupFrames.get()); source.addToReceivedBuffer(errorFrame(2).retain()); assertEquals(2, clientFrames.get()); assertEquals(3, serverFrames.get()); - assertEquals(1, setupFrames.get()); source.addToReceivedBuffer(errorFrame(0).retain()); assertEquals(2, clientFrames.get()); assertEquals(4, serverFrames.get()); - assertEquals(1, setupFrames.get()); source.addToReceivedBuffer(metadataPushFrame().retain()); assertEquals(3, clientFrames.get()); assertEquals(4, serverFrames.get()); - assertEquals(1, setupFrames.get()); - - source.addToReceivedBuffer(resumeFrame().retain()); - assertEquals(3, clientFrames.get()); - assertEquals(4, serverFrames.get()); - assertEquals(2, setupFrames.get()); - - source.addToReceivedBuffer(resumeOkFrame().retain()); - assertEquals(3, clientFrames.get()); - assertEquals(4, serverFrames.get()); - assertEquals(3, setupFrames.get()); - } - - @Test - public void unexpectedFramesBeforeSetupFrame() { - AtomicInteger clientFrames = new AtomicInteger(); - AtomicInteger serverFrames = new AtomicInteger(); - AtomicInteger setupFrames = new AtomicInteger(); - - AtomicReference clientError = new AtomicReference<>(); - AtomicReference serverError = new AtomicReference<>(); - AtomicReference setupError = new AtomicReference<>(); - - serverMultiplexer - .asClientConnection() - .receive() - .subscribe(bb -> clientFrames.incrementAndGet(), clientError::set); - serverMultiplexer - .asServerConnection() - .receive() - .subscribe(bb -> serverFrames.incrementAndGet(), serverError::set); - serverMultiplexer - .asSetupConnection() - .receive() - .subscribe(bb -> setupFrames.incrementAndGet(), setupError::set); - - source.addToReceivedBuffer(keepAliveFrame().retain()); - - assertThat(clientError.get().getMessage()) - .isEqualTo("SETUP or LEASE frame must be received before any others."); - assertThat(serverError.get().getMessage()) - .isEqualTo("SETUP or LEASE frame must be received before any others."); - assertThat(setupError.get().getMessage()) - .isEqualTo("SETUP or LEASE frame must be received before any others."); - - assertEquals(0, clientFrames.get()); - assertEquals(0, serverFrames.get()); - assertEquals(0, setupFrames.get()); - } - - private ByteBuf resumeFrame() { - return ResumeFrameCodec.encode(allocator, Unpooled.EMPTY_BUFFER, 0, 0); - } - - private ByteBuf setupFrame() { - return SetupFrameCodec.encode( - ByteBufAllocator.DEFAULT, - false, - 0, - 42, - "application/octet-stream", - "application/octet-stream", - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER)); } private ByteBuf leaseFrame() { @@ -260,10 +146,6 @@ private ByteBuf errorFrame(int i) { return ErrorFrameCodec.encode(allocator, i, new Exception()); } - private ByteBuf resumeOkFrame() { - return ResumeOkFrameCodec.encode(allocator, 0); - } - private ByteBuf keepAliveFrame() { return KeepAliveFrameCodec.encode(allocator, false, 0, Unpooled.EMPTY_BUFFER); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/FireAndForgetRequesterMonoTest.java b/rsocket-core/src/test/java/io/rsocket/core/FireAndForgetRequesterMonoTest.java index cb5044e17..0857a2de8 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/FireAndForgetRequesterMonoTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/FireAndForgetRequesterMonoTest.java @@ -14,7 +14,7 @@ import io.rsocket.Payload; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.FrameType; -import io.rsocket.internal.UnboundedProcessor; +import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.ByteBufPayload; import java.time.Duration; import java.util.Arrays; @@ -63,7 +63,7 @@ public void frameShouldBeSentOnSubscription(Consumer stateAssert.isTerminated(); activeStreams.assertNoActiveStreams(); - final ByteBuf frame = activeStreams.getSendProcessor().poll(); + final ByteBuf frame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(frame) .isNotNull() .hasPayloadSize( @@ -77,7 +77,7 @@ public void frameShouldBeSentOnSubscription(Consumer .hasStreamId(1) .hasNoLeaks(); - Assertions.assertThat(activeStreams.getSendProcessor().isEmpty()).isTrue(); + Assertions.assertThat(activeStreams.getDuplexConnection().isEmpty()).isTrue(); activeStreams.getAllocator().assertHasNoLeaks(); } @@ -93,7 +93,7 @@ public void frameFragmentsShouldBeSentOnSubscription( final int mtu = 64; final TestRequesterResponderSupport streamManager = TestRequesterResponderSupport.client(mtu); final LeaksTrackingByteBufAllocator allocator = streamManager.getAllocator(); - final UnboundedProcessor sender = streamManager.getSendProcessor(); + final TestDuplexConnection sender = streamManager.getDuplexConnection(); final byte[] metadata = new byte[65]; final byte[] data = new byte[129]; @@ -118,7 +118,7 @@ public void frameFragmentsShouldBeSentOnSubscription( Assertions.assertThat(payload.refCnt()).isZero(); - final ByteBuf frameFragment1 = sender.poll(); + final ByteBuf frameFragment1 = sender.awaitFrame(); FrameAssert.assertThat(frameFragment1) .isNotNull() .hasPayloadSize( @@ -132,7 +132,7 @@ public void frameFragmentsShouldBeSentOnSubscription( .hasStreamId(1) .hasNoLeaks(); - final ByteBuf frameFragment2 = sender.poll(); + final ByteBuf frameFragment2 = sender.awaitFrame(); FrameAssert.assertThat(frameFragment2) .isNotNull() .hasPayloadSize( @@ -146,7 +146,7 @@ public void frameFragmentsShouldBeSentOnSubscription( .hasStreamId(1) .hasNoLeaks(); - final ByteBuf frameFragment3 = sender.poll(); + final ByteBuf frameFragment3 = sender.awaitFrame(); FrameAssert.assertThat(frameFragment3) .isNotNull() .hasPayloadSize( @@ -159,7 +159,7 @@ public void frameFragmentsShouldBeSentOnSubscription( .hasStreamId(1) .hasNoLeaks(); - final ByteBuf frameFragment4 = sender.poll(); + final ByteBuf frameFragment4 = sender.awaitFrame(); FrameAssert.assertThat(frameFragment4) .isNotNull() .hasPayloadSize(35) @@ -191,7 +191,7 @@ public void shouldErrorOnIncorrectRefCntInGivenPayload( Consumer monoConsumer) { final TestRequesterResponderSupport streamManager = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = streamManager.getAllocator(); - final UnboundedProcessor sender = streamManager.getSendProcessor(); + final TestDuplexConnection sender = streamManager.getDuplexConnection(); final Payload payload = ByteBufPayload.create(""); payload.release(); @@ -235,7 +235,7 @@ public void shouldErrorIfFragmentExitsAllowanceIfFragmentationDisabled( Consumer monoConsumer) { final TestRequesterResponderSupport streamManager = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = streamManager.getAllocator(); - final UnboundedProcessor sender = streamManager.getSendProcessor(); + final TestDuplexConnection sender = streamManager.getDuplexConnection(); final byte[] metadata = new byte[FRAME_LENGTH_MASK]; final byte[] data = new byte[FRAME_LENGTH_MASK]; @@ -292,7 +292,7 @@ public void shouldErrorIfNoAvailability(Consumer mon final TestRequesterResponderSupport streamManager = TestRequesterResponderSupport.client(new RuntimeException("test")); final LeaksTrackingByteBufAllocator allocator = streamManager.getAllocator(); - final UnboundedProcessor sender = streamManager.getSendProcessor(); + final TestDuplexConnection sender = streamManager.getDuplexConnection(); final Payload payload = genericPayload(allocator); final FireAndForgetRequesterMono fireAndForgetRequesterMono = @@ -335,7 +335,7 @@ static Stream> shouldErrorIfNoAvailabilityS public void shouldSubscribeExactlyOnce1() { final TestRequesterResponderSupport streamManager = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = streamManager.getAllocator(); - final UnboundedProcessor sender = streamManager.getSendProcessor(); + final TestDuplexConnection sender = streamManager.getDuplexConnection(); for (int i = 1; i < 50000; i += 2) { final Payload payload = ByteBufPayload.create("testData", "testMetadata"); @@ -364,7 +364,7 @@ public void shouldSubscribeExactlyOnce1() { return true; }); - final ByteBuf frame = sender.poll(); + final ByteBuf frame = sender.awaitFrame(); FrameAssert.assertThat(frame) .isNotNull() .hasPayloadSize( @@ -391,7 +391,6 @@ public void checkName() { final TestRequesterResponderSupport testRequesterResponderSupport = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = testRequesterResponderSupport.getAllocator(); - final UnboundedProcessor sender = testRequesterResponderSupport.getSendProcessor(); final Payload payload = ByteBufPayload.create("testData", "testMetadata"); final FireAndForgetRequesterMono fireAndForgetRequesterMono = 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 78f7bff66..a895fc5ad 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java @@ -1,371 +1,376 @@ -/* - * Copyright 2015-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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.lease.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.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.lease.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; +// } +// } +// } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java index 468a13505..422bf1f6b 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java @@ -3,24 +3,90 @@ import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCounted; import io.rsocket.ConnectionSetupPayload; +import io.rsocket.FrameAssert; import io.rsocket.Payload; import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.KeepAliveFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; import io.rsocket.test.util.TestClientTransport; +import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.ByteBufPayload; import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; import reactor.test.StepVerifier; +import reactor.util.retry.Retry; public class RSocketConnectorTest { + @ParameterizedTest + @ValueSource(strings = {"KEEPALIVE", "REQUEST_RESPONSE"}) + public void unexpectedFramesBeforeResumeOKFrame(String frameType) { + TestClientTransport transport = new TestClientTransport(); + RSocketConnector.create() + .resume(new Resume().retry(Retry.indefinitely())) + .connect(transport) + .block(); + + final TestDuplexConnection duplexConnection = transport.testConnection(); + + duplexConnection.addToReceivedBuffer( + KeepAliveFrameCodec.encode(duplexConnection.alloc(), false, 1, Unpooled.EMPTY_BUFFER)); + FrameAssert.assertThat(duplexConnection.pollFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + + FrameAssert.assertThat(duplexConnection.pollFrame()).isNull(); + + duplexConnection.dispose(); + + final TestDuplexConnection duplexConnection2 = transport.testConnection(); + + final ByteBuf frame; + switch (frameType) { + case "KEEPALIVE": + frame = + KeepAliveFrameCodec.encode(duplexConnection2.alloc(), false, 1, Unpooled.EMPTY_BUFFER); + break; + case "REQUEST_RESPONSE": + default: + frame = + RequestResponseFrameCodec.encode( + duplexConnection2.alloc(), 2, false, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); + } + duplexConnection2.addToReceivedBuffer(frame); + + StepVerifier.create(duplexConnection2.onClose()) + .expectSubscription() + .expectComplete() + .verify(Duration.ofSeconds(10)); + + FrameAssert.assertThat(duplexConnection2.pollFrame()) + .typeOf(FrameType.RESUME) + .hasStreamIdZero() + .hasNoLeaks(); + + FrameAssert.assertThat(duplexConnection2.pollFrame()) + .isNotNull() + .typeOf(FrameType.ERROR) + .hasData("RESUME_OK frame must be received before any others") + .hasStreamIdZero() + .hasNoLeaks(); + } + @Test public void ensuresThatSetupPayloadCanBeRetained() { MonoProcessor retainedSetupPayload = MonoProcessor.create(); @@ -86,6 +152,16 @@ public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions .expectComplete() .verify(Duration.ofMillis(100)); + Assertions.assertThat(testClientTransport.testConnection().getSent()) + .hasSize(1) + .allMatch( + bb -> { + DefaultConnectionSetupPayload payload = new DefaultConnectionSetupPayload(bb); + return payload.getDataUtf8().equals("TestData") + && payload.getMetadataUtf8().equals("TestMetadata"); + }) + .allMatch(ReferenceCounted::release); + connectionMono .as(StepVerifier::create) .expectNextCount(1) @@ -93,7 +169,7 @@ public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions .verify(Duration.ofMillis(100)); Assertions.assertThat(testClientTransport.testConnection().getSent()) - .hasSize(2) + .hasSize(1) .allMatch( bb -> { DefaultConnectionSetupPayload payload = new DefaultConnectionSetupPayload(bb); @@ -107,10 +183,13 @@ public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions @Test public void ensuresThatSetupPayloadProvidedAsMonoIsReleased() { List saved = new ArrayList<>(); + AtomicLong subscriptions = new AtomicLong(); Mono setupPayloadMono = Mono.create( sink -> { - Payload payload = ByteBufPayload.create("TestData", "TestMetadata"); + final long subscriptionN = subscriptions.getAndIncrement(); + Payload payload = + ByteBufPayload.create("TestData" + subscriptionN, "TestMetadata" + subscriptionN); saved.add(payload); sink.success(payload); }); @@ -125,6 +204,16 @@ public void ensuresThatSetupPayloadProvidedAsMonoIsReleased() { .expectComplete() .verify(Duration.ofMillis(100)); + Assertions.assertThat(testClientTransport.testConnection().getSent()) + .hasSize(1) + .allMatch( + bb -> { + DefaultConnectionSetupPayload payload = new DefaultConnectionSetupPayload(bb); + return payload.getDataUtf8().equals("TestData0") + && payload.getMetadataUtf8().equals("TestMetadata0"); + }) + .allMatch(ReferenceCounted::release); + connectionMono .as(StepVerifier::create) .expectNextCount(1) @@ -132,12 +221,12 @@ public void ensuresThatSetupPayloadProvidedAsMonoIsReleased() { .verify(Duration.ofMillis(100)); Assertions.assertThat(testClientTransport.testConnection().getSent()) - .hasSize(2) + .hasSize(1) .allMatch( bb -> { DefaultConnectionSetupPayload payload = new DefaultConnectionSetupPayload(bb); - return payload.getDataUtf8().equals("TestData") - && payload.getMetadataUtf8().equals("TestMetadata"); + return payload.getDataUtf8().equals("TestData1") + && payload.getMetadataUtf8().equals("TestMetadata1"); }) .allMatch(ReferenceCounted::release); 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 32bae9270..ae1282c1e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -39,7 +39,6 @@ import io.rsocket.frame.PayloadFrameCodec; import io.rsocket.frame.SetupFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.ClientServerInputMultiplexer; import io.rsocket.internal.subscriber.AssertSubscriber; import io.rsocket.lease.*; import io.rsocket.lease.MissingLeaseException; 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 de7e48d64..0d0b0f093 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -132,7 +132,7 @@ public void tearDown() { public void testHandleKeepAlive() throws Exception { rule.connection.addToReceivedBuffer( KeepAliveFrameCodec.encode(rule.alloc(), true, 0, Unpooled.EMPTY_BUFFER)); - ByteBuf sent = rule.connection.awaitSend(); + ByteBuf sent = rule.connection.awaitFrame(); assertThat("Unexpected frame sent.", frameType(sent), is(FrameType.KEEPALIVE)); /*Keep alive ack must not have respond flag else, it will result in infinite ping-pong of keep alive frames.*/ assertThat( @@ -158,7 +158,7 @@ public Mono requestResponse(Payload payload) { testPublisher.complete(); assertThat( "Unexpected frame sent.", - frameType(rule.connection.awaitSend()), + frameType(rule.connection.awaitFrame()), anyOf(is(FrameType.COMPLETE), is(FrameType.NEXT_COMPLETE))); testPublisher.assertWasNotCancelled(); } @@ -170,7 +170,7 @@ public void testHandlerEmitsError() throws Exception { final int streamId = 4; rule.sendRequest(streamId, FrameType.REQUEST_STREAM); assertThat( - "Unexpected frame sent.", frameType(rule.connection.awaitSend()), is(FrameType.ERROR)); + "Unexpected frame sent.", frameType(rule.connection.awaitFrame()), is(FrameType.ERROR)); } @Test 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 a6103a2ba..fc3da93dd 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java @@ -5,7 +5,10 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.rsocket.FrameAssert; import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.KeepAliveFrameCodec; import io.rsocket.frame.RequestResponseFrameCodec; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestServerTransport; @@ -18,6 +21,29 @@ public class RSocketServerTest { + @Test + public void unexpectedFramesBeforeSetupFrame() { + TestServerTransport transport = new TestServerTransport(); + RSocketServer.create().bind(transport).block(); + + final TestDuplexConnection duplexConnection = transport.connect(); + + duplexConnection.addToReceivedBuffer( + KeepAliveFrameCodec.encode(duplexConnection.alloc(), false, 1, Unpooled.EMPTY_BUFFER)); + + StepVerifier.create(duplexConnection.onClose()) + .expectSubscription() + .expectComplete() + .verify(Duration.ofSeconds(10)); + + FrameAssert.assertThat(duplexConnection.pollFrame()) + .isNotNull() + .typeOf(FrameType.ERROR) + .hasData("SETUP or RESUME frame must be received before any others") + .hasStreamIdZero() + .hasNoLeaks(); + } + @Test public void ensuresMaxFrameLengthCanNotBeLessThenMtu() { RSocketServer.create() diff --git a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelRequesterFluxTest.java b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelRequesterFluxTest.java index 4fc06fdc2..b9d63cf5e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelRequesterFluxTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelRequesterFluxTest.java @@ -27,8 +27,8 @@ import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.frame.FrameType; -import io.rsocket.internal.UnboundedProcessor; import io.rsocket.internal.subscriber.AssertSubscriber; +import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.ByteBufPayload; import io.rsocket.util.DefaultPayload; import java.time.Duration; @@ -66,7 +66,7 @@ public static void setUp() { public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String completionCase) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final Payload payload = TestRequesterResponderSupport.genericPayload(allocator); final TestPublisher publisher = TestPublisher.create(); @@ -103,7 +103,7 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String comp // state machine check stateAssert.hasSubscribedFlag().hasRequestN(10).hasFirstFrameSentFlag(); - final ByteBuf frame = sender.poll(); + final ByteBuf frame = sender.awaitFrame(); FrameAssert.assertThat(frame) .isNotNull() .hasPayloadSize( @@ -121,7 +121,7 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String comp Assertions.assertThat(sender.isEmpty()).isTrue(); assertSubscriber.request(1); - final ByteBuf requestNFrame = sender.poll(); + final ByteBuf requestNFrame = sender.awaitFrame(); FrameAssert.assertThat(requestNFrame) .isNotNull() .hasRequestN(1) @@ -137,7 +137,7 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String comp stateAssert.hasSubscribedFlag().hasRequestN(11).hasFirstFrameSentFlag(); assertSubscriber.request(Long.MAX_VALUE); - final ByteBuf requestMaxNFrame = sender.poll(); + final ByteBuf requestMaxNFrame = sender.awaitFrame(); FrameAssert.assertThat(requestMaxNFrame) .isNotNull() .hasRequestN(Integer.MAX_VALUE) @@ -211,10 +211,10 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String comp .hasInboundTerminated(); publisher.complete(); - FrameAssert.assertThat(sender.poll()).typeOf(FrameType.COMPLETE).hasNoLeaks(); + FrameAssert.assertThat(sender.awaitFrame()).typeOf(FrameType.COMPLETE).hasNoLeaks(); } else if (completionCase.equals("outbound")) { publisher.complete(); - FrameAssert.assertThat(sender.poll()).typeOf(FrameType.COMPLETE).hasNoLeaks(); + FrameAssert.assertThat(sender.awaitFrame()).typeOf(FrameType.COMPLETE).hasNoLeaks(); // state machine check stateAssert @@ -247,7 +247,7 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String comp public void streamShouldErrorWithoutInitializingRemoteStreamIfSourceIsEmpty(boolean doRequest) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final TestPublisher publisher = TestPublisher.create(); final RequestChannelRequesterFlux requestChannelRequesterFlux = @@ -292,7 +292,7 @@ public void streamShouldPropagateErrorWithoutInitializingRemoteStreamIfTheFirstS boolean doRequest) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final TestPublisher publisher = TestPublisher.create(); final RequestChannelRequesterFlux requestChannelRequesterFlux = @@ -336,7 +336,7 @@ public void streamShouldPropagateErrorWithoutInitializingRemoteStreamIfTheFirstS public void streamShouldBeInHalfClosedStateOnTheInboundCancellation(String terminationMode) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final TestPublisher publisher = TestPublisher.create(); final RequestChannelRequesterFlux requestChannelRequesterFlux = @@ -366,7 +366,7 @@ public void streamShouldBeInHalfClosedStateOnTheInboundCancellation(String termi publisher.next(payload1.retain()); - FrameAssert.assertThat(sender.poll()) + FrameAssert.assertThat(sender.awaitFrame()) .typeOf(FrameType.REQUEST_CHANNEL) .hasPayload(payload1) .hasRequestN(Integer.MAX_VALUE) @@ -386,10 +386,16 @@ public void streamShouldBeInHalfClosedStateOnTheInboundCancellation(String termi publisher.next(payload2.retain(), payload3.retain()); - FrameAssert.assertThat(sender.poll()).typeOf(FrameType.NEXT).hasPayload(payload2).hasNoLeaks(); + FrameAssert.assertThat(sender.awaitFrame()) + .typeOf(FrameType.NEXT) + .hasPayload(payload2) + .hasNoLeaks(); payload2.release(); - FrameAssert.assertThat(sender.poll()).typeOf(FrameType.NEXT).hasPayload(payload3).hasNoLeaks(); + FrameAssert.assertThat(sender.awaitFrame()) + .typeOf(FrameType.NEXT) + .hasPayload(payload3) + .hasNoLeaks(); payload3.release(); if (terminationMode.equals("outbound")) { @@ -428,7 +434,7 @@ public void streamShouldBeInHalfClosedStateOnTheInboundCancellation(String termi public void errorShouldTerminateExecution(String terminationMode) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final TestPublisher publisher = TestPublisher.create(); final RequestChannelRequesterFlux requestChannelRequesterFlux = @@ -458,7 +464,7 @@ public void errorShouldTerminateExecution(String terminationMode) { publisher.next(payload1.retain()); - FrameAssert.assertThat(sender.poll()) + FrameAssert.assertThat(sender.awaitFrame()) .typeOf(FrameType.REQUEST_CHANNEL) .hasPayload(payload1) .hasRequestN(Integer.MAX_VALUE) @@ -478,15 +484,24 @@ public void errorShouldTerminateExecution(String terminationMode) { publisher.next(payload2.retain(), payload3.retain()); - FrameAssert.assertThat(sender.poll()).typeOf(FrameType.NEXT).hasPayload(payload2).hasNoLeaks(); + FrameAssert.assertThat(sender.awaitFrame()) + .typeOf(FrameType.NEXT) + .hasPayload(payload2) + .hasNoLeaks(); payload2.release(); - FrameAssert.assertThat(sender.poll()).typeOf(FrameType.NEXT).hasPayload(payload3).hasNoLeaks(); + FrameAssert.assertThat(sender.awaitFrame()) + .typeOf(FrameType.NEXT) + .hasPayload(payload3) + .hasNoLeaks(); payload3.release(); if (terminationMode.equals("outbound")) { publisher.error(new ApplicationErrorException("test")); - FrameAssert.assertThat(sender.poll()).typeOf(FrameType.ERROR).hasData("test").hasNoLeaks(); + FrameAssert.assertThat(sender.awaitFrame()) + .typeOf(FrameType.ERROR) + .hasData("test") + .hasNoLeaks(); } else if (terminationMode.equals("inbound")) { requestChannelRequesterFlux.handleError(new ApplicationErrorException("test")); publisher.assertWasCancelled(); @@ -533,7 +548,7 @@ public void shouldHaveEventsDeliveredSeriallyWhenOutboundErrorRacingWithInboundS for (int i = 0; i < 10000; i++) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final TestPublisher publisher = TestPublisher.createNoncompliant(TestPublisher.Violation.DEFER_CANCELLATION); @@ -561,7 +576,7 @@ public void shouldHaveEventsDeliveredSeriallyWhenOutboundErrorRacingWithInboundS stateAssert.hasSubscribedFlag().hasRequestN(Integer.MAX_VALUE).hasFirstFrameSentFlag(); activeStreams.assertHasStream(1, requestChannelRequesterFlux); - FrameAssert.assertThat(sender.poll()) + FrameAssert.assertThat(sender.awaitFrame()) .typeOf(FrameType.REQUEST_CHANNEL) .hasRequestN(Integer.MAX_VALUE) .hasNoLeaks(); @@ -599,7 +614,7 @@ public void shouldHaveEventsDeliveredSeriallyWhenOutboundErrorRacingWithInboundS } }); - ByteBuf errorFrameOrEmpty = sender.poll(); + ByteBuf errorFrameOrEmpty = sender.pollFrame(); if (errorFrameOrEmpty != null) { if (outboundTerminationMode.equals("onError")) { FrameAssert.assertThat(errorFrameOrEmpty) @@ -694,7 +709,7 @@ public void shouldRemoveItselfFromActiveStreamsWhenInboundAndOutboundAreTerminat for (int i = 0; i < 10000; i++) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final TestPublisher publisher = TestPublisher.createNoncompliant(TestPublisher.Violation.DEFER_CANCELLATION); @@ -723,7 +738,7 @@ public void shouldRemoveItselfFromActiveStreamsWhenInboundAndOutboundAreTerminat stateAssert.hasSubscribedFlag().hasRequestN(Integer.MAX_VALUE).hasFirstFrameSentFlag(); activeStreams.assertHasStream(1, requestChannelRequesterFlux); - FrameAssert.assertThat(sender.poll()) + FrameAssert.assertThat(sender.awaitFrame()) .typeOf(FrameType.REQUEST_CHANNEL) .hasRequestN(Integer.MAX_VALUE) .hasNoLeaks(); @@ -740,7 +755,7 @@ public void shouldRemoveItselfFromActiveStreamsWhenInboundAndOutboundAreTerminat }, requestChannelRequesterFlux::handleComplete); - ByteBuf completeFrameOrNull = sender.poll(); + ByteBuf completeFrameOrNull = sender.pollFrame(); if (completeFrameOrNull != null) { FrameAssert.assertThat(completeFrameOrNull) .hasStreamId(1) diff --git a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java index b1c1e8cf9..4b4311a00 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java @@ -31,8 +31,8 @@ import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.frame.FrameType; -import io.rsocket.internal.UnboundedProcessor; import io.rsocket.internal.subscriber.AssertSubscriber; +import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.ByteBufPayload; import io.rsocket.util.DefaultPayload; import java.time.Duration; @@ -73,7 +73,7 @@ public static void setUp() { public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String completionCase) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final Payload firstPayload = TestRequesterResponderSupport.genericPayload(allocator); final TestPublisher publisher = TestPublisher.create(); @@ -112,7 +112,7 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String comp stateAssert.hasSubscribedFlag().hasRequestN(2).hasFirstFrameSentFlag(); // should not send requestN since 1 is remaining - FrameAssert.assertThat(sender.poll()) + FrameAssert.assertThat(sender.awaitFrame()) .typeOf(REQUEST_N) .hasStreamId(1) .hasRequestN(1) @@ -120,7 +120,7 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String comp publisher.next(TestRequesterResponderSupport.genericPayload(allocator)); - final ByteBuf frame = sender.poll(); + final ByteBuf frame = sender.awaitFrame(); FrameAssert.assertThat(frame) .isNotNull() .hasPayloadSize( @@ -135,7 +135,7 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String comp .hasNoLeaks(); assertSubscriber.request(Long.MAX_VALUE); - final ByteBuf requestMaxNFrame = sender.poll(); + final ByteBuf requestMaxNFrame = sender.awaitFrame(); FrameAssert.assertThat(requestMaxNFrame) .isNotNull() .hasRequestN(Integer.MAX_VALUE) @@ -204,7 +204,7 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String comp .hasInboundTerminated(); publisher.complete(); - FrameAssert.assertThat(sender.poll()).typeOf(FrameType.COMPLETE).hasNoLeaks(); + FrameAssert.assertThat(sender.awaitFrame()).typeOf(FrameType.COMPLETE).hasNoLeaks(); } else if (completionCase.equals("inboundCancel")) { assertSubscriber.cancel(); assertSubscriber.assertValuesWith( @@ -215,7 +215,7 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String comp randomPayload.release(); }); - FrameAssert.assertThat(sender.poll()).typeOf(CANCEL).hasStreamId(1).hasNoLeaks(); + FrameAssert.assertThat(sender.awaitFrame()).typeOf(CANCEL).hasStreamId(1).hasNoLeaks(); // state machine check stateAssert @@ -226,10 +226,13 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String comp .hasInboundTerminated(); publisher.complete(); - FrameAssert.assertThat(sender.poll()).typeOf(FrameType.COMPLETE).hasStreamId(1).hasNoLeaks(); + FrameAssert.assertThat(sender.awaitFrame()) + .typeOf(FrameType.COMPLETE) + .hasStreamId(1) + .hasNoLeaks(); } else if (completionCase.equals("outbound")) { publisher.complete(); - FrameAssert.assertThat(sender.poll()).typeOf(FrameType.COMPLETE).hasNoLeaks(); + FrameAssert.assertThat(sender.awaitFrame()).typeOf(FrameType.COMPLETE).hasNoLeaks(); // state machine check stateAssert @@ -270,7 +273,8 @@ public void streamShouldWorkCorrectlyWhenRacingHandleCompleteWithSubscription() for (int i = 0; i < 10000; i++) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + ; final Payload firstPayload = TestRequesterResponderSupport.randomPayload(allocator); final TestPublisher publisher = TestPublisher.create(); @@ -308,14 +312,14 @@ public void streamShouldWorkCorrectlyWhenRacingHandleCompleteWithSubscription() publisher.complete(); - if (sender.size() > 1) { - FrameAssert.assertThat(sender.poll()) + if (sender.getSent().size() > 1) { + FrameAssert.assertThat(sender.awaitFrame()) .hasStreamId(1) .typeOf(REQUEST_N) .hasRequestN(1) .hasNoLeaks(); } - FrameAssert.assertThat(sender.poll()).hasStreamId(1).typeOf(COMPLETE).hasNoLeaks(); + FrameAssert.assertThat(sender.awaitFrame()).hasStreamId(1).typeOf(COMPLETE).hasNoLeaks(); // state machine check stateAssert.isTerminated(); @@ -442,7 +446,7 @@ public void shouldHaveEventsDeliveredSeriallyWhenOutboundErrorRacingWithInboundS for (int i = 0; i < 10000; i++) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final TestPublisher publisher = TestPublisher.createNoncompliant(DEFER_CANCELLATION, CLEANUP_ON_TERMINATE); @@ -460,7 +464,7 @@ public void shouldHaveEventsDeliveredSeriallyWhenOutboundErrorRacingWithInboundS assertSubscriber.request(Integer.MAX_VALUE); - FrameAssert.assertThat(sender.poll()) + FrameAssert.assertThat(sender.awaitFrame()) .typeOf(FrameType.REQUEST_N) .hasRequestN(Integer.MAX_VALUE) .hasNoLeaks(); @@ -498,7 +502,7 @@ public void shouldHaveEventsDeliveredSeriallyWhenOutboundErrorRacingWithInboundS } }); - ByteBuf errorFrameOrEmpty = sender.poll(); + ByteBuf errorFrameOrEmpty = sender.pollFrame(); if (errorFrameOrEmpty != null) { String message; if (outboundTerminationMode.equals("onError")) { @@ -605,7 +609,8 @@ public void shouldHaveNoLeaksOnReassemblyAndCancelRacing(String terminationMode) for (int i = 0; i < 10000; i++) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + ; final TestPublisher publisher = TestPublisher.createNoncompliant(DEFER_CANCELLATION, CLEANUP_ON_TERMINATE); final AssertSubscriber assertSubscriber = new AssertSubscriber<>(1); @@ -669,7 +674,7 @@ public void shouldHaveNoLeaksOnReassemblyAndCancelRacing(String terminationMode) assertSubscriber.assertTerminated().assertError(); } - final ByteBuf frame = sender.poll(); + final ByteBuf frame = sender.awaitFrame(); FrameAssert.assertThat(frame) .isNotNull() .typeOf(terminationMode.equals("cancel") ? CANCEL : ERROR) diff --git a/rsocket-core/src/test/java/io/rsocket/core/RequestResponseRequesterMonoTest.java b/rsocket-core/src/test/java/io/rsocket/core/RequestResponseRequesterMonoTest.java index 86babe671..b39ac62d9 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequestResponseRequesterMonoTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequestResponseRequesterMonoTest.java @@ -31,7 +31,7 @@ import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.frame.FrameType; -import io.rsocket.internal.UnboundedProcessor; +import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.ByteBufPayload; import io.rsocket.util.EmptyPayload; import java.time.Duration; @@ -75,7 +75,7 @@ public void frameShouldBeSentOnSubscription( transformer) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final Payload payload = genericPayload(allocator); final RequestResponseRequesterMono requestResponseRequesterMono = @@ -105,7 +105,7 @@ public void frameShouldBeSentOnSubscription( // should not add anything to map activeStreams.assertNoActiveStreams(); - final ByteBuf frame = sender.poll(); + final ByteBuf frame = sender.awaitFrame(); FrameAssert.assertThat(frame) .isNotNull() .hasPayloadSize( @@ -122,7 +122,7 @@ public void frameShouldBeSentOnSubscription( stateAssert.isTerminated(); if (!sender.isEmpty()) { - ByteBuf cancelFrame = sender.poll(); + ByteBuf cancelFrame = sender.awaitFrame(); FrameAssert.assertThat(cancelFrame) .isNotNull() .typeOf(FrameType.CANCEL) @@ -320,7 +320,7 @@ public void frameFragmentsShouldBeSentOnSubscription( final int mtu = 64; final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(mtu); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final byte[] metadata = new byte[65]; final byte[] data = new byte[129]; @@ -356,7 +356,7 @@ public void frameFragmentsShouldBeSentOnSubscription( Assertions.assertThat(payload.refCnt()).isZero(); - final ByteBuf frameFragment1 = sender.poll(); + final ByteBuf frameFragment1 = sender.awaitFrame(); FrameAssert.assertThat(frameFragment1) .isNotNull() .hasPayloadSize( @@ -370,7 +370,7 @@ public void frameFragmentsShouldBeSentOnSubscription( .hasStreamId(1) .hasNoLeaks(); - final ByteBuf frameFragment2 = sender.poll(); + final ByteBuf frameFragment2 = sender.awaitFrame(); FrameAssert.assertThat(frameFragment2) .isNotNull() .hasPayloadSize( @@ -384,7 +384,7 @@ public void frameFragmentsShouldBeSentOnSubscription( .hasStreamId(1) .hasNoLeaks(); - final ByteBuf frameFragment3 = sender.poll(); + final ByteBuf frameFragment3 = sender.awaitFrame(); FrameAssert.assertThat(frameFragment3) .isNotNull() .hasPayloadSize( @@ -397,7 +397,7 @@ public void frameFragmentsShouldBeSentOnSubscription( .hasStreamId(1) .hasNoLeaks(); - final ByteBuf frameFragment4 = sender.poll(); + final ByteBuf frameFragment4 = sender.awaitFrame(); FrameAssert.assertThat(frameFragment4) .isNotNull() .hasPayloadSize(35) @@ -410,14 +410,14 @@ public void frameFragmentsShouldBeSentOnSubscription( .hasNoLeaks(); if (!sender.isEmpty()) { - FrameAssert.assertThat(sender.poll()) + FrameAssert.assertThat(sender.awaitFrame()) .isNotNull() .typeOf(FrameType.CANCEL) .hasClientSideStreamId() .hasStreamId(1) .hasNoLeaks(); } - Assertions.assertThat(sender).isEmpty(); + Assertions.assertThat(sender.isEmpty()).isTrue(); stateAssert.isTerminated(); allocator.assertHasNoLeaks(); } @@ -430,7 +430,7 @@ public void frameFragmentsShouldBeSentOnSubscription( public void shouldBeNoOpsOnCancel() { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final Payload payload = ByteBufPayload.create("testData", "testMetadata"); final RequestResponseRequesterMono requestResponseRequesterMono = @@ -466,7 +466,8 @@ public void shouldErrorOnIncorrectRefCntInGivenPayload( Consumer monoConsumer) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + ; final Payload payload = ByteBufPayload.create(""); payload.release(); @@ -483,7 +484,7 @@ public void shouldErrorOnIncorrectRefCntInGivenPayload( stateAssert.isTerminated(); activeStreams.assertNoActiveStreams(); - Assertions.assertThat(sender).isEmpty(); + Assertions.assertThat(sender.isEmpty()).isTrue(); allocator.assertHasNoLeaks(); } @@ -509,7 +510,8 @@ public void shouldErrorOnIncorrectRefCntInGivenPayload( public void shouldErrorOnIncorrectRefCntInGivenPayloadLatePhase() { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + ; final Payload payload = ByteBufPayload.create(""); final RequestResponseRequesterMono requestResponseRequesterMono = @@ -543,7 +545,8 @@ public void shouldErrorOnIncorrectRefCntInGivenPayloadLatePhaseWithFragmentation final int mtu = 64; final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(mtu); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + ; final byte[] metadata = new byte[65]; final byte[] data = new byte[129]; ThreadLocalRandom.current().nextBytes(metadata); @@ -582,7 +585,8 @@ public void shouldErrorIfFragmentExitsAllowanceIfFragmentationDisabled( Consumer monoConsumer) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + ; final byte[] metadata = new byte[FRAME_LENGTH_MASK]; final byte[] data = new byte[FRAME_LENGTH_MASK]; @@ -604,7 +608,7 @@ public void shouldErrorIfFragmentExitsAllowanceIfFragmentationDisabled( Assertions.assertThat(payload.refCnt()).isZero(); activeStreams.assertNoActiveStreams(); - Assertions.assertThat(sender).isEmpty(); + Assertions.assertThat(sender.isEmpty()).isTrue(); stateAssert.isTerminated(); allocator.assertHasNoLeaks(); } @@ -639,7 +643,6 @@ public void shouldErrorIfNoAvailability(Consumer m final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(new RuntimeException("test")); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); final Payload payload = genericPayload(allocator); final RequestResponseRequesterMono requestResponseRequesterMono = diff --git a/rsocket-core/src/test/java/io/rsocket/core/RequestStreamRequesterFluxTest.java b/rsocket-core/src/test/java/io/rsocket/core/RequestStreamRequesterFluxTest.java index 9791b0786..88dd5441e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequestStreamRequesterFluxTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequestStreamRequesterFluxTest.java @@ -31,8 +31,8 @@ import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.frame.FrameType; -import io.rsocket.internal.UnboundedProcessor; import io.rsocket.internal.subscriber.AssertSubscriber; +import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.ByteBufPayload; import io.rsocket.util.EmptyPayload; import java.time.Duration; @@ -80,7 +80,7 @@ public static void setUp() { public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately() { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final Payload payload = TestRequesterResponderSupport.genericPayload(allocator); final RequestStreamRequesterFlux requestStreamRequesterFlux = @@ -108,7 +108,7 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately() { // state machine check stateAssert.hasSubscribedFlag().hasRequestN(1).hasFirstFrameSentFlag(); - final ByteBuf frame = sender.poll(); + final ByteBuf frame = sender.awaitFrame(); FrameAssert.assertThat(frame) .isNotNull() .hasPayloadSize( @@ -126,7 +126,7 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately() { Assertions.assertThat(sender.isEmpty()).isTrue(); assertSubscriber.request(1); - final ByteBuf requestNFrame = sender.poll(); + final ByteBuf requestNFrame = sender.awaitFrame(); FrameAssert.assertThat(requestNFrame) .isNotNull() .hasRequestN(1) @@ -142,7 +142,7 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately() { stateAssert.hasSubscribedFlag().hasRequestN(2).hasFirstFrameSentFlag(); assertSubscriber.request(Long.MAX_VALUE); - final ByteBuf requestMaxNFrame = sender.poll(); + final ByteBuf requestMaxNFrame = sender.awaitFrame(); FrameAssert.assertThat(requestMaxNFrame) .isNotNull() .hasRequestN(Integer.MAX_VALUE) @@ -227,7 +227,7 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately() { public void requestNFrameShouldBeSentExactlyOnceIfItIsMaxAllowed() { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final Payload payload = TestRequesterResponderSupport.genericPayload(allocator); final RequestStreamRequesterFlux requestStreamRequesterFlux = @@ -257,7 +257,7 @@ public void requestNFrameShouldBeSentExactlyOnceIfItIsMaxAllowed() { Assertions.assertThat(payload.refCnt()).isZero(); activeStreams.assertHasStream(1, requestStreamRequesterFlux); - final ByteBuf frame = sender.poll(); + final ByteBuf frame = sender.awaitFrame(); FrameAssert.assertThat(frame) .isNotNull() .hasPayloadSize( @@ -332,7 +332,7 @@ public void frameShouldBeSentOnFirstRequest( transformer) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final Payload payload = TestRequesterResponderSupport.genericPayload(allocator); final RequestStreamRequesterFlux requestStreamRequesterFlux = @@ -369,7 +369,7 @@ public void frameShouldBeSentOnFirstRequest( // should not add anything to map activeStreams.assertNoActiveStreams(); - final ByteBuf frame = sender.poll(); + final ByteBuf frame = sender.awaitFrame(); FrameAssert.assertThat(frame) .isNotNull() .hasPayloadSize( @@ -384,7 +384,7 @@ public void frameShouldBeSentOnFirstRequest( .hasStreamId(1) .hasNoLeaks(); - final ByteBuf requestNFrame = sender.poll(); + final ByteBuf requestNFrame = sender.awaitFrame(); FrameAssert.assertThat(requestNFrame) .isNotNull() .typeOf(FrameType.REQUEST_N) @@ -394,7 +394,7 @@ public void frameShouldBeSentOnFirstRequest( .hasNoLeaks(); if (!sender.isEmpty()) { - final ByteBuf cancelFrame = sender.poll(); + final ByteBuf cancelFrame = sender.awaitFrame(); FrameAssert.assertThat(cancelFrame) .isNotNull() .typeOf(FrameType.CANCEL) @@ -764,7 +764,7 @@ public void frameFragmentsShouldBeSentOnFirstRequest( final int mtu = 64; final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(mtu); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final byte[] metadata = new byte[65]; final byte[] data = new byte[129]; @@ -799,7 +799,7 @@ public void frameFragmentsShouldBeSentOnFirstRequest( Assertions.assertThat(payload.refCnt()).isZero(); - final ByteBuf frameFragment1 = sender.poll(); + final ByteBuf frameFragment1 = sender.awaitFrame(); FrameAssert.assertThat(frameFragment1) .isNotNull() .hasPayloadSize(64 - FRAME_OFFSET_WITH_METADATA_AND_INITIAL_REQUEST_N) @@ -812,7 +812,7 @@ public void frameFragmentsShouldBeSentOnFirstRequest( .hasStreamId(1) .hasNoLeaks(); - final ByteBuf frameFragment2 = sender.poll(); + final ByteBuf frameFragment2 = sender.awaitFrame(); FrameAssert.assertThat(frameFragment2) .isNotNull() .hasPayloadSize(64 - FRAME_OFFSET_WITH_METADATA) @@ -825,7 +825,7 @@ public void frameFragmentsShouldBeSentOnFirstRequest( .hasStreamId(1) .hasNoLeaks(); - final ByteBuf frameFragment3 = sender.poll(); + final ByteBuf frameFragment3 = sender.awaitFrame(); FrameAssert.assertThat(frameFragment3) .isNotNull() .hasPayloadSize(64 - FRAME_OFFSET) @@ -837,7 +837,7 @@ public void frameFragmentsShouldBeSentOnFirstRequest( .hasStreamId(1) .hasNoLeaks(); - final ByteBuf frameFragment4 = sender.poll(); + final ByteBuf frameFragment4 = sender.awaitFrame(); FrameAssert.assertThat(frameFragment4) .isNotNull() .hasPayloadSize(39) @@ -849,7 +849,7 @@ public void frameFragmentsShouldBeSentOnFirstRequest( .hasStreamId(1) .hasNoLeaks(); - final ByteBuf requestNFrame = sender.poll(); + final ByteBuf requestNFrame = sender.awaitFrame(); FrameAssert.assertThat(requestNFrame) .isNotNull() .typeOf(FrameType.REQUEST_N) @@ -859,14 +859,14 @@ public void frameFragmentsShouldBeSentOnFirstRequest( .hasNoLeaks(); if (!sender.isEmpty()) { - FrameAssert.assertThat(sender.poll()) + FrameAssert.assertThat(sender.awaitFrame()) .isNotNull() .typeOf(FrameType.CANCEL) .hasClientSideStreamId() .hasStreamId(1) .hasNoLeaks(); } - Assertions.assertThat(sender).isEmpty(); + Assertions.assertThat(sender.isEmpty()).isTrue(); // state machine check stateAssert.isTerminated(); allocator.assertHasNoLeaks(); @@ -882,7 +882,7 @@ public void shouldErrorOnIncorrectRefCntInGivenPayload( Consumer monoConsumer) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final Payload payload = ByteBufPayload.create(""); payload.release(); @@ -898,7 +898,7 @@ public void shouldErrorOnIncorrectRefCntInGivenPayload( monoConsumer.accept(requestStreamRequesterFlux); activeStreams.assertNoActiveStreams(); - Assertions.assertThat(sender).isEmpty(); + Assertions.assertThat(sender.isEmpty()).isTrue(); // state machine check stateAssert.isTerminated(); allocator.assertHasNoLeaks(); @@ -925,7 +925,8 @@ public void shouldErrorOnIncorrectRefCntInGivenPayload( public void shouldErrorOnIncorrectRefCntInGivenPayloadLatePhase() { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + ; final Payload payload = ByteBufPayload.create(""); final RequestStreamRequesterFlux requestStreamRequesterFlux = @@ -953,7 +954,7 @@ public void shouldErrorOnIncorrectRefCntInGivenPayloadLatePhase() { .verify(); activeStreams.assertNoActiveStreams(); - Assertions.assertThat(sender).isEmpty(); + Assertions.assertThat(sender.isEmpty()).isTrue(); // state machine check stateAssert.isTerminated(); @@ -969,7 +970,7 @@ public void shouldErrorOnIncorrectRefCntInGivenPayloadLatePhaseWithFragmentation final int mtu = 64; final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(mtu); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final byte[] metadata = new byte[65]; final byte[] data = new byte[129]; @@ -1003,7 +1004,7 @@ public void shouldErrorOnIncorrectRefCntInGivenPayloadLatePhaseWithFragmentation .verify(); activeStreams.assertNoActiveStreams(); - Assertions.assertThat(sender).isEmpty(); + Assertions.assertThat(sender.isEmpty()).isTrue(); // state machine check stateAssert.isTerminated(); allocator.assertHasNoLeaks(); @@ -1019,7 +1020,7 @@ public void shouldErrorIfFragmentExitsAllowanceIfFragmentationDisabled( Consumer monoConsumer) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final byte[] metadata = new byte[FRAME_LENGTH_MASK]; final byte[] data = new byte[FRAME_LENGTH_MASK]; @@ -1043,7 +1044,7 @@ public void shouldErrorIfFragmentExitsAllowanceIfFragmentationDisabled( Assertions.assertThat(payload.refCnt()).isZero(); activeStreams.assertNoActiveStreams(); - Assertions.assertThat(sender).isEmpty(); + Assertions.assertThat(sender.isEmpty()).isTrue(); // state machine check stateAssert.isTerminated(); allocator.assertHasNoLeaks(); @@ -1083,7 +1084,6 @@ public void shouldErrorIfNoAvailability(Consumer mon final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(new RuntimeException("test")); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); - final UnboundedProcessor sender = activeStreams.getSendProcessor(); final Payload payload = TestRequesterResponderSupport.genericPayload(allocator); final RequestStreamRequesterFlux requestStreamRequesterFlux = diff --git a/rsocket-core/src/test/java/io/rsocket/core/RequesterOperatorsRacingTest.java b/rsocket-core/src/test/java/io/rsocket/core/RequesterOperatorsRacingTest.java index 8aee36467..520dd0196 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequesterOperatorsRacingTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequesterOperatorsRacingTest.java @@ -180,7 +180,7 @@ public void shouldSubscribeExactlyOnce(Scenario scenario) { scenario.requestOperator(payloadSupplier, requesterResponderSupport); StepVerifier stepVerifier = - StepVerifier.create(requesterResponderSupport.getSendProcessor()) + StepVerifier.create(requesterResponderSupport.getDuplexConnection().getSentAsPublisher()) .assertNext( frame -> { FrameAssert frameAssert = @@ -239,7 +239,6 @@ public void shouldSubscribeExactlyOnce(Scenario scenario) { }); stepVerifier.verify(Duration.ofSeconds(1)); - Assertions.assertThat(requesterResponderSupport.getSendProcessor().isEmpty()).isTrue(); requesterResponderSupport.getAllocator().assertHasNoLeaks(); } } @@ -266,11 +265,11 @@ public void shouldSentRequestFrameOnceInCaseOfRequestRacing(Scenario scenario) { RaceTestUtils.race(() -> assertSubscriber.request(1), () -> assertSubscriber.request(1)); - final ByteBuf sentFrame = activeStreams.getSendProcessor().poll(); + final ByteBuf sentFrame = activeStreams.getDuplexConnection().awaitFrame(); if (scenario.requestType().hasInitialRequestN()) { if (RequestStreamFrameCodec.initialRequestN(sentFrame) == 1) { - FrameAssert.assertThat(activeStreams.getSendProcessor().poll()) + FrameAssert.assertThat(activeStreams.getDuplexConnection().awaitFrame()) .isNotNull() .hasStreamId(1) .hasRequestN(1) @@ -300,7 +299,7 @@ public void shouldSentRequestFrameOnceInCaseOfRequestRacing(Scenario scenario) { if (scenario.requestType() == REQUEST_CHANNEL) { ((CoreSubscriber) requestOperator).onComplete(); - FrameAssert.assertThat(activeStreams.getSendProcessor().poll()) + FrameAssert.assertThat(activeStreams.getDuplexConnection().awaitFrame()) .typeOf(COMPLETE) .hasStreamId(1) .hasNoLeaks(); @@ -315,7 +314,7 @@ public void shouldSentRequestFrameOnceInCaseOfRequestRacing(Scenario scenario) { }); activeStreams.assertNoActiveStreams(); - Assertions.assertThat(activeStreams.getSendProcessor().isEmpty()).isTrue(); + Assertions.assertThat(activeStreams.getDuplexConnection().isEmpty()).isTrue(); activeStreams.getAllocator().assertHasNoLeaks(); } } @@ -342,7 +341,7 @@ public void shouldHaveNoLeaksOnReassemblyAndCancelRacing(Scenario scenario) { requestOperator.subscribe(assertSubscriber); - final ByteBuf sentFrame = activeStreams.getSendProcessor().poll(); + final ByteBuf sentFrame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(sentFrame) .isNotNull() .hasPayloadSize( @@ -393,12 +392,12 @@ public void shouldHaveNoLeaksOnReassemblyAndCancelRacing(Scenario scenario) { }); } - if (!activeStreams.getSendProcessor().isEmpty()) { + if (!activeStreams.getDuplexConnection().isEmpty()) { if (scenario.requestType() != REQUEST_CHANNEL) { assertSubscriber.assertNotTerminated(); } - final ByteBuf cancellationFrame = activeStreams.getSendProcessor().poll(); + final ByteBuf cancellationFrame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(cancellationFrame) .isNotNull() .typeOf(FrameType.CANCEL) @@ -411,7 +410,7 @@ public void shouldHaveNoLeaksOnReassemblyAndCancelRacing(Scenario scenario) { Assertions.assertThat(responsePayload.refCnt()).isZero(); activeStreams.assertNoActiveStreams(); - Assertions.assertThat(activeStreams.getSendProcessor().isEmpty()).isTrue(); + Assertions.assertThat(activeStreams.getDuplexConnection().isEmpty()).isTrue(); activeStreams.getAllocator().assertHasNoLeaks(); } } @@ -437,7 +436,7 @@ public void shouldHaveNoLeaksOnNextAndCancelRacing() { .expectComplete() .verifyLater(); - final ByteBuf sentFrame = activeStreams.getSendProcessor().poll(); + final ByteBuf sentFrame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(sentFrame) .isNotNull() .hasPayloadSize( @@ -460,9 +459,9 @@ public void shouldHaveNoLeaksOnNextAndCancelRacing() { Assertions.assertThat(response.refCnt()).isZero(); activeStreams.assertNoActiveStreams(); - final boolean isEmpty = activeStreams.getSendProcessor().isEmpty(); + final boolean isEmpty = activeStreams.getDuplexConnection().isEmpty(); if (!isEmpty) { - final ByteBuf cancellationFrame = activeStreams.getSendProcessor().poll(); + final ByteBuf cancellationFrame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(cancellationFrame) .isNotNull() .typeOf(FrameType.CANCEL) @@ -470,7 +469,7 @@ public void shouldHaveNoLeaksOnNextAndCancelRacing() { .hasStreamId(1) .hasNoLeaks(); } - Assertions.assertThat(activeStreams.getSendProcessor().isEmpty()).isTrue(); + Assertions.assertThat(activeStreams.getDuplexConnection().isEmpty()).isTrue(); StateAssert.assertThat(requestResponseRequesterMono).isTerminated(); activeStreams.getAllocator().assertHasNoLeaks(); @@ -509,7 +508,7 @@ public void shouldHaveNoUnexpectedErrorDuringOnErrorAndCancelRacing(boolean with stateAssert.hasSubscribedFlag().hasRequestN(1).hasFirstFrameSentFlag(); - final ByteBuf sentFrame = activeStreams.getSendProcessor().poll(); + final ByteBuf sentFrame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(sentFrame) .isNotNull() .hasPayloadSize( @@ -542,9 +541,9 @@ public void shouldHaveNoUnexpectedErrorDuringOnErrorAndCancelRacing(boolean with activeStreams.assertNoActiveStreams(); stateAssert.isTerminated(); - final boolean isEmpty = activeStreams.getSendProcessor().isEmpty(); + final boolean isEmpty = activeStreams.getDuplexConnection().isEmpty(); if (!isEmpty) { - final ByteBuf cancellationFrame = activeStreams.getSendProcessor().poll(); + final ByteBuf cancellationFrame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(cancellationFrame) .isNotNull() .typeOf(FrameType.CANCEL) @@ -556,7 +555,7 @@ public void shouldHaveNoUnexpectedErrorDuringOnErrorAndCancelRacing(boolean with } else { assertSubscriber.assertTerminated().assertErrorMessage("test"); } - Assertions.assertThat(activeStreams.getSendProcessor().isEmpty()).isTrue(); + Assertions.assertThat(activeStreams.getDuplexConnection().isEmpty()).isTrue(); stateAssert.isTerminated(); droppedErrors.clear(); @@ -601,8 +600,8 @@ public void shouldBeConsistentInCaseOfRacingOfCancellationAndRequest() { RaceTestUtils.race(() -> assertSubscriber.cancel(), () -> assertSubscriber.request(1)); - if (!activeStreams.getSendProcessor().isEmpty()) { - final ByteBuf sentFrame = activeStreams.getSendProcessor().poll(); + if (!activeStreams.getDuplexConnection().isEmpty()) { + final ByteBuf sentFrame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(sentFrame) .isNotNull() .typeOf(FrameType.REQUEST_RESPONSE) @@ -617,7 +616,7 @@ public void shouldBeConsistentInCaseOfRacingOfCancellationAndRequest() { .hasStreamId(1) .hasNoLeaks(); - final ByteBuf cancelFrame = activeStreams.getSendProcessor().poll(); + final ByteBuf cancelFrame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(cancelFrame) .isNotNull() .typeOf(FrameType.CANCEL) @@ -634,7 +633,7 @@ public void shouldBeConsistentInCaseOfRacingOfCancellationAndRequest() { Assertions.assertThat(response.refCnt()).isZero(); activeStreams.assertNoActiveStreams(); - Assertions.assertThat(activeStreams.getSendProcessor().isEmpty()).isTrue(); + Assertions.assertThat(activeStreams.getDuplexConnection().isEmpty()).isTrue(); activeStreams.getAllocator().assertHasNoLeaks(); } } @@ -657,7 +656,7 @@ public void shouldSentCancelFrameExactlyOnce() { assertSubscriber.request(1); - final ByteBuf sentFrame = activeStreams.getSendProcessor().poll(); + final ByteBuf sentFrame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(sentFrame) .isNotNull() .hasNoFragmentsFollow() @@ -675,7 +674,7 @@ public void shouldSentCancelFrameExactlyOnce() { RaceTestUtils.race( requestResponseRequesterMono::cancel, requestResponseRequesterMono::cancel); - final ByteBuf cancelFrame = activeStreams.getSendProcessor().poll(); + final ByteBuf cancelFrame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(cancelFrame) .isNotNull() .typeOf(FrameType.CANCEL) @@ -695,7 +694,7 @@ public void shouldSentCancelFrameExactlyOnce() { assertSubscriber.assertNotTerminated(); activeStreams.assertNoActiveStreams(); - Assertions.assertThat(activeStreams.getSendProcessor().isEmpty()).isTrue(); + Assertions.assertThat(activeStreams.getDuplexConnection().isEmpty()).isTrue(); activeStreams.getAllocator().assertHasNoLeaks(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/ResponderOperatorsCommonTest.java b/rsocket-core/src/test/java/io/rsocket/core/ResponderOperatorsCommonTest.java index 2872d8d78..270bc4a05 100755 --- a/rsocket-core/src/test/java/io/rsocket/core/ResponderOperatorsCommonTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ResponderOperatorsCommonTest.java @@ -28,8 +28,8 @@ import io.rsocket.RSocket; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.FrameType; -import io.rsocket.internal.UnboundedProcessor; import io.rsocket.internal.subscriber.AssertSubscriber; +import io.rsocket.test.util.TestDuplexConnection; import java.util.ArrayList; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Stream; @@ -245,7 +245,7 @@ void shouldHandleRequest(Scenario scenario) { TestRequesterResponderSupport testRequesterResponderSupport = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = testRequesterResponderSupport.getAllocator(); - final UnboundedProcessor sender = testRequesterResponderSupport.getSendProcessor(); + final TestDuplexConnection sender = testRequesterResponderSupport.getDuplexConnection(); TestPublisher testPublisher = TestPublisher.create(); TestHandler testHandler = new TestHandler(testPublisher, new AssertSubscriber<>(0)); @@ -261,7 +261,7 @@ void shouldHandleRequest(Scenario scenario) { testPublisher.next(randomPayload.retain()); testPublisher.complete(); - FrameAssert.assertThat(sender.poll()) + FrameAssert.assertThat(sender.awaitFrame()) .isNotNull() .hasStreamId(1) .typeOf(scenario.requestType() == REQUEST_RESPONSE ? FrameType.NEXT_COMPLETE : NEXT) @@ -274,11 +274,14 @@ void shouldHandleRequest(Scenario scenario) { if (scenario.requestType() != REQUEST_RESPONSE) { - FrameAssert.assertThat(sender.poll()).typeOf(FrameType.COMPLETE).hasStreamId(1).hasNoLeaks(); + FrameAssert.assertThat(sender.awaitFrame()) + .typeOf(FrameType.COMPLETE) + .hasStreamId(1) + .hasNoLeaks(); if (scenario.requestType() == REQUEST_CHANNEL) { testHandler.consumer.request(2); - FrameAssert.assertThat(sender.poll()) + FrameAssert.assertThat(sender.awaitFrame()) .typeOf(FrameType.REQUEST_N) .hasStreamId(1) .hasRequestN(1) @@ -302,7 +305,7 @@ void shouldHandleFragmentedRequest(Scenario scenario) { TestRequesterResponderSupport testRequesterResponderSupport = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = testRequesterResponderSupport.getAllocator(); - final UnboundedProcessor sender = testRequesterResponderSupport.getSendProcessor(); + final TestDuplexConnection sender = testRequesterResponderSupport.getDuplexConnection(); TestPublisher testPublisher = TestPublisher.create(); TestHandler testHandler = new TestHandler(testPublisher, new AssertSubscriber<>(0)); @@ -332,7 +335,7 @@ void shouldHandleFragmentedRequest(Scenario scenario) { testPublisher.next(randomPayload.retain()); testPublisher.complete(); - FrameAssert.assertThat(sender.poll()) + FrameAssert.assertThat(sender.awaitFrame()) .isNotNull() .hasStreamId(1) .typeOf(scenario.requestType() == REQUEST_RESPONSE ? FrameType.NEXT_COMPLETE : NEXT) @@ -345,11 +348,14 @@ void shouldHandleFragmentedRequest(Scenario scenario) { if (scenario.requestType() != REQUEST_RESPONSE) { - FrameAssert.assertThat(sender.poll()).typeOf(FrameType.COMPLETE).hasStreamId(1).hasNoLeaks(); + FrameAssert.assertThat(sender.awaitFrame()) + .typeOf(FrameType.COMPLETE) + .hasStreamId(1) + .hasNoLeaks(); if (scenario.requestType() == REQUEST_CHANNEL) { testHandler.consumer.request(2); - FrameAssert.assertThat(sender.poll()).isNull(); + FrameAssert.assertThat(sender.pollFrame()).isNull(); } } @@ -375,7 +381,6 @@ void shouldHandleInterruptedFragmentation(Scenario scenario) { TestRequesterResponderSupport testRequesterResponderSupport = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = testRequesterResponderSupport.getAllocator(); - final UnboundedProcessor sender = testRequesterResponderSupport.getSendProcessor(); TestPublisher testPublisher = TestPublisher.create(); TestHandler testHandler = new TestHandler(testPublisher, new AssertSubscriber<>(0)); 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 a64bf9b81..da0fb2364 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -145,11 +145,7 @@ public Mono start(ConnectionAcceptor acceptor) { } public ByteBuf awaitSent() { - try { - return conn.awaitSend(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } + return conn.awaitFrame(); } public void connect() { diff --git a/rsocket-core/src/test/java/io/rsocket/core/TestRequesterResponderSupport.java b/rsocket-core/src/test/java/io/rsocket/core/TestRequesterResponderSupport.java index 8069e7362..f81e8a610 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/TestRequesterResponderSupport.java +++ b/rsocket-core/src/test/java/io/rsocket/core/TestRequesterResponderSupport.java @@ -21,10 +21,12 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; +import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.FrameType; import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.ByteBufPayload; import java.util.ArrayList; import java.util.concurrent.ThreadLocalRandom; @@ -42,6 +44,7 @@ final class TestRequesterResponderSupport extends RequesterResponderSupport { TestRequesterResponderSupport( @Nullable Throwable error, StreamIdSupplier streamIdSupplier, + DuplexConnection connection, int mtu, int maxFrameLength, int maxInboundPayloadSize) { @@ -50,11 +53,16 @@ final class TestRequesterResponderSupport extends RequesterResponderSupport { maxFrameLength, maxInboundPayloadSize, PayloadDecoder.ZERO_COPY, - LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT), + connection, streamIdSupplier); this.error = error; } + @Override + public TestDuplexConnection getDuplexConnection() { + return (TestDuplexConnection) super.getDuplexConnection(); + } + static Payload genericPayload(LeaksTrackingByteBufAllocator allocator) { ByteBuf data = allocator.buffer(); data.writeCharSequence(DATA_CONTENT, CharsetUtil.UTF_8); @@ -168,8 +176,28 @@ public static TestRequesterResponderSupport client(@Nullable Throwable e) { public static TestRequesterResponderSupport client( int mtu, int maxFrameLength, int maxInboundPayloadSize, @Nullable Throwable e) { + return client( + new TestDuplexConnection( + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT)), + mtu, + maxFrameLength, + maxInboundPayloadSize, + e); + } + + public static TestRequesterResponderSupport client( + TestDuplexConnection duplexConnection, + int mtu, + int maxFrameLength, + int maxInboundPayloadSize, + @Nullable Throwable e) { return new TestRequesterResponderSupport( - e, StreamIdSupplier.clientSupplier(), mtu, maxFrameLength, maxInboundPayloadSize); + e, + StreamIdSupplier.clientSupplier(), + duplexConnection, + mtu, + maxFrameLength, + maxInboundPayloadSize); } public static TestRequesterResponderSupport client( diff --git a/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java b/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java index 9da66d424..e0374eede 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java @@ -1,93 +1,93 @@ -package io.rsocket.resume; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import java.util.Arrays; -import org.junit.Assert; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; - -public class InMemoryResumeStoreTest { - - @Test - void saveWithoutTailRemoval() { - InMemoryResumableFramesStore store = inMemoryStore(25); - ByteBuf frame = frameMock(10); - store.saveFrames(Flux.just(frame)).block(); - Assert.assertEquals(1, store.cachedFrames.size()); - Assert.assertEquals(frame.readableBytes(), store.cacheSize); - Assert.assertEquals(0, store.position); - } - - @Test - void saveRemoveOneFromTail() { - InMemoryResumableFramesStore store = inMemoryStore(25); - ByteBuf frame1 = frameMock(20); - ByteBuf frame2 = frameMock(10); - store.saveFrames(Flux.just(frame1, frame2)).block(); - Assert.assertEquals(1, store.cachedFrames.size()); - Assert.assertEquals(frame2.readableBytes(), store.cacheSize); - Assert.assertEquals(frame1.readableBytes(), store.position); - } - - @Test - void saveRemoveTwoFromTail() { - InMemoryResumableFramesStore store = inMemoryStore(25); - ByteBuf frame1 = frameMock(10); - ByteBuf frame2 = frameMock(10); - ByteBuf frame3 = frameMock(20); - store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); - Assert.assertEquals(1, store.cachedFrames.size()); - Assert.assertEquals(frame3.readableBytes(), store.cacheSize); - Assert.assertEquals(size(frame1, frame2), store.position); - } - - @Test - void saveBiggerThanStore() { - InMemoryResumableFramesStore store = inMemoryStore(25); - ByteBuf frame1 = frameMock(10); - ByteBuf frame2 = frameMock(10); - ByteBuf frame3 = frameMock(30); - store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); - Assert.assertEquals(0, store.cachedFrames.size()); - Assert.assertEquals(0, store.cacheSize); - Assert.assertEquals(size(frame1, frame2, frame3), store.position); - } - - @Test - void releaseFrames() { - InMemoryResumableFramesStore store = inMemoryStore(100); - ByteBuf frame1 = frameMock(10); - ByteBuf frame2 = frameMock(10); - ByteBuf frame3 = frameMock(30); - store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); - store.releaseFrames(20); - Assert.assertEquals(1, store.cachedFrames.size()); - Assert.assertEquals(frame3.readableBytes(), store.cacheSize); - Assert.assertEquals(size(frame1, frame2), store.position); - } - - @Test - void receiveImpliedPosition() { - InMemoryResumableFramesStore store = inMemoryStore(100); - ByteBuf frame1 = frameMock(10); - ByteBuf frame2 = frameMock(30); - store.resumableFrameReceived(frame1); - store.resumableFrameReceived(frame2); - Assert.assertEquals(size(frame1, frame2), store.frameImpliedPosition()); - } - - private int size(ByteBuf... byteBufs) { - return Arrays.stream(byteBufs).mapToInt(ByteBuf::readableBytes).sum(); - } - - private static InMemoryResumableFramesStore inMemoryStore(int size) { - return new InMemoryResumableFramesStore("test", size); - } - - private static ByteBuf frameMock(int size) { - byte[] bytes = new byte[size]; - Arrays.fill(bytes, (byte) 7); - return Unpooled.wrappedBuffer(bytes); - } -} +// package io.rsocket.resume; +// +// import io.netty.buffer.ByteBuf; +// import io.netty.buffer.Unpooled; +// import java.util.Arrays; +// import org.junit.Assert; +// import org.junit.jupiter.api.Test; +// import reactor.core.publisher.Flux; +// +// public class InMemoryResumeStoreTest { +// +// @Test +// void saveWithoutTailRemoval() { +// InMemoryResumableFramesStore store = inMemoryStore(25); +// ByteBuf frame = frameMock(10); +// store.saveFrames(Flux.just(frame)).block(); +// Assert.assertEquals(1, store.cachedFrames.size()); +// Assert.assertEquals(frame.readableBytes(), store.cacheSize); +// Assert.assertEquals(0, store.position); +// } +// +// @Test +// void saveRemoveOneFromTail() { +// InMemoryResumableFramesStore store = inMemoryStore(25); +// ByteBuf frame1 = frameMock(20); +// ByteBuf frame2 = frameMock(10); +// store.saveFrames(Flux.just(frame1, frame2)).block(); +// Assert.assertEquals(1, store.cachedFrames.size()); +// Assert.assertEquals(frame2.readableBytes(), store.cacheSize); +// Assert.assertEquals(frame1.readableBytes(), store.position); +// } +// +// @Test +// void saveRemoveTwoFromTail() { +// InMemoryResumableFramesStore store = inMemoryStore(25); +// ByteBuf frame1 = frameMock(10); +// ByteBuf frame2 = frameMock(10); +// ByteBuf frame3 = frameMock(20); +// store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); +// Assert.assertEquals(1, store.cachedFrames.size()); +// Assert.assertEquals(frame3.readableBytes(), store.cacheSize); +// Assert.assertEquals(size(frame1, frame2), store.position); +// } +// +// @Test +// void saveBiggerThanStore() { +// InMemoryResumableFramesStore store = inMemoryStore(25); +// ByteBuf frame1 = frameMock(10); +// ByteBuf frame2 = frameMock(10); +// ByteBuf frame3 = frameMock(30); +// store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); +// Assert.assertEquals(0, store.cachedFrames.size()); +// Assert.assertEquals(0, store.cacheSize); +// Assert.assertEquals(size(frame1, frame2, frame3), store.position); +// } +// +// @Test +// void releaseFrames() { +// InMemoryResumableFramesStore store = inMemoryStore(100); +// ByteBuf frame1 = frameMock(10); +// ByteBuf frame2 = frameMock(10); +// ByteBuf frame3 = frameMock(30); +// store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); +// store.releaseFrames(20); +// Assert.assertEquals(1, store.cachedFrames.size()); +// Assert.assertEquals(frame3.readableBytes(), store.cacheSize); +// Assert.assertEquals(size(frame1, frame2), store.position); +// } +// +// @Test +// void receiveImpliedPosition() { +// InMemoryResumableFramesStore store = inMemoryStore(100); +// ByteBuf frame1 = frameMock(10); +// ByteBuf frame2 = frameMock(30); +// store.resumableFrameReceived(frame1); +// store.resumableFrameReceived(frame2); +// Assert.assertEquals(size(frame1, frame2), store.frameImpliedPosition()); +// } +// +// private int size(ByteBuf... byteBufs) { +// return Arrays.stream(byteBufs).mapToInt(ByteBuf::readableBytes).sum(); +// } +// +// private static InMemoryResumableFramesStore inMemoryStore(int size) { +// return new InMemoryResumableFramesStore("test", size); +// } +// +// private static ByteBuf frameMock(int size) { +// byte[] bytes = new byte[size]; +// Arrays.fill(bytes, (byte) 7); +// return Unpooled.wrappedBuffer(bytes); +// } +// } diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java index 7d2a7bcc8..d15abd189 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java @@ -1,57 +1,57 @@ -/* - * Copyright 2015-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.resume; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class ResumeCalculatorTest { - - @BeforeEach - void setUp() {} - - @Test - void clientResumeSuccess() { - long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 42, -1, 3); - Assertions.assertEquals(3, position); - } - - @Test - void clientResumeError() { - long position = ResumableDuplexConnection.calculateRemoteImpliedPos(4, 42, -1, 3); - Assertions.assertEquals(-1, position); - } - - @Test - void serverResumeSuccess() { - long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 42, 4, 23); - Assertions.assertEquals(23, position); - } - - @Test - void serverResumeErrorClientState() { - long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 3, 4, 23); - Assertions.assertEquals(-1, position); - } - - @Test - void serverResumeErrorServerState() { - long position = ResumableDuplexConnection.calculateRemoteImpliedPos(4, 42, 4, 1); - Assertions.assertEquals(-1, position); - } -} +/// * +// * Copyright 2015-2019 the original author or authors. +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +// +// package io.rsocket.resume; +// +// import org.junit.jupiter.api.Assertions; +// import org.junit.jupiter.api.BeforeEach; +// import org.junit.jupiter.api.Test; +// +// public class ResumeCalculatorTest { +// +// @BeforeEach +// void setUp() {} +// +// @Test +// void clientResumeSuccess() { +// long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 42, -1, 3); +// Assertions.assertEquals(3, position); +// } +// +// @Test +// void clientResumeError() { +// long position = ResumableDuplexConnection.calculateRemoteImpliedPos(4, 42, -1, 3); +// Assertions.assertEquals(-1, position); +// } +// +// @Test +// void serverResumeSuccess() { +// long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 42, 4, 23); +// Assertions.assertEquals(23, position); +// } +// +// @Test +// void serverResumeErrorClientState() { +// long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 3, 4, 23); +// Assertions.assertEquals(-1, position); +// } +// +// @Test +// void serverResumeErrorServerState() { +// long position = ResumableDuplexConnection.calculateRemoteImpliedPos(4, 42, 4, 1); +// Assertions.assertEquals(-1, position); +// } +// } diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/LocalDuplexConnection.java b/rsocket-core/src/test/java/io/rsocket/test/util/LocalDuplexConnection.java index f455c8385..9f5c021af 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/LocalDuplexConnection.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/LocalDuplexConnection.java @@ -19,8 +19,9 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; +import io.rsocket.RSocketErrorException; +import io.rsocket.frame.ErrorFrameCodec; import java.net.SocketAddress; -import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.publisher.DirectProcessor; @@ -49,12 +50,17 @@ public LocalDuplexConnection( } @Override - public Mono send(Publisher frame) { - return Flux.from(frame) - .doOnNext(f -> System.out.println(name + " - " + f.toString())) - .doOnNext(send::onNext) - .doOnError(send::onError) - .then(); + public void sendFrame(int streamId, ByteBuf frame) { + System.out.println(name + " - " + frame.toString()); + send.onNext(frame); + } + + @Override + public void sendErrorAndClose(RSocketErrorException e) { + final ByteBuf errorFrame = ErrorFrameCodec.encode(allocator, 0, e); + System.out.println(name + " - " + errorFrame.toString()); + send.onNext(errorFrame); + onClose.onComplete(); } @Override 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 88694d209..e307627ff 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 @@ -11,13 +11,14 @@ public class TestClientTransport implements ClientTransport { private final LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); - private final TestDuplexConnection testDuplexConnection = new TestDuplexConnection(allocator); + + private volatile TestDuplexConnection testDuplexConnection; int maxFrameLength = FRAME_LENGTH_MASK; @Override public Mono connect() { - return Mono.just(testDuplexConnection); + return Mono.fromSupplier(() -> testDuplexConnection = new TestDuplexConnection(allocator)); } public TestDuplexConnection testConnection() { diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java index 91de8f0de..8793d6ca4 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java @@ -17,8 +17,10 @@ package io.rsocket.test.util; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; +import io.rsocket.RSocketErrorException; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.frame.ErrorFrameCodec; import java.net.SocketAddress; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -33,6 +35,7 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Operators; +import reactor.util.annotation.NonNull; /** * An implementation of {@link DuplexConnection} that provides functionality to modify the behavior @@ -43,16 +46,17 @@ public class TestDuplexConnection implements DuplexConnection { private static final Logger logger = LoggerFactory.getLogger(TestDuplexConnection.class); private final LinkedBlockingQueue sent; + private final DirectProcessor sentPublisher; private final FluxSink sendSink; private final DirectProcessor received; private final FluxSink receivedSink; private final MonoProcessor onClose; - private final ByteBufAllocator allocator; + private final LeaksTrackingByteBufAllocator allocator; private volatile double availability = 1; private volatile int initialSendRequestN = Integer.MAX_VALUE; - public TestDuplexConnection(ByteBufAllocator allocator) { + public TestDuplexConnection(LeaksTrackingByteBufAllocator allocator) { this.allocator = allocator; this.sent = new LinkedBlockingQueue<>(); this.received = DirectProcessor.create(); @@ -63,19 +67,13 @@ public TestDuplexConnection(ByteBufAllocator allocator) { } @Override - public Mono send(Publisher frames) { + public void sendFrame(int streamId, ByteBuf frame) { if (availability <= 0) { - return Mono.error( - new IllegalStateException("RSocket not available. Availability: " + availability)); + throw new IllegalStateException("RSocket not available. Availability: " + availability); } - return Flux.from(frames) - .doOnNext( - frame -> { - sendSink.next(frame); - sent.offer(frame); - }) - .doOnError(throwable -> logger.error("Error in send stream on test connection.", throwable)) - .then(); + + sendSink.next(frame); + sent.offer(frame); } @Override @@ -108,7 +106,21 @@ public void onComplete() { } @Override - public ByteBufAllocator alloc() { + public void sendErrorAndClose(RSocketErrorException e) { + final ByteBuf errorFrame = ErrorFrameCodec.encode(allocator, 0, e); + sendSink.next(errorFrame); + sent.offer(errorFrame); + + final Throwable cause = e.getCause(); + if (cause == null) { + onClose.onComplete(); + } else { + onClose.onError(cause); + } + } + + @Override + public LeaksTrackingByteBufAllocator alloc() { return allocator; } @@ -137,8 +149,21 @@ public Mono onClose() { return onClose; } - public ByteBuf awaitSend() throws InterruptedException { - return sent.take(); + public boolean isEmpty() { + return sent.isEmpty(); + } + + @NonNull + public ByteBuf awaitFrame() { + try { + return sent.take(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public ByteBuf pollFrame() { + return sent.poll(); } public void setAvailability(double availability) { diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java index 93b54e146..fb2383755 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java @@ -62,7 +62,8 @@ public static void main(String[] args) { return Files.fileSource(fileName, chunkSize) .map(DefaultPayload::create) - .zipWith(ticks, (p, tick) -> p); + .zipWith(ticks, (p, tick) -> p) + .log("server"); })) .resume(resume) .bind(TcpServerTransport.create("localhost", 8000)) @@ -76,8 +77,9 @@ public static void main(String[] args) { client .requestStream(codec.encode(new Request(16, "lorem.txt"))) + .log("client") .doFinally(s -> server.dispose()) - .subscribe(Files.fileSink("rsocket-examples/out/lorem_output.txt", PREFETCH_WINDOW_SIZE)); + .subscribe(Files.fileSink("rsocket-examples/build/lorem_output.txt", PREFETCH_WINDOW_SIZE)); server.onClose().block(); } diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java index 4f00073eb..7c7ac37b9 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java @@ -22,13 +22,13 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; +import io.rsocket.RSocketErrorException; import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; import java.net.SocketAddress; import java.util.Objects; import java.util.function.Consumer; -import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; @@ -111,10 +111,14 @@ public Flux receive() { } @Override - public Mono send(Publisher frames) { - Objects.requireNonNull(frames, "frames must not be null"); + public void sendFrame(int streamId, ByteBuf frame) { + frameCounters.accept(frame); + delegate.sendFrame(streamId, frame); + } - return delegate.send(Flux.from(frames).doOnNext(frameCounters)); + @Override + public void sendErrorAndClose(RSocketErrorException e) { + delegate.sendErrorAndClose(e); } private static final class FrameCounters implements Consumer { diff --git a/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerDuplexConnectionTest.java b/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerDuplexConnectionTest.java index 03abd2084..7806200dd 100644 --- a/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerDuplexConnectionTest.java +++ b/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerDuplexConnectionTest.java @@ -34,7 +34,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import org.reactivestreams.Publisher; +import org.mockito.Mockito; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; @@ -153,32 +153,29 @@ void receive() { @SuppressWarnings("unchecked") @Test void send() { - ArgumentCaptor> captor = ArgumentCaptor.forClass(Publisher.class); - when(delegate.send(captor.capture())).thenReturn(Mono.empty()); - - Flux frames = - Flux.just( - createTestCancelFrame(), - createTestErrorFrame(), - createTestKeepaliveFrame(), - createTestLeaseFrame(), - createTestMetadataPushFrame(), - createTestPayloadFrame(), - createTestRequestChannelFrame(), - createTestRequestFireAndForgetFrame(), - createTestRequestNFrame(), - createTestRequestResponseFrame(), - createTestRequestStreamFrame(), - createTestSetupFrame()); - - new MicrometerDuplexConnection( - SERVER, delegate, meterRegistry, Tag.of("test-key", "test-value")) - .send(frames) - .as(StepVerifier::create) + ArgumentCaptor captor = ArgumentCaptor.forClass(ByteBuf.class); + doNothing().when(delegate).sendFrame(Mockito.anyInt(), captor.capture()); + + final MicrometerDuplexConnection micrometerDuplexConnection = + new MicrometerDuplexConnection( + SERVER, delegate, meterRegistry, Tag.of("test-key", "test-value")); + micrometerDuplexConnection.sendFrame(1, createTestCancelFrame()); + micrometerDuplexConnection.sendFrame(1, createTestErrorFrame()); + micrometerDuplexConnection.sendFrame(1, createTestKeepaliveFrame()); + micrometerDuplexConnection.sendFrame(1, createTestLeaseFrame()); + micrometerDuplexConnection.sendFrame(1, createTestMetadataPushFrame()); + micrometerDuplexConnection.sendFrame(1, createTestPayloadFrame()); + micrometerDuplexConnection.sendFrame(1, createTestRequestChannelFrame()); + micrometerDuplexConnection.sendFrame(1, createTestRequestFireAndForgetFrame()); + micrometerDuplexConnection.sendFrame(1, createTestRequestNFrame()); + micrometerDuplexConnection.sendFrame(1, createTestRequestResponseFrame()); + micrometerDuplexConnection.sendFrame(1, createTestRequestStreamFrame()); + micrometerDuplexConnection.sendFrame(1, createTestSetupFrame()); + + StepVerifier.create(Flux.fromIterable(captor.getAllValues())) + .expectNextCount(12) .verifyComplete(); - StepVerifier.create(captor.getValue()).expectNextCount(12).verifyComplete(); - assertThat(findCounter(SERVER, CANCEL).count()).isEqualTo(1); assertThat(findCounter(SERVER, COMPLETE).count()).isEqualTo(1); assertThat(findCounter(SERVER, ERROR).count()).isEqualTo(1); @@ -193,15 +190,6 @@ void send() { assertThat(findCounter(SERVER, SETUP).count()).isEqualTo(1); } - @DisplayName("send throws NullPointerException with null frames") - @Test - void sendNullFrames() { - assertThatNullPointerException() - .isThrownBy( - () -> new MicrometerDuplexConnection(CLIENT, delegate, meterRegistry).send(null)) - .withMessage("frames must not be null"); - } - private Counter findCounter(Type connectionType, FrameType frameType) { return meterRegistry .get("rsocket.frame") 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 141ed4385..2ca646936 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java +++ b/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java @@ -6,6 +6,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import java.time.Duration; +import java.util.ArrayList; import java.util.concurrent.ConcurrentLinkedQueue; import org.assertj.core.api.Assertions; @@ -50,26 +51,35 @@ private LeaksTrackingByteBufAllocator( public LeaksTrackingByteBufAllocator assertHasNoLeaks() { try { - Assertions.assertThat(tracker) - .allSatisfy( - buf -> - Assertions.assertThat(buf) - .matches( - bb -> { - final Duration awaitZeroRefCntDuration = this.awaitZeroRefCntDuration; - if (!awaitZeroRefCntDuration.isZero()) { - long end = - awaitZeroRefCntDuration.plusNanos(System.nanoTime()).toNanos(); - while (bb.refCnt() != 0) { - if (System.nanoTime() >= end) { - break; - } - parkNanos(100); - } - } - return bb.refCnt() == 0; - }, - "buffer should be released")); + ArrayList unreleased = new ArrayList<>(); + for (ByteBuf bb : tracker) { + if (bb.refCnt() != 0) { + unreleased.add(bb); + } + } + + final Duration awaitZeroRefCntDuration = this.awaitZeroRefCntDuration; + if (!unreleased.isEmpty() && !awaitZeroRefCntDuration.isZero()) { + long endTimeInMillis = System.currentTimeMillis() + awaitZeroRefCntDuration.toMillis(); + boolean hasUnreleased; + while (System.currentTimeMillis() <= endTimeInMillis) { + hasUnreleased = false; + for (ByteBuf bb : unreleased) { + if (bb.refCnt() != 0) { + hasUnreleased = true; + break; + } + } + + if (!hasUnreleased) { + break; + } + + parkNanos(100); + } + } + + Assertions.assertThat(unreleased).allMatch(bb -> bb.refCnt() == 0); } finally { tracker.clear(); } diff --git a/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java b/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java index e322ad292..1b294e394 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java @@ -95,7 +95,7 @@ public boolean awaitAllInteractionTermination(Duration duration) { } public boolean awaitUntilObserved(int interactions, Duration duration) { - long end = duration.plusNanos(System.nanoTime()).toNanos(); + long end = System.nanoTime() + duration.toNanos(); long observed; while ((observed = observedInteractions.get()) < interactions) { if (System.nanoTime() >= end) { 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 f55e08bd7..48472dec9 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -25,9 +25,13 @@ import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.RSocket; +import io.rsocket.RSocketErrorException; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; +import io.rsocket.core.Resume; import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.plugins.DuplexConnectionInterceptor; +import io.rsocket.resume.InMemoryResumableFramesStore; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import io.rsocket.util.ByteBufPayload; @@ -38,19 +42,18 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.Executors; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; 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.junit.platform.commons.logging.Logger; -import org.junit.platform.commons.logging.LoggerFactory; -import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Disposable; @@ -58,14 +61,17 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Operators; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; +import reactor.util.Logger; +import reactor.util.Loggers; public interface TransportTest { - Logger logger = LoggerFactory.getLogger(TransportTest.class); + Logger logger = Loggers.getLogger(TransportTest.class); String MOCK_DATA = "test-data"; String MOCK_METADATA = "metadata"; @@ -153,6 +159,7 @@ default RSocket getClient() { @DisplayName("makes 10 metadataPush requests") @Test default void metadataPush10() { + Assumptions.assumeThat(getTransportPair().withResumability).isFalse(); Flux.range(1, 10) .flatMap(i -> getClient().metadataPush(ByteBufPayload.create("", "test-metadata"))) .as(StepVerifier::create) @@ -165,6 +172,7 @@ default void metadataPush10() { @DisplayName("makes 10 metadataPush with Large Metadata in requests") @Test default void largePayloadMetadataPush10() { + Assumptions.assumeThat(getTransportPair().withResumability).isFalse(); Flux.range(1, 10) .flatMap(i -> getClient().metadataPush(ByteBufPayload.create("", LARGE_DATA))) .as(StepVerifier::create) @@ -275,10 +283,19 @@ default void requestChannel3() { Assertions.assertThat(requested.get()).isEqualTo(3L); } - @DisplayName("makes 1 requestChannel request with 512 payloads") + @DisplayName("makes 1 requestChannel request with 256 payloads") @Test - default void requestChannel512() { - Flux payloads = Flux.range(0, 512).map(this::createTestPayload); + default void requestChannel256() { + Assumptions.assumeThat(getTransportPair().withResumability).isFalse(); + AtomicInteger counter = new AtomicInteger(); + Flux payloads = + Flux.defer( + () -> { + final int subscription = counter.getAndIncrement(); + return Flux.range(0, 256) + .map(i -> "S{" + subscription + "}: Data{" + i + "}") + .map(data -> ByteBufPayload.create(data)); + }); final Scheduler scheduler = Schedulers.fromExecutorService(Executors.newFixedThreadPool(13)); Flux.range(0, 1024) @@ -289,10 +306,15 @@ default void requestChannel512() { default void check(Flux payloads) { getClient() .requestChannel(payloads) - .doOnNext(Payload::release) + .map( + payload -> { + final String data = payload.getDataUtf8(); + payload.release(); + return data; + }) .as(StepVerifier::create) - .expectNextCount(512) - .as("expected 512 items") + .expectNextCount(256) + .as("expected 256 items") .expectComplete() .verify(getTimeout()); } @@ -418,6 +440,8 @@ default void assertChannelPayload(Payload p) { } class TransportPair implements Disposable { + + private final boolean withResumability; private static final String data = "hello world"; private static final String metadata = "metadata"; @@ -442,6 +466,21 @@ public TransportPair( TriFunction clientTransportSupplier, BiFunction> serverTransportSupplier, boolean withRandomFragmentation) { + this( + addressSupplier, + clientTransportSupplier, + serverTransportSupplier, + withRandomFragmentation, + false); + } + + public TransportPair( + Supplier addressSupplier, + TriFunction clientTransportSupplier, + BiFunction> serverTransportSupplier, + boolean withRandomFragmentation, + boolean withResumability) { + this.withResumability = withResumability; T address = addressSupplier.get(); @@ -451,7 +490,7 @@ public TransportPair( ByteBufAllocator allocatorToSupply; if (ResourceLeakDetector.getLevel() == ResourceLeakDetector.Level.ADVANCED || ResourceLeakDetector.getLevel() == ResourceLeakDetector.Level.PARANOID) { - logger.info(() -> "Using LeakTrackingByteBufAllocator"); + logger.info("Using LeakTrackingByteBufAllocator"); allocatorToSupply = byteBufAllocator; } else { allocatorToSupply = ByteBufAllocator.DEFAULT; @@ -462,10 +501,9 @@ public TransportPair( .payloadDecoder(PayloadDecoder.ZERO_COPY) .interceptors( registry -> { - if (runServerWithAsyncInterceptors) { + if (runServerWithAsyncInterceptors && !withResumability) { logger.info( - () -> - "Perform Integration Test with Async Interceptors Enabled For Server"); + "Perform Integration Test with Async Interceptors Enabled For Server"); registry .forConnection( (type, duplexConnection) -> @@ -477,8 +515,26 @@ public TransportPair( .accept(connectionSetupPayload, sendingSocket) .subscribeOn(Schedulers.parallel())); } + + if (withResumability) { + registry.forConnection( + (type, duplexConnection) -> + type == DuplexConnectionInterceptor.Type.SOURCE + ? new DisconnectingDuplexConnection( + "Server", + duplexConnection, + Duration.ofMillis( + ThreadLocalRandom.current().nextInt(200, 500))) + : duplexConnection); + } }); + if (withResumability) { + rSocketServer.resume( + new Resume() + .storeFactory(__ -> new InMemoryResumableFramesStore("server", Integer.MAX_VALUE))); + } + if (withRandomFragmentation) { rSocketServer.fragment(ThreadLocalRandom.current().nextInt(256, 512)); } @@ -492,10 +548,9 @@ public TransportPair( .keepAlive(Duration.ofMillis(Integer.MAX_VALUE), Duration.ofMillis(Integer.MAX_VALUE)) .interceptors( registry -> { - if (runClientWithAsyncInterceptors) { + if (runClientWithAsyncInterceptors && !withResumability) { logger.info( - () -> - "Perform Integration Test with Async Interceptors Enabled For Client"); + "Perform Integration Test with Async Interceptors Enabled For Client"); registry .forConnection( (type, duplexConnection) -> @@ -507,8 +562,26 @@ public TransportPair( .accept(connectionSetupPayload, sendingSocket) .subscribeOn(Schedulers.parallel())); } + + if (withResumability) { + registry.forConnection( + (type, duplexConnection) -> + type == DuplexConnectionInterceptor.Type.SOURCE + ? new DisconnectingDuplexConnection( + "Client", + duplexConnection, + Duration.ofMillis( + ThreadLocalRandom.current().nextInt(200, 500))) + : duplexConnection); + } }); + if (withResumability) { + rSocketConnector.resume( + new Resume() + .storeFactory(__ -> new InMemoryResumableFramesStore("client", Integer.MAX_VALUE))); + } + if (withRandomFragmentation) { rSocketConnector.fragment(ThreadLocalRandom.current().nextInt(256, 512)); } @@ -541,27 +614,37 @@ public String expectedPayloadMetadata() { private static class AsyncDuplexConnection implements DuplexConnection { private final DuplexConnection duplexConnection; + private final ByteBufReleaserOperator bufReleaserOperator; public AsyncDuplexConnection(DuplexConnection duplexConnection) { this.duplexConnection = duplexConnection; + this.bufReleaserOperator = new ByteBufReleaserOperator(); } @Override - public Mono send(Publisher frames) { - return duplexConnection.send(frames); + public void sendFrame(int streamId, ByteBuf frame) { + duplexConnection.sendFrame(streamId, frame); + } + + @Override + public void sendErrorAndClose(RSocketErrorException e) { + duplexConnection.sendErrorAndClose(e); } @Override public Flux receive() { return duplexConnection .receive() - .subscribeOn(Schedulers.parallel()) + .subscribeOn(Schedulers.boundedElastic()) .doOnNext(ByteBuf::retain) - .publishOn(Schedulers.parallel(), Integer.MAX_VALUE) + .publishOn(Schedulers.boundedElastic(), Integer.MAX_VALUE) .doOnDiscard(ReferenceCounted.class, ReferenceCountUtil::safeRelease) .transform( Operators.lift( - (__, actual) -> new ByteBufReleaserOperator(actual))); + (__, actual) -> { + bufReleaserOperator.actual = actual; + return bufReleaserOperator; + })); } @Override @@ -576,7 +659,7 @@ public SocketAddress remoteAddress() { @Override public Mono onClose() { - return duplexConnection.onClose(); + return duplexConnection.onClose().and(bufReleaserOperator.onClose()); } @Override @@ -585,15 +668,79 @@ public void dispose() { } } + private static class DisconnectingDuplexConnection implements DuplexConnection { + + private final String tag; + final DuplexConnection source; + final Duration delay; + + DisconnectingDuplexConnection(String tag, DuplexConnection source, Duration delay) { + this.tag = tag; + this.source = source; + this.delay = delay; + } + + @Override + public void dispose() { + source.dispose(); + } + + @Override + public Mono onClose() { + return source.onClose(); + } + + @Override + public void sendFrame(int streamId, ByteBuf frame) { + source.sendFrame(streamId, frame); + } + + @Override + public void sendErrorAndClose(RSocketErrorException errorException) { + source.sendErrorAndClose(errorException); + } + + boolean receivedFirst; + + @Override + public Flux receive() { + return source + .receive() + .doOnNext( + bb -> { + if (!receivedFirst) { + receivedFirst = true; + Mono.delay(delay) + .subscribe( + __ -> { + logger.warn("Tag {}. Disposing Connection", tag); + source.dispose(); + }); + } + }); + } + + @Override + public ByteBufAllocator alloc() { + return source.alloc(); + } + + @Override + public SocketAddress remoteAddress() { + return source.remoteAddress(); + } + } + private static class ByteBufReleaserOperator implements CoreSubscriber, Subscription, Fuseable.QueueSubscription { - final CoreSubscriber actual; + CoreSubscriber actual; + final MonoProcessor closeableMono; Subscription s; - public ByteBufReleaserOperator(CoreSubscriber actual) { - this.actual = actual; + public ByteBufReleaserOperator() { + this.closeableMono = MonoProcessor.create(); } @Override @@ -610,14 +757,20 @@ public void onNext(ByteBuf buf) { buf.release(); } + Mono onClose() { + return closeableMono; + } + @Override public void onError(Throwable t) { actual.onError(t); + closeableMono.onError(t); } @Override public void onComplete() { actual.onComplete(); + closeableMono.onComplete(); } @Override @@ -628,6 +781,7 @@ public void request(long n) { @Override public void cancel() { s.cancel(); + closeableMono.onComplete(); } @Override 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 40d09f4aa..00a133969 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 @@ -19,10 +19,11 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; +import io.rsocket.RSocketErrorException; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.internal.UnboundedProcessor; import java.net.SocketAddress; import java.util.Objects; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; @@ -40,7 +41,7 @@ final class LocalDuplexConnection implements DuplexConnection { private final MonoProcessor onClose; - private final Subscriber out; + private final UnboundedProcessor out; /** * Creates a new instance. @@ -55,7 +56,7 @@ final class LocalDuplexConnection implements DuplexConnection { String name, ByteBufAllocator allocator, Flux in, - Subscriber out, + UnboundedProcessor out, MonoProcessor onClose) { this.address = new LocalSocketAddress(name); this.allocator = Objects.requireNonNull(allocator, "allocator must not be null"); @@ -87,17 +88,19 @@ public Flux receive() { } @Override - public Mono send(Publisher frames) { - Objects.requireNonNull(frames, "frames must not be null"); - - return Flux.from(frames).doOnNext(out::onNext).then(); + public void sendFrame(int streamId, ByteBuf frame) { + if (streamId == 0) { + out.onNextPrioritized(frame); + } else { + out.onNext(frame); + } } @Override - public Mono sendOne(ByteBuf frame) { - Objects.requireNonNull(frame, "frame must not be null"); - out.onNext(frame); - return Mono.empty(); + public void sendErrorAndClose(RSocketErrorException e) { + final ByteBuf errorFrame = ErrorFrameCodec.encode(allocator, 0, e); + out.onNext(errorFrame); + dispose(); } @Override 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 new file mode 100644 index 000000000..57ef63402 --- /dev/null +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.transport.local; + +import io.rsocket.test.TransportTest; +import java.time.Duration; +import java.util.UUID; +import org.junit.jupiter.api.Disabled; + +@Disabled +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); + + @Override + public Duration getTimeout() { + return Duration.ofSeconds(10); + } + + @Override + public TransportPair getTransportPair() { + return transportPair; + } +} 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 0dc766e6f..c57ebe59c 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java @@ -19,13 +19,13 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; +import io.rsocket.RSocketErrorException; +import io.rsocket.frame.ErrorFrameCodec; import io.rsocket.frame.FrameLengthCodec; import io.rsocket.internal.BaseDuplexConnection; import java.net.SocketAddress; import java.util.Objects; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import reactor.netty.Connection; /** An implementation of {@link DuplexConnection} that connects via TCP. */ @@ -48,6 +48,8 @@ public TcpDuplexConnection(Connection connection) { future -> { if (!isDisposed()) dispose(); }); + + connection.outbound().send(sender).then().subscribe(); } @Override @@ -62,25 +64,37 @@ public SocketAddress remoteAddress() { @Override protected void doOnClose() { - if (!connection.isDisposed()) { - connection.dispose(); - } + sender.dispose(); + connection.dispose(); } @Override - public Flux receive() { - return connection.inbound().receive().map(FrameLengthCodec::frame); + public void sendErrorAndClose(RSocketErrorException e) { + final ByteBuf errorFrame = ErrorFrameCodec.encode(alloc(), 0, e); + connection + .outbound() + .sendObject(FrameLengthCodec.encode(alloc(), errorFrame.readableBytes(), errorFrame)) + .then() + .subscribe( + null, + t -> onClose.onError(t), + () -> { + final Throwable cause = e.getCause(); + if (cause == null) { + onClose.onComplete(); + } else { + onClose.onError(cause); + } + }); } @Override - public Mono send(Publisher frames) { - if (frames instanceof Mono) { - return connection.outbound().sendObject(((Mono) frames).map(this::encode)).then(); - } - return connection.outbound().send(Flux.from(frames).map(this::encode)).then(); + public Flux receive() { + return connection.inbound().receive().map(FrameLengthCodec::frame); } - private ByteBuf encode(ByteBuf frame) { - return FrameLengthCodec.encode(alloc(), frame.readableBytes(), frame); + @Override + public void sendFrame(int streamId, ByteBuf frame) { + super.sendFrame(streamId, FrameLengthCodec.encode(alloc(), frame.readableBytes(), frame)); } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java index 208e78905..b6d542dcb 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 @@ -19,12 +19,12 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.rsocket.DuplexConnection; +import io.rsocket.RSocketErrorException; +import io.rsocket.frame.ErrorFrameCodec; import io.rsocket.internal.BaseDuplexConnection; import java.net.SocketAddress; import java.util.Objects; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import reactor.netty.Connection; /** @@ -53,6 +53,8 @@ public WebsocketDuplexConnection(Connection connection) { future -> { if (!isDisposed()) dispose(); }); + + connection.outbound().sendObject(sender.map(BinaryWebSocketFrame::new)).then().subscribe(); } @Override @@ -67,9 +69,8 @@ public SocketAddress remoteAddress() { @Override protected void doOnClose() { - if (!connection.isDisposed()) { - connection.dispose(); - } + sender.dispose(); + connection.dispose(); } @Override @@ -78,16 +79,22 @@ public Flux receive() { } @Override - public Mono send(Publisher frames) { - if (frames instanceof Mono) { - return connection - .outbound() - .sendObject(((Mono) frames).map(BinaryWebSocketFrame::new)) - .then(); - } - return connection + public void sendErrorAndClose(RSocketErrorException e) { + final ByteBuf errorFrame = ErrorFrameCodec.encode(alloc(), 0, e); + connection .outbound() - .sendObject(Flux.from(frames).map(BinaryWebSocketFrame::new)) - .then(); + .sendObject(new BinaryWebSocketFrame(errorFrame)) + .then() + .subscribe( + null, + t -> onClose.onError(t), + () -> { + final Throwable cause = e.getCause(); + if (cause == null) { + onClose.onComplete(); + } else { + onClose.onError(cause); + } + }); } } 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 new file mode 100644 index 000000000..18ef55d30 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableTransportTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.transport.netty; + +import io.netty.channel.ChannelOption; +import io.rsocket.test.TransportTest; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.TcpServerTransport; +import java.net.InetSocketAddress; +import java.time.Duration; +import org.junit.jupiter.api.Disabled; +import reactor.netty.tcp.TcpClient; +import reactor.netty.tcp.TcpServer; + +@Disabled +final class TcpResumableTransportTest implements TransportTest { + + private final TransportPair transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + TcpClientTransport.create( + TcpClient.create() + .remoteAddress(server::address) + .option(ChannelOption.ALLOCATOR, allocator)), + (address, allocator) -> + TcpServerTransport.create( + TcpServer.create() + .bindAddress(() -> address) + .option(ChannelOption.ALLOCATOR, allocator)), + false, + true); + + @Override + public Duration getTimeout() { + return Duration.ofMinutes(3); + } + + @Override + public TransportPair getTransportPair() { + return transportPair; + } +} From 2c0e76dd0e2f3512b5f8f4d521d07a9818bfedc8 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 14 Sep 2020 18:32:00 +0300 Subject: [PATCH 017/183] fixes integration test Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/core/SetupRejectionTest.java | 2 -- .../test/LeaksTrackingByteBufAllocator.java | 14 +++++++++++--- .../main/java/io/rsocket/test/TransportTest.java | 5 +++++ 3 files changed, 16 insertions(+), 5 deletions(-) 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 da0fb2364..b96139fb5 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -19,7 +19,6 @@ import io.rsocket.transport.ServerTransport; import io.rsocket.util.DefaultPayload; import java.time.Duration; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.core.publisher.UnicastProcessor; @@ -47,7 +46,6 @@ void responderRejectSetup() { } @Test - @Disabled("FIXME: needs to be revised") void requesterStreamsTerminatedOnZeroErrorFrame() { LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); 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 2ca646936..0ddfb5449 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java +++ b/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java @@ -60,7 +60,8 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { final Duration awaitZeroRefCntDuration = this.awaitZeroRefCntDuration; if (!unreleased.isEmpty() && !awaitZeroRefCntDuration.isZero()) { - long endTimeInMillis = System.currentTimeMillis() + awaitZeroRefCntDuration.toMillis(); + final long startTime = System.currentTimeMillis(); + final long endTimeInMillis = startTime + awaitZeroRefCntDuration.toMillis(); boolean hasUnreleased; while (System.currentTimeMillis() <= endTimeInMillis) { hasUnreleased = false; @@ -72,14 +73,21 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { } if (!hasUnreleased) { - break; + System.out.println("all the buffers are released..."); + return this; } - parkNanos(100); + System.out.println("await buffers to be released"); + for (int i = 0; i < 100; i++) { + System.gc(); + parkNanos(1000); + System.gc(); + } } } Assertions.assertThat(unreleased).allMatch(bb -> bb.refCnt() == 0); + System.out.println("all the buffers are released..."); } finally { tracker.clear(); } 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 48472dec9..4f316b02d 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -101,6 +101,7 @@ default void setUp() { default void close() { getTransportPair().responder.awaitAllInteractionTermination(getTimeout()); getTransportPair().dispose(); + getTransportPair().awaitClosed(); getTransportPair().byteBufAllocator.assertHasNoLeaks(); Hooks.resetOnOperatorDebug(); } @@ -611,6 +612,10 @@ public String expectedPayloadMetadata() { return metadata; } + public void awaitClosed() { + server.onClose().and(client.onClose()).block(Duration.ofMinutes(1)); + } + private static class AsyncDuplexConnection implements DuplexConnection { private final DuplexConnection duplexConnection; From 9a4d5ab6558f5681957ba9dd1216dbd9378c599d Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 14 Sep 2020 20:15:33 +0300 Subject: [PATCH 018/183] updates dependencies versions Signed-off-by: Oleh Dokuka --- build.gradle | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 3c63cf58b..c2642cdac 100644 --- a/build.gradle +++ b/build.gradle @@ -32,11 +32,10 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = '2020.0.0-M2' + ext['reactor-bom.version'] = '2020.0.0-RC1' ext['logback.version'] = '1.2.3' - ext['findbugs.version'] = '3.0.2' - ext['netty-bom.version'] = '4.1.50.Final' - ext['netty-boringssl.version'] = '2.0.30.Final' + ext['netty-bom.version'] = '4.1.52.Final' + ext['netty-boringssl.version'] = '2.0.34.Final' ext['hdrhistogram.version'] = '2.1.10' ext['mockito.version'] = '3.2.0' ext['slf4j.version'] = '1.7.25' From 18050edda0dff73682d6643e8aca9c5a89ffbd51 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 22 Sep 2020 16:08:36 +0100 Subject: [PATCH 019/183] Avoid queueing in UnicastProcessor receivers Closes gh-887 Signed-off-by: Rossen Stoyanchev --- .../src/main/java/io/rsocket/core/RSocketRequester.java | 6 +++--- .../src/main/java/io/rsocket/core/RSocketResponder.java | 5 +++-- rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java | 6 ++++-- .../test/java/io/rsocket/integration/TestingStreaming.java | 3 +++ 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 2ecdec215..272194bb2 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-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. @@ -348,7 +348,7 @@ private Flux handleRequestStream(final Payload payload) { } final UnboundedProcessor sendProcessor = this.sendProcessor; - final UnicastProcessor receiver = UnicastProcessor.create(); + final UnicastProcessor receiver = UnicastProcessor.create(Queues.one().get()); final AtomicBoolean once = new AtomicBoolean(); return Flux.defer( @@ -456,7 +456,7 @@ private Flux handleChannel(Flux request) { private Flux handleChannel(Payload initialPayload, Flux inboundFlux) { final UnboundedProcessor sendProcessor = this.sendProcessor; - final UnicastProcessor receiver = UnicastProcessor.create(); + final UnicastProcessor receiver = UnicastProcessor.create(Queues.one().get()); return receiver .transform( 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 581605ff4..3e2c06e92 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-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. @@ -48,6 +48,7 @@ import reactor.core.Exceptions; import reactor.core.publisher.*; import reactor.util.annotation.Nullable; +import reactor.util.concurrent.Queues; /** Responder side of RSocket. Receives {@link ByteBuf}s from a peer's {@link RSocketRequester} */ class RSocketResponder implements RSocket { @@ -537,7 +538,7 @@ protected void hookOnError(Throwable throwable) { } private void handleChannel(int streamId, Payload payload, long initialRequestN) { - UnicastProcessor frames = UnicastProcessor.create(); + UnicastProcessor frames = UnicastProcessor.create(Queues.one().get()); channelProcessors.put(streamId, frames); Flux payloads = 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 1e7bb337f..d3e614e1c 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -158,13 +158,13 @@ public Flux requestChannel(Publisher payloads) { } @Test(timeout = 2000) - public void testStream() throws Exception { + public void testStream() { Flux responses = rule.crs.requestStream(DefaultPayload.create("Payload In")); StepVerifier.create(responses).expectNextCount(10).expectComplete().verify(); } @Test(timeout = 2000) - public void testChannel() throws Exception { + public void testChannel() { Flux requests = Flux.range(0, 10).map(i -> DefaultPayload.create("streaming in -> " + i)); Flux responses = rule.crs.requestChannel(requests); @@ -543,6 +543,7 @@ public Mono requestResponse(Payload payload) { @Override public Flux requestStream(Payload payload) { return Flux.range(1, 10) + .delaySubscription(Duration.ofMillis(100)) .map( i -> DefaultPayload.create("server got -> [" + payload.toString() + "]")); } @@ -556,6 +557,7 @@ public Flux requestChannel(Publisher payloads) { .subscribe(); return Flux.range(1, 10) + .delaySubscription(Duration.ofMillis(100)) .map( payload -> DefaultPayload.create("server got -> [" + payload.toString() + "]")); diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java index 7d34ba478..f583355e6 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java @@ -49,6 +49,7 @@ public void testRangeButThrowException() { } }) .map(l -> DefaultPayload.create("l -> " + l)) + .delaySubscription(Duration.ofMillis(100)) .cast(Payload.class))) .bind(serverTransport) .block(); @@ -71,6 +72,7 @@ public void testRangeOfConsumers() { payload -> Flux.range(1, 1000) .map(l -> DefaultPayload.create("l -> " + l)) + .delaySubscription(Duration.ofMillis(100)) .cast(Payload.class))) .bind(serverTransport) .block(); @@ -104,6 +106,7 @@ public void testSingleConsumer() { payload -> Flux.range(1, 10_000) .map(l -> DefaultPayload.create("l -> " + l)) + .delaySubscription(Duration.ofMillis(100)) .cast(Payload.class))) .bind(serverTransport) .block(); From 38a38e93ad06a8286788d70d7636a63f7ff5de4f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 23 Sep 2020 17:42:41 +0100 Subject: [PATCH 020/183] Refactoring in RequestOperator --- .../io/rsocket/core/RSocketRequester.java | 477 ++++++++---------- .../java/io/rsocket/core/RequestOperator.java | 36 +- .../java/io/rsocket/core/RSocketTest.java | 15 +- .../rsocket/integration/TestingStreaming.java | 3 - 4 files changed, 260 insertions(+), 271 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 272194bb2..a249ea888 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -64,7 +64,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; -import reactor.core.publisher.Operators; import reactor.core.publisher.SignalType; import reactor.core.publisher.UnicastProcessor; import reactor.core.scheduler.Scheduler; @@ -267,68 +266,54 @@ private Mono handleRequestResponse(final Payload payload) { final UnboundedProcessor sendProcessor = this.sendProcessor; final UnicastProcessor receiver = UnicastProcessor.create(Queues.one().get()); - final AtomicBoolean once = new AtomicBoolean(); + return Mono.fromDirect( + new RequestOperator( + receiver.next(), "RequestResponseMono allows only a single subscriber") { - return Mono.defer( - () -> { - if (once.getAndSet(true)) { - return Mono.error( - new IllegalStateException("RequestResponseMono allows only a single subscriber")); - } + @Override + void hookOnFirstRequest(long n) { + if (isDisposed()) { + payload.release(); + final Throwable t = terminationError; + receiver.onError(t); + return; + } - return receiver - .next() - .transform( - Operators.lift( - (s, actual) -> - new RequestOperator(actual) { - - @Override - void hookOnFirstRequest(long n) { - if (isDisposed()) { - payload.release(); - final Throwable t = terminationError; - receiver.onError(t); - return; - } - - RequesterLeaseHandler lh = leaseHandler; - if (!lh.useLease()) { - payload.release(); - receiver.onError(lh.leaseError()); - return; - } - - int streamId = streamIdSupplier.nextStreamId(receivers); - this.streamId = streamId; - - ByteBuf requestResponseFrame = - RequestResponseFrameCodec.encodeReleasingPayload( - allocator, streamId, payload); - - receivers.put(streamId, receiver); - sendProcessor.onNext(requestResponseFrame); - } - - @Override - void hookOnCancel() { - if (receivers.remove(streamId, receiver)) { - sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); - } else { - if (this.firstRequest) { - payload.release(); - } - } - } - - @Override - public void hookOnTerminal(SignalType signalType) { - receivers.remove(streamId, receiver); - } - })) - .subscribeOn(serialScheduler) - .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); - }); + RequesterLeaseHandler lh = leaseHandler; + if (!lh.useLease()) { + payload.release(); + receiver.onError(lh.leaseError()); + return; + } + + int streamId = streamIdSupplier.nextStreamId(receivers); + this.streamId = streamId; + + ByteBuf requestResponseFrame = + RequestResponseFrameCodec.encodeReleasingPayload(allocator, streamId, payload); + + receivers.put(streamId, receiver); + sendProcessor.onNext(requestResponseFrame); + } + + @Override + void hookOnCancel() { + if (receivers.remove(streamId, receiver)) { + sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); + } else { + if (this.firstRequest) { + payload.release(); + } + } + } + + @Override + public void hookOnTerminal(SignalType signalType) { + receivers.remove(streamId, receiver); + } + }) + .subscribeOn(serialScheduler) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); } private Flux handleRequestStream(final Payload payload) { @@ -349,78 +334,64 @@ private Flux handleRequestStream(final Payload payload) { final UnboundedProcessor sendProcessor = this.sendProcessor; final UnicastProcessor receiver = UnicastProcessor.create(Queues.one().get()); - final AtomicBoolean once = new AtomicBoolean(); - return Flux.defer( - () -> { - if (once.getAndSet(true)) { - return Flux.error( - new IllegalStateException("RequestStreamFlux allows only a single subscriber")); - } + return Flux.from( + new RequestOperator(receiver, "RequestStreamFlux allows only a single subscriber") { - return receiver - .transform( - Operators.lift( - (s, actual) -> - new RequestOperator(actual) { - - @Override - void hookOnFirstRequest(long n) { - if (isDisposed()) { - payload.release(); - final Throwable t = terminationError; - receiver.onError(t); - return; - } - - RequesterLeaseHandler lh = leaseHandler; - if (!lh.useLease()) { - payload.release(); - receiver.onError(lh.leaseError()); - return; - } - - int streamId = streamIdSupplier.nextStreamId(receivers); - this.streamId = streamId; - - ByteBuf requestStreamFrame = - RequestStreamFrameCodec.encodeReleasingPayload( - allocator, streamId, n, payload); - - receivers.put(streamId, receiver); - - sendProcessor.onNext(requestStreamFrame); - } - - @Override - void hookOnRemainingRequests(long n) { - if (receiver.isDisposed()) { - return; - } - - sendProcessor.onNext( - RequestNFrameCodec.encode(allocator, streamId, n)); - } - - @Override - void hookOnCancel() { - if (receivers.remove(streamId, receiver)) { - sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); - } else { - if (this.firstRequest) { - payload.release(); - } - } - } - - @Override - void hookOnTerminal(SignalType signalType) { - receivers.remove(streamId); - } - })) - .subscribeOn(serialScheduler, false) - .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); - }); + @Override + void hookOnFirstRequest(long n) { + if (isDisposed()) { + payload.release(); + final Throwable t = terminationError; + receiver.onError(t); + return; + } + + RequesterLeaseHandler lh = leaseHandler; + if (!lh.useLease()) { + payload.release(); + receiver.onError(lh.leaseError()); + return; + } + + int streamId = streamIdSupplier.nextStreamId(receivers); + this.streamId = streamId; + + ByteBuf requestStreamFrame = + RequestStreamFrameCodec.encodeReleasingPayload(allocator, streamId, n, payload); + + receivers.put(streamId, receiver); + + sendProcessor.onNext(requestStreamFrame); + } + + @Override + void hookOnRemainingRequests(long n) { + if (receiver.isDisposed()) { + return; + } + + sendProcessor.onNext(RequestNFrameCodec.encode(allocator, streamId, n)); + } + + @Override + void hookOnCancel() { + if (receivers.remove(streamId, receiver)) { + sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); + } else { + if (this.firstRequest) { + payload.release(); + } + } + } + + @Override + void hookOnTerminal(SignalType signalType) { + receivers.remove(streamId); + } + }) + .subscribeOn(serialScheduler, false) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); } private Flux handleChannel(Flux request) { @@ -458,135 +429,133 @@ private Flux handleChannel(Payload initialPayload, Flux receiver = UnicastProcessor.create(Queues.one().get()); - return receiver - .transform( - Operators.lift( - (s, actual) -> - new RequestOperator(actual) { - - final BaseSubscriber upstreamSubscriber = - new BaseSubscriber() { - - boolean first = true; - - @Override - protected void hookOnSubscribe(Subscription subscription) { - // noops - } - - @Override - protected void hookOnNext(Payload payload) { - if (first) { - // need to skip first since we have already sent it - // no need to release it since it was released earlier on the - // request - // establishment - // phase - first = false; - request(1); - return; - } - if (!PayloadValidationUtils.isValid(mtu, payload, maxFrameLength)) { - payload.release(); - cancel(); - final IllegalArgumentException t = - new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); - // no need to send any errors. - sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); - receiver.onError(t); - return; - } - final ByteBuf frame = - PayloadFrameCodec.encodeNextReleasingPayload( - allocator, streamId, payload); - - sendProcessor.onNext(frame); - } - - @Override - protected void hookOnComplete() { - ByteBuf frame = PayloadFrameCodec.encodeComplete(allocator, streamId); - sendProcessor.onNext(frame); - } - - @Override - protected void hookOnError(Throwable t) { - ByteBuf frame = ErrorFrameCodec.encode(allocator, streamId, t); - sendProcessor.onNext(frame); - receiver.onError(t); - } - - @Override - protected void hookFinally(SignalType type) { - senders.remove(streamId, this); - } - }; - - @Override - void hookOnFirstRequest(long n) { - if (isDisposed()) { - initialPayload.release(); - final Throwable t = terminationError; - upstreamSubscriber.cancel(); - receiver.onError(t); - return; - } - - RequesterLeaseHandler lh = leaseHandler; - if (!lh.useLease()) { - initialPayload.release(); - receiver.onError(lh.leaseError()); - return; - } - - final int streamId = streamIdSupplier.nextStreamId(receivers); - this.streamId = streamId; - - final ByteBuf frame = - RequestChannelFrameCodec.encodeReleasingPayload( - allocator, streamId, false, n, initialPayload); - - senders.put(streamId, upstreamSubscriber); - receivers.put(streamId, receiver); - - inboundFlux - .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER) - .subscribe(upstreamSubscriber); - - sendProcessor.onNext(frame); + return Flux.from( + new RequestOperator( + receiver, "RequestStreamFlux allows only a " + "single subscriber") { + + final BaseSubscriber upstreamSubscriber = + new BaseSubscriber() { + + boolean first = true; + + @Override + protected void hookOnSubscribe(Subscription subscription) { + // noops + } + + @Override + protected void hookOnNext(Payload payload) { + if (first) { + // need to skip first since we have already sent it + // no need to release it since it was released earlier on the + // request + // establishment + // phase + first = false; + request(1); + return; + } + if (!PayloadValidationUtils.isValid(mtu, payload, maxFrameLength)) { + payload.release(); + cancel(); + final IllegalArgumentException t = + new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); + // no need to send any errors. + sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); + receiver.onError(t); + return; } + final ByteBuf frame = + PayloadFrameCodec.encodeNextReleasingPayload( + allocator, streamId, payload); + + sendProcessor.onNext(frame); + } + + @Override + protected void hookOnComplete() { + ByteBuf frame = PayloadFrameCodec.encodeComplete(allocator, streamId); + sendProcessor.onNext(frame); + } + + @Override + protected void hookOnError(Throwable t) { + ByteBuf frame = ErrorFrameCodec.encode(allocator, streamId, t); + sendProcessor.onNext(frame); + receiver.onError(t); + } + + @Override + protected void hookFinally(SignalType type) { + senders.remove(streamId, this); + } + }; + + @Override + void hookOnFirstRequest(long n) { + if (isDisposed()) { + initialPayload.release(); + final Throwable t = terminationError; + upstreamSubscriber.cancel(); + receiver.onError(t); + return; + } - @Override - void hookOnRemainingRequests(long n) { - if (receiver.isDisposed()) { - return; - } + RequesterLeaseHandler lh = leaseHandler; + if (!lh.useLease()) { + initialPayload.release(); + receiver.onError(lh.leaseError()); + return; + } - sendProcessor.onNext(RequestNFrameCodec.encode(allocator, streamId, n)); - } + final int streamId = streamIdSupplier.nextStreamId(receivers); + this.streamId = streamId; - @Override - void hookOnCancel() { - senders.remove(streamId, upstreamSubscriber); - if (receivers.remove(streamId, receiver)) { - sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); - } - } + final ByteBuf frame = + RequestChannelFrameCodec.encodeReleasingPayload( + allocator, streamId, false, n, initialPayload); - @Override - void hookOnTerminal(SignalType signalType) { - if (signalType == SignalType.ON_ERROR) { - upstreamSubscriber.cancel(); - } - receivers.remove(streamId, receiver); - } + senders.put(streamId, upstreamSubscriber); + receivers.put(streamId, receiver); - @Override - public void cancel() { - upstreamSubscriber.cancel(); - super.cancel(); - } - })) + inboundFlux + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER) + .subscribe(upstreamSubscriber); + + sendProcessor.onNext(frame); + } + + @Override + void hookOnRemainingRequests(long n) { + if (receiver.isDisposed()) { + return; + } + + sendProcessor.onNext(RequestNFrameCodec.encode(allocator, streamId, n)); + } + + @Override + void hookOnCancel() { + senders.remove(streamId, upstreamSubscriber); + if (receivers.remove(streamId, receiver)) { + sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); + } + } + + @Override + void hookOnTerminal(SignalType signalType) { + if (signalType == SignalType.ON_ERROR) { + upstreamSubscriber.cancel(); + } + receivers.remove(streamId, receiver); + } + + @Override + public void cancel() { + upstreamSubscriber.cancel(); + super.cancel(); + } + }) .subscribeOn(serialScheduler, false); } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java b/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java index 6123b0492..dbca5fef2 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java @@ -1,14 +1,17 @@ package io.rsocket.core; import io.rsocket.Payload; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import reactor.core.CorePublisher; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.core.publisher.Operators; import reactor.core.publisher.SignalType; import reactor.util.context.Context; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + /** * This is a support class for handling of request input, intended for use with {@link * Operators#lift}. It ensures serial execution of cancellation vs first request signals and also @@ -16,9 +19,14 @@ * invocations. */ abstract class RequestOperator - implements CoreSubscriber, Fuseable.QueueSubscription { + implements CoreSubscriber, + CorePublisher, + Fuseable.QueueSubscription, + Fuseable { - final CoreSubscriber actual; + final String errorMessageOnSecondSubscription; + + CoreSubscriber actual; Subscription s; Fuseable.QueueSubscription qs; @@ -30,8 +38,25 @@ abstract class RequestOperator static final AtomicIntegerFieldUpdater WIP = AtomicIntegerFieldUpdater.newUpdater(RequestOperator.class, "wip"); - RequestOperator(CoreSubscriber actual) { - this.actual = actual; + RequestOperator(CorePublisher source, String errorMessageOnSecondSubscription) { + this.errorMessageOnSecondSubscription = errorMessageOnSecondSubscription; + source.subscribe(this); + WIP.lazySet(this, -1); + } + + @Override + public void subscribe(Subscriber actual) { + subscribe(Operators.toCoreSubscriber(actual)); + } + + @Override + public void subscribe(CoreSubscriber actual) { + if (this.wip == -1 && WIP.compareAndSet(this, -1, 0)) { + this.actual = actual; + actual.onSubscribe(this); + } else { + Operators.error(actual, new IllegalStateException(this.errorMessageOnSecondSubscription)); + } } /** @@ -129,7 +154,6 @@ public void onSubscribe(Subscription s) { if (s instanceof Fuseable.QueueSubscription) { this.qs = (Fuseable.QueueSubscription) s; } - this.actual.onSubscribe(this); } } 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 d3e614e1c..d78a1d032 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -16,8 +16,6 @@ package io.rsocket.core; -import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; - import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.Payload; @@ -33,10 +31,6 @@ import io.rsocket.test.util.LocalDuplexConnection; import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CancellationException; -import java.util.concurrent.atomic.AtomicReference; import org.assertj.core.api.Assertions; import org.junit.Rule; import org.junit.Test; @@ -52,6 +46,13 @@ import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicReference; + +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + public class RSocketTest { @Rule public final SocketRule rule = new SocketRule(); @@ -543,7 +544,6 @@ public Mono requestResponse(Payload payload) { @Override public Flux requestStream(Payload payload) { return Flux.range(1, 10) - .delaySubscription(Duration.ofMillis(100)) .map( i -> DefaultPayload.create("server got -> [" + payload.toString() + "]")); } @@ -557,7 +557,6 @@ public Flux requestChannel(Publisher payloads) { .subscribe(); return Flux.range(1, 10) - .delaySubscription(Duration.ofMillis(100)) .map( payload -> DefaultPayload.create("server got -> [" + payload.toString() + "]")); 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 f583355e6..7d34ba478 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java @@ -49,7 +49,6 @@ public void testRangeButThrowException() { } }) .map(l -> DefaultPayload.create("l -> " + l)) - .delaySubscription(Duration.ofMillis(100)) .cast(Payload.class))) .bind(serverTransport) .block(); @@ -72,7 +71,6 @@ public void testRangeOfConsumers() { payload -> Flux.range(1, 1000) .map(l -> DefaultPayload.create("l -> " + l)) - .delaySubscription(Duration.ofMillis(100)) .cast(Payload.class))) .bind(serverTransport) .block(); @@ -106,7 +104,6 @@ public void testSingleConsumer() { payload -> Flux.range(1, 10_000) .map(l -> DefaultPayload.create("l -> " + l)) - .delaySubscription(Duration.ofMillis(100)) .cast(Payload.class))) .bind(serverTransport) .block(); From b5952f6cebf9ba61c7ae0bc4afd1fc47624be836 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 23 Sep 2020 19:43:27 +0100 Subject: [PATCH 021/183] Fix formatting --- .../main/java/io/rsocket/core/RequestOperator.java | 3 +-- .../src/test/java/io/rsocket/core/RSocketTest.java | 13 ++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java b/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java index dbca5fef2..f95a5f66c 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java @@ -1,6 +1,7 @@ package io.rsocket.core; import io.rsocket.Payload; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.CorePublisher; @@ -10,8 +11,6 @@ import reactor.core.publisher.SignalType; import reactor.util.context.Context; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; - /** * This is a support class for handling of request input, intended for use with {@link * Operators#lift}. It ensures serial execution of cancellation vs first request signals and also 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 d78a1d032..7320d9ade 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -16,6 +16,8 @@ package io.rsocket.core; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.Payload; @@ -31,6 +33,10 @@ import io.rsocket.test.util.LocalDuplexConnection; import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicReference; import org.assertj.core.api.Assertions; import org.junit.Rule; import org.junit.Test; @@ -46,13 +52,6 @@ import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CancellationException; -import java.util.concurrent.atomic.AtomicReference; - -import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; - public class RSocketTest { @Rule public final SocketRule rule = new SocketRule(); From b330634d836fbfbf65cb671c9ee11be40ec31700 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 22 Sep 2020 18:18:09 +0100 Subject: [PATCH 022/183] Turn off suppressed exceptions (to squash) Signed-off-by: Rossen Stoyanchev Use static errors in default RSocket methods Closes gh-865 Signed-off-by: Rossen Stoyanchev --- .../src/main/java/io/rsocket/RSocket.java | 16 ++-- .../main/java/io/rsocket/RSocketAdapter.java | 78 +++++++++++++++++++ 2 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/RSocketAdapter.java diff --git a/rsocket-core/src/main/java/io/rsocket/RSocket.java b/rsocket-core/src/main/java/io/rsocket/RSocket.java index 773c93dc2..b05241365 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocket.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,8 +34,7 @@ public interface RSocket extends Availability, Closeable { * handled, otherwise errors. */ default Mono fireAndForget(Payload payload) { - payload.release(); - return Mono.error(new UnsupportedOperationException("Fire-and-Forget not implemented.")); + return RSocketAdapter.fireAndForget(payload); } /** @@ -46,8 +45,7 @@ default Mono fireAndForget(Payload payload) { * response. */ default Mono requestResponse(Payload payload) { - payload.release(); - return Mono.error(new UnsupportedOperationException("Request-Response not implemented.")); + return RSocketAdapter.requestResponse(payload); } /** @@ -57,8 +55,7 @@ default Mono requestResponse(Payload payload) { * @return {@code Publisher} containing the stream of {@code Payload}s representing the response. */ default Flux requestStream(Payload payload) { - payload.release(); - return Flux.error(new UnsupportedOperationException("Request-Stream not implemented.")); + return RSocketAdapter.requestStream(payload); } /** @@ -68,7 +65,7 @@ default Flux requestStream(Payload payload) { * @return Stream of response payloads. */ default Flux requestChannel(Publisher payloads) { - return Flux.error(new UnsupportedOperationException("Request-Channel not implemented.")); + return RSocketAdapter.requestChannel(payloads); } /** @@ -79,8 +76,7 @@ default Flux requestChannel(Publisher payloads) { * handled, otherwise errors. */ default Mono metadataPush(Payload payload) { - payload.release(); - return Mono.error(new UnsupportedOperationException("Metadata-Push not implemented.")); + return RSocketAdapter.metadataPush(payload); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketAdapter.java b/rsocket-core/src/main/java/io/rsocket/RSocketAdapter.java new file mode 100644 index 000000000..b5a64b8dd --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/RSocketAdapter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Package private class with default implementations for use in {@link RSocket}. The main purpose + * is to hide static {@link UnsupportedOperationException} declarations. + * + * @since 1.0.3 + */ +class RSocketAdapter { + + private static final Mono UNSUPPORTED_FIRE_AND_FORGET = + Mono.error(new UnsupportedInteractionException("Fire-and-Forget")); + + private static final Mono UNSUPPORTED_REQUEST_RESPONSE = + Mono.error(new UnsupportedInteractionException("Request-Response")); + + private static final Flux UNSUPPORTED_REQUEST_STREAM = + Flux.error(new UnsupportedInteractionException("Request-Stream")); + + private static final Flux UNSUPPORTED_REQUEST_CHANNEL = + Flux.error(new UnsupportedInteractionException("Request-Channel")); + + private static final Mono UNSUPPORTED_METADATA_PUSH = + Mono.error(new UnsupportedInteractionException("Metadata-Push")); + + static Mono fireAndForget(Payload payload) { + payload.release(); + return RSocketAdapter.UNSUPPORTED_FIRE_AND_FORGET; + } + + static Mono requestResponse(Payload payload) { + payload.release(); + return RSocketAdapter.UNSUPPORTED_REQUEST_RESPONSE; + } + + static Flux requestStream(Payload payload) { + payload.release(); + return RSocketAdapter.UNSUPPORTED_REQUEST_STREAM; + } + + static Flux requestChannel(Publisher payloads) { + return RSocketAdapter.UNSUPPORTED_REQUEST_CHANNEL; + } + + static Mono metadataPush(Payload payload) { + payload.release(); + return RSocketAdapter.UNSUPPORTED_METADATA_PUSH; + } + + private static class UnsupportedInteractionException extends RuntimeException { + + private static final long serialVersionUID = 5084623297446471999L; + + UnsupportedInteractionException(String interactionName) { + super(interactionName + " not implemented.", null, false, false); + } + } +} From fdf7fb01392d2faa4bb2fb34f3a09d270783648d Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 24 Sep 2020 15:44:26 +0100 Subject: [PATCH 023/183] Update Netty and Reactor versions Signed-off-by: Rossen Stoyanchev --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index a576f22f8..b5910fb86 100644 --- a/build.gradle +++ b/build.gradle @@ -32,10 +32,10 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = 'Dysprosium-SR11' + ext['reactor-bom.version'] = 'Dysprosium-SR12' ext['logback.version'] = '1.2.3' - ext['netty-bom.version'] = '4.1.51.Final' - ext['netty-boringssl.version'] = '2.0.31.Final' + ext['netty-bom.version'] = '4.1.52.Final' + ext['netty-boringssl.version'] = '2.0.34.Final' ext['hdrhistogram.version'] = '2.1.10' ext['mockito.version'] = '3.2.0' ext['slf4j.version'] = '1.7.25' From 29638da7e7ce36faf2248eddf4dede795ff09b67 Mon Sep 17 00:00:00 2001 From: Jakob Skov Date: Sat, 26 Sep 2020 15:29:55 +0200 Subject: [PATCH 024/183] migrates from Reactor UnicastProcessor to new Sinks API (#931) Co-authored-by: jakobs --- ...ingWithServerSideNotificationsExample.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java index cf68dcdde..89f9950c7 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java @@ -35,7 +35,7 @@ import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.UnicastProcessor; +import reactor.core.publisher.Sinks; import reactor.util.concurrent.Queues; /** @@ -48,12 +48,12 @@ public class TaskProcessingWithServerSideNotificationsExample { public static void main(String[] args) throws InterruptedException { - UnicastProcessor tasksProcessor = - UnicastProcessor.create(Queues.unboundedMultiproducer().get()); + Sinks.Many tasksProcessor = + Sinks.many().unicast().onBackpressureBuffer(Queues.unboundedMultiproducer().get()); ConcurrentMap> idToCompletedTasksMap = new ConcurrentHashMap<>(); ConcurrentMap idToRSocketMap = new ConcurrentHashMap<>(); BackgroundWorker backgroundWorker = - new BackgroundWorker(tasksProcessor, idToCompletedTasksMap, idToRSocketMap); + new BackgroundWorker(tasksProcessor.asFlux(), idToCompletedTasksMap, idToRSocketMap); RSocketServer.create(new TasksAcceptor(tasksProcessor, idToCompletedTasksMap, idToRSocketMap)) .bindNow(TcpServerTransport.create(9991)); @@ -132,12 +132,12 @@ static class TasksAcceptor implements SocketAcceptor { static final Logger logger = LoggerFactory.getLogger(TasksAcceptor.class); - final UnicastProcessor tasksToProcess; + final Sinks.Many tasksToProcess; final ConcurrentMap> idToCompletedTasksMap; final ConcurrentMap idToRSocketMap; TasksAcceptor( - UnicastProcessor tasksToProcess, + Sinks.Many tasksToProcess, ConcurrentMap> idToCompletedTasksMap, ConcurrentMap idToRSocketMap) { this.tasksToProcess = tasksToProcess; @@ -197,11 +197,11 @@ private static class RSocketTaskHandler implements RSocket { private final String id; private final RSocket sendingSocket; private ConcurrentMap idToRSocketMap; - private UnicastProcessor tasksToProcess; + private Sinks.Many tasksToProcess; public RSocketTaskHandler( ConcurrentMap idToRSocketMap, - UnicastProcessor tasksToProcess, + Sinks.Many tasksToProcess, String id, RSocket sendingSocket) { this.id = id; @@ -213,9 +213,11 @@ public RSocketTaskHandler( @Override public Mono fireAndForget(Payload payload) { logger.info("Received a Task[{}] from Client.ID[{}]", payload.getDataUtf8(), id); - tasksToProcess.onNext(new Task(id, payload.getDataUtf8())); + Sinks.Emission emission = tasksToProcess.tryEmitNext(new Task(id, payload.getDataUtf8())); payload.release(); - return Mono.empty(); + return emission.hasFailed() + ? Mono.error(new Sinks.EmissionException(emission)) + : Mono.empty(); } @Override From 378251450ed1c2c803811137b4d7402dba7d3bec Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 21 Sep 2020 12:39:51 +0300 Subject: [PATCH 025/183] improves Resumability implementation Signed-off-by: Oleh Dokuka --- .../io/rsocket/core/RSocketConnector.java | 7 +- .../java/io/rsocket/core/ServerSetup.java | 5 +- .../rsocket/resume/ClientRSocketSession.java | 40 ++++---- .../resume/InMemoryResumableFramesStore.java | 36 ++++--- .../resume/ResumableDuplexConnection.java | 34 ++++++- .../rsocket/resume/ServerRSocketSession.java | 65 +++++++++---- .../test/LeaksTrackingByteBufAllocator.java | 94 +++++++++++++++++-- .../java/io/rsocket/test/TransportTest.java | 66 ++++++++----- .../local/LocalResumableTransportTest.java | 2 +- .../src/test/resources/logback-test.xml | 19 +++- .../netty/TcpResumableTransportTest.java | 2 - .../src/test/resources/logback-test.xml | 9 ++ 12 files changed, 287 insertions(+), 92 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index 4de9df1d1..0236adf47 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -583,15 +583,16 @@ public Mono connect(Supplier transportSupplier) { resume.getStoreFactory(CLIENT_TAG).apply(resumeToken); final ResumableDuplexConnection resumableDuplexConnection = new ResumableDuplexConnection( - clientServerConnection, resumableFramesStore); + CLIENT_TAG, + clientServerConnection, + resumableFramesStore); final ResumableClientSetup resumableClientSetup = new ResumableClientSetup(); final ClientRSocketSession session = new ClientRSocketSession( resumeToken, - clientServerConnection, resumableDuplexConnection, - connectionMono, + connectionMono, // supplies pure resumableClientSetup::init, resumableFramesStore, resume.getSessionDuration(), 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 25dae6084..1c2b2c7dc 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java @@ -109,7 +109,7 @@ public Mono acceptRSocketSetup( final ResumableFramesStore resumableFramesStore = resumeStoreFactory.apply(resumeToken); final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection(duplexConnection, resumableFramesStore); + new ResumableDuplexConnection("server", duplexConnection, resumableFramesStore); final ServerRSocketSession serverRSocketSession = new ServerRSocketSession( resumeToken, @@ -134,7 +134,8 @@ public Mono acceptRSocketSetup( public Mono acceptRSocketResume(ByteBuf frame, DuplexConnection duplexConnection) { ServerRSocketSession session = sessionManager.get(ResumeFrameCodec.token(frame)); if (session != null) { - return session.resumeWith(frame, duplexConnection); + session.resumeWith(frame, duplexConnection); + return duplexConnection.onClose(); } else { sendError(duplexConnection, new RejectedResumeException("unknown resume token")); return duplexConnection.onClose(); 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 f5463a6e9..cf7d793a6 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -63,7 +63,6 @@ public class ClientRSocketSession public ClientRSocketSession( ByteBuf resumeToken, - DuplexConnection initialDuplexConnection, ResumableDuplexConnection resumableDuplexConnection, Mono connectionFactory, Function>> connectionTransformer, @@ -80,7 +79,9 @@ public ClientRSocketSession( ResumeFrameCodec.encode( dc.alloc(), resumeToken.retain(), + // server uses this to release its cache resumableFramesStore.frameImpliedPosition(), // observed on the client side + // server uses this to check whether there is no mismatch resumableFramesStore.framePosition() // sent from the client sent )); logger.debug("Resume Frame has been sent"); @@ -95,25 +96,20 @@ public ClientRSocketSession( this.resumableConnection = resumableDuplexConnection; resumableDuplexConnection.onClose().doFinally(__ -> dispose()).subscribe(); - - observeDisconnection(initialDuplexConnection); + resumableDuplexConnection.onActiveConnectionClosed().subscribe(this::reconnect); S.lazySet(this, Operators.cancelledSubscription()); } - void reconnect() { + void reconnect(int index) { if (this.s == Operators.cancelledSubscription() && S.compareAndSet(this, Operators.cancelledSubscription(), null)) { keepAliveSupport.stop(); + logger.debug("Connection[" + index + "] is lost. Reconnecting..."); connectionFactory.retryWhen(retry).timeout(resumeSessionDuration).subscribe(this); - logger.debug("Connection is lost. Reconnecting..."); } } - void observeDisconnection(DuplexConnection activeConnection) { - activeConnection.onClose().subscribe(null, e -> reconnect(), () -> reconnect()); - } - @Override public long impliedPosition() { return resumableFramesStore.frameImpliedPosition(); @@ -132,9 +128,11 @@ public void onImpliedPosition(long remoteImpliedPos) { @Override public void dispose() { - if (Operators.terminate(S, this)) { - resumableFramesStore.dispose(); - resumableConnection.dispose(); + Operators.terminate(S, this); + resumableConnection.dispose(); + resumableFramesStore.dispose(); + + if (resumeToken.refCnt() > 0) { resumeToken.release(); } } @@ -152,8 +150,8 @@ public void onSubscribe(Subscription s) { } @Override - public synchronized void onNext(Tuple2 tuple2) { - ByteBuf frame = tuple2.getT1(); + public void onNext(Tuple2 tuple2) { + ByteBuf shouldBeResumeOKFrame = tuple2.getT1(); DuplexConnection nextDuplexConnection = tuple2.getT2(); if (!Operators.terminate(S, this)) { @@ -164,9 +162,7 @@ public synchronized void onNext(Tuple2 tuple2) { return; } - final FrameType frameType = FrameHeaderCodec.nativeFrameType(frame); - final int streamId = FrameHeaderCodec.streamId(frame); - + final int streamId = FrameHeaderCodec.streamId(shouldBeResumeOKFrame); if (streamId != 0) { logger.debug( "Illegal first frame received. RESUME_OK frame must be received before any others. Terminating received connection"); @@ -177,8 +173,13 @@ public synchronized void onNext(Tuple2 tuple2) { return; } + final FrameType frameType = FrameHeaderCodec.nativeFrameType(shouldBeResumeOKFrame); if (frameType == FrameType.RESUME_OK) { - long remoteImpliedPos = ResumeOkFrameCodec.lastReceivedClientPos(frame); + // how many frames the server has received from the client + // so the client can release cached frames by this point + long remoteImpliedPos = ResumeOkFrameCodec.lastReceivedClientPos(shouldBeResumeOKFrame); + // what was the last notification from the server about number of frames being + // observed final long position = resumableFramesStore.framePosition(); final long impliedPosition = resumableFramesStore.frameImpliedPosition(); logger.debug( @@ -200,7 +201,6 @@ public synchronized void onNext(Tuple2 tuple2) { } if (resumableConnection.connect(nextDuplexConnection)) { - observeDisconnection(nextDuplexConnection); keepAliveSupport.start(); logger.debug("Session has been resumed successfully"); } else { @@ -220,7 +220,7 @@ public synchronized void onNext(Tuple2 tuple2) { nextDuplexConnection.sendErrorAndClose(connectionErrorException); } } else if (frameType == FrameType.ERROR) { - final RuntimeException exception = Exceptions.from(0, frame); + final RuntimeException exception = Exceptions.from(0, shouldBeResumeOKFrame); logger.debug("Received error frame. Terminating received connection", exception); resumableConnection.dispose(); } else { 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 a6148bd08..36d2efdbc 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java @@ -61,6 +61,7 @@ public class InMemoryResumableFramesStore extends Flux CoreSubscriber actual; + // indicates whether there is active connection or not volatile int state; static final AtomicIntegerFieldUpdater STATE = AtomicIntegerFieldUpdater.newUpdater(InMemoryResumableFramesStore.class, "state"); @@ -94,9 +95,6 @@ public void releaseFrames(long remoteImpliedPos) { while (toRemoveBytes > removedBytes && frames.size() > 0) { ByteBuf cachedFrame = frames.remove(0); int frameSize = cachedFrame.readableBytes(); - // logger.debug( - // "{} Removing frame {}", tag, - // cachedFrame.toString(CharsetUtil.UTF_8)); cachedFrame.release(); removedBytes += frameSize; } @@ -110,7 +108,7 @@ public void releaseFrames(long remoteImpliedPos) { toRemoveBytes)); } else if (toRemoveBytes < removedBytes) { throw new IllegalStateException( - "Local and remote state disagreement: " + "local and remote frame sizes are not equal"); + "Local and remote state disagreement: local and remote frame sizes are not equal"); } else { POSITION.addAndGet(this, removedBytes); if (cacheLimit != Integer.MAX_VALUE) { @@ -184,6 +182,7 @@ public void dispose() { if (STATE.getAndSet(this, 2) != 2) { cacheSize = 0; synchronized (this) { + logger.debug("Tag {}.Disposing InMemoryFrameStore", tag); for (ByteBuf frame : cachedFrames) { if (frame != null) { frame.release(); @@ -197,7 +196,7 @@ public void dispose() { @Override public boolean isDisposed() { - return disposed.isTerminated(); + return state == 2; } @Override @@ -218,6 +217,9 @@ public void onComplete() { @Override public void onNext(ByteBuf frame) { + frame.touch("Tag : " + tag + ". InMemoryResumableFramesStore:onNext"); + + final int state; final boolean isResumable = isResumableFrame(frame); if (isResumable) { final ArrayList frames = cachedFrames; @@ -244,20 +246,23 @@ public void onNext(ByteBuf frame) { POSITION.addAndGet(this, removedBytes); } } - synchronized (this) { - frames.add(frame); + state = this.state; + if (state != 2) { + frames.add(frame); + } } if (cacheLimit != Integer.MAX_VALUE) { CACHE_SIZE.addAndGet(this, incomingFrameSize); } + } else { + state = this.state; } - final int state = this.state; final CoreSubscriber actual = this.actual; if (state == 1) { actual.onNext(frame.retain()); - } else if (!isResumable) { + } else if (!isResumable || state == 2) { frame.release(); } } @@ -274,18 +279,25 @@ public void cancel() { @Override public void subscribe(CoreSubscriber actual) { final int state = this.state; - logger.debug("Tag: {}. Subscribed State[{}]", tag, state); - actual.onSubscribe(this); if (state != 2) { + resumeImplied(); + logger.debug( + "Tag: {}. Subscribed at Position[{}] and ImpliedPosition[{}]", + tag, + position, + impliedPosition); + actual.onSubscribe(this); synchronized (this) { for (final ByteBuf frame : cachedFrames) { + frame.touch("Tag : " + tag + ". InMemoryResumableFramesStore:subscribe"); actual.onNext(frame.retain()); } } this.actual = actual; - resumeImplied(); STATE.compareAndSet(this, 0, 1); + } else { + Operators.complete(actual); } } } 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 60484f9d1..9ea889e0d 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -26,22 +26,29 @@ import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.CoreSubscriber; import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Operators; +import reactor.core.publisher.Sinks; public class ResumableDuplexConnection extends Flux implements DuplexConnection, Subscription { + static final Logger logger = LoggerFactory.getLogger(ResumableDuplexConnection.class); + + final String tag; final ResumableFramesStore resumableFramesStore; final UnboundedProcessor savableFramesSender; final Disposable framesSaverDisposable; final MonoProcessor onClose; final SocketAddress remoteAddress; + final Sinks.Many onConnectionClosedSink; CoreSubscriber receiveSubscriber; FrameReceivingSubscriber activeReceivingSubscriber; @@ -56,8 +63,12 @@ public class ResumableDuplexConnection extends Flux AtomicReferenceFieldUpdater.newUpdater( ResumableDuplexConnection.class, DuplexConnection.class, "activeConnection"); + int connectionIndex = 0; + public ResumableDuplexConnection( - DuplexConnection initialConnection, ResumableFramesStore resumableFramesStore) { + String tag, DuplexConnection initialConnection, ResumableFramesStore resumableFramesStore) { + this.tag = tag; + this.onConnectionClosedSink = Sinks.many().unsafe().unicast().onBackpressureBuffer(); this.resumableFramesStore = resumableFramesStore; this.savableFramesSender = new UnboundedProcessor<>(); this.framesSaverDisposable = resumableFramesStore.saveFrames(savableFramesSender).subscribe(); @@ -83,9 +94,15 @@ public boolean connect(DuplexConnection nextConnection) { } void initConnection(DuplexConnection nextConnection) { + logger.debug("Tag {}. Initializing connection {}", tag, nextConnection); + + final int currentConnectionIndex = connectionIndex; final FrameReceivingSubscriber frameReceivingSubscriber = - new FrameReceivingSubscriber(resumableFramesStore, receiveSubscriber); + new FrameReceivingSubscriber(tag, resumableFramesStore, receiveSubscriber); + + this.connectionIndex = currentConnectionIndex + 1; this.activeReceivingSubscriber = frameReceivingSubscriber; + final Disposable disposable = resumableFramesStore .resumeStream() @@ -97,6 +114,7 @@ void initConnection(DuplexConnection nextConnection) { __ -> { frameReceivingSubscriber.dispose(); disposable.dispose(); + onConnectionClosedSink.emitNext(currentConnectionIndex); }) .subscribe(); } @@ -117,6 +135,10 @@ public void sendFrame(int streamId, ByteBuf frame) { } } + Flux onActiveConnectionClosed() { + return onConnectionClosedSink.asFlux(); + } + @Override public void sendErrorAndClose(RSocketErrorException rSocketErrorException) { final DuplexConnection activeConnection = @@ -133,11 +155,13 @@ public void sendErrorAndClose(RSocketErrorException rSocketErrorException) { t -> { framesSaverDisposable.dispose(); savableFramesSender.dispose(); + onConnectionClosedSink.emitComplete(); onClose.onError(t); }, () -> { framesSaverDisposable.dispose(); savableFramesSender.dispose(); + onConnectionClosedSink.emitComplete(); final Throwable cause = rSocketErrorException.getCause(); if (cause == null) { onClose.onComplete(); @@ -177,6 +201,7 @@ public void dispose() { framesSaverDisposable.dispose(); activeReceivingSubscriber.dispose(); savableFramesSender.dispose(); + onConnectionClosedSink.emitComplete(); onClose.onComplete(); } @@ -256,6 +281,7 @@ private static final class FrameReceivingSubscriber final ResumableFramesStore resumableFramesStore; final CoreSubscriber actual; + final String tag; volatile Subscription s; static final AtomicReferenceFieldUpdater S = @@ -265,7 +291,8 @@ private static final class FrameReceivingSubscriber boolean cancelled; private FrameReceivingSubscriber( - ResumableFramesStore store, CoreSubscriber actual) { + String tag, ResumableFramesStore store, CoreSubscriber actual) { + this.tag = tag; this.resumableFramesStore = store; this.actual = actual; } @@ -279,6 +306,7 @@ public void onSubscribe(Subscription s) { @Override public void onNext(ByteBuf frame) { + frame.touch("Tag : " + tag + ". FrameReceivingSubscriber#onNext"); if (cancelled || s == Operators.cancelledSubscription()) { return; } 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 ad405afc0..18ca50401 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -26,6 +26,8 @@ import io.rsocket.frame.ResumeOkFrameCodec; import io.rsocket.keepalive.KeepAliveSupport; import java.time.Duration; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.reactivestreams.Subscription; import org.slf4j.Logger; @@ -33,6 +35,7 @@ import reactor.core.CoreSubscriber; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; +import reactor.util.concurrent.Queues; public class ServerRSocketSession implements RSocketSession, ResumeStateHolder, CoreSubscriber { @@ -45,6 +48,12 @@ public class ServerRSocketSession final ByteBufAllocator allocator; final boolean cleanupStoreOnKeepAlive; + final Queue connectionsQueue; + + volatile int wip; + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(ServerRSocketSession.class, "wip"); + volatile Subscription s; static final AtomicReferenceFieldUpdater S = AtomicReferenceFieldUpdater.newUpdater(ServerRSocketSession.class, Subscription.class, "s"); @@ -64,26 +73,48 @@ public ServerRSocketSession( this.resumableFramesStore = resumableFramesStore; this.cleanupStoreOnKeepAlive = cleanupStoreOnKeepAlive; this.resumableConnection = resumableDuplexConnection; + this.connectionsQueue = Queues.unboundedMultiproducer().get(); - resumableDuplexConnection.onClose().doFinally(__ -> dispose()).subscribe(); - - observeDisconnection(initialDuplexConnection); - } + WIP.lazySet(this, 1); - void observeDisconnection(DuplexConnection activeConnection) { - activeConnection.onClose().subscribe(null, e -> tryTimeoutSession(), () -> tryTimeoutSession()); + resumableDuplexConnection.onClose().doFinally(__ -> dispose()).subscribe(); + resumableDuplexConnection.onActiveConnectionClosed().subscribe(__ -> tryTimeoutSession()); } void tryTimeoutSession() { keepAliveSupport.stop(); Mono.delay(resumeSessionDuration).subscribe(this); logger.debug("Connection is lost. Trying to timeout the active session[{}]", resumeToken); + + if (WIP.decrementAndGet(this) == 0) { + return; + } + + final Runnable doResumeRunnable = connectionsQueue.poll(); + if (doResumeRunnable != null) { + doResumeRunnable.run(); + } } - public synchronized Mono resumeWith( - ByteBuf resumeFrame, DuplexConnection nextDuplexConnection) { + public void resumeWith(ByteBuf resumeFrame, DuplexConnection nextDuplexConnection) { + long remotePos = ResumeFrameCodec.firstAvailableClientPos(resumeFrame); long remoteImpliedPos = ResumeFrameCodec.lastReceivedServerPos(resumeFrame); + + connectionsQueue.offer(() -> doResume(remotePos, remoteImpliedPos, nextDuplexConnection)); + + if (WIP.getAndIncrement(this) != 0) { + return; + } + + final Runnable doResumeRunnable = connectionsQueue.poll(); + if (doResumeRunnable != null) { + doResumeRunnable.run(); + } + } + + void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplexConnection) { + long impliedPosition = resumableFramesStore.frameImpliedPosition(); long position = resumableFramesStore.framePosition(); @@ -102,7 +133,7 @@ public synchronized Mono resumeWith( final RejectedResumeException rejectedResumeException = new RejectedResumeException("resume_internal_error: Session Expired"); nextDuplexConnection.sendErrorAndClose(rejectedResumeException); - return nextDuplexConnection.onClose(); + return; } if (S.compareAndSet(this, subscription, null)) { @@ -121,12 +152,11 @@ public synchronized Mono resumeWith( logger.debug("ResumeOK Frame has been sent"); } catch (Throwable t) { logger.debug("Exception occurred while releasing frames in the frameStore", t); - resumableConnection.dispose(); + tryTimeoutSession(); nextDuplexConnection.sendErrorAndClose(new RejectedResumeException(t.getMessage(), t)); - return nextDuplexConnection.onClose(); + return; } if (resumableConnection.connect(nextDuplexConnection)) { - observeDisconnection(nextDuplexConnection); keepAliveSupport.start(); logger.debug("Session[{}] has been resumed successfully", resumeToken); } else { @@ -142,7 +172,7 @@ public synchronized Mono resumeWith( position, remotePos, impliedPosition); - resumableConnection.dispose(); + tryTimeoutSession(); final RejectedResumeException rejectedResumeException = new RejectedResumeException( String.format( @@ -150,8 +180,6 @@ public synchronized Mono resumeWith( remotePos, remoteImpliedPos, position, impliedPosition)); nextDuplexConnection.sendErrorAndClose(rejectedResumeException); } - - return nextDuplexConnection.onClose(); } @Override @@ -194,10 +222,9 @@ public void setKeepAliveSupport(KeepAliveSupport keepAliveSupport) { @Override public void dispose() { - if (Operators.terminate(S, this)) { - resumableFramesStore.dispose(); - resumableConnection.dispose(); - } + Operators.terminate(S, this); + resumableConnection.dispose(); + resumableFramesStore.dispose(); } @Override 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 0ddfb5449..0345f2c48 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java +++ b/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java @@ -5,8 +5,12 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; +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.Set; import java.util.concurrent.ConcurrentLinkedQueue; import org.assertj.core.api.Assertions; @@ -23,7 +27,7 @@ class LeaksTrackingByteBufAllocator implements ByteBufAllocator { * @return */ public static LeaksTrackingByteBufAllocator instrument(ByteBufAllocator allocator) { - return new LeaksTrackingByteBufAllocator(allocator, Duration.ZERO); + return new LeaksTrackingByteBufAllocator(allocator, Duration.ZERO, ""); } /** @@ -33,8 +37,8 @@ public static LeaksTrackingByteBufAllocator instrument(ByteBufAllocator allocato * @return */ public static LeaksTrackingByteBufAllocator instrument( - ByteBufAllocator allocator, Duration awaitZeroRefCntDuration) { - return new LeaksTrackingByteBufAllocator(allocator, awaitZeroRefCntDuration); + ByteBufAllocator allocator, Duration awaitZeroRefCntDuration, String tag) { + return new LeaksTrackingByteBufAllocator(allocator, awaitZeroRefCntDuration, tag); } final ConcurrentLinkedQueue tracker = new ConcurrentLinkedQueue<>(); @@ -43,10 +47,13 @@ public static LeaksTrackingByteBufAllocator instrument( final Duration awaitZeroRefCntDuration; + final String tag; + private LeaksTrackingByteBufAllocator( - ByteBufAllocator delegate, Duration awaitZeroRefCntDuration) { + ByteBufAllocator delegate, Duration awaitZeroRefCntDuration, String tag) { this.delegate = delegate; this.awaitZeroRefCntDuration = awaitZeroRefCntDuration; + this.tag = tag; } public LeaksTrackingByteBufAllocator assertHasNoLeaks() { @@ -73,11 +80,11 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { } if (!hasUnreleased) { - System.out.println("all the buffers are released..."); + System.out.println(tag + " all the buffers are released..."); return this; } - System.out.println("await buffers to be released"); + System.out.println(tag + " await buffers to be released"); for (int i = 0; i < 100; i++) { System.gc(); parkNanos(1000); @@ -86,8 +93,23 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { } } - Assertions.assertThat(unreleased).allMatch(bb -> bb.refCnt() == 0); - System.out.println("all the buffers are released..."); + 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); + System.out.println(tag + " all the buffers are released..."); } finally { tracker.clear(); } @@ -201,4 +223,60 @@ T track(T buffer) { return buffer; } + + static final Class simpleLeakAwareCompositeByteBufClass; + static final Field leakFieldForComposite; + static final Class simpleLeakAwareByteBufClass; + static final Field leakFieldForNormal; + static final Field allLeaksField; + + static { + try { + { + final Class aClass = Class.forName("io.netty.buffer.SimpleLeakAwareCompositeByteBuf"); + final Field leakField = aClass.getDeclaredField("leak"); + + leakField.setAccessible(true); + + simpleLeakAwareCompositeByteBufClass = aClass; + leakFieldForComposite = leakField; + } + + { + final Class aClass = Class.forName("io.netty.buffer.SimpleLeakAwareByteBuf"); + final Field leakField = aClass.getDeclaredField("leak"); + + leakField.setAccessible(true); + + simpleLeakAwareByteBufClass = aClass; + leakFieldForNormal = leakField; + } + + { + final Class aClass = + Class.forName("io.netty.util.ResourceLeakDetector$DefaultResourceLeak"); + final Field field = aClass.getDeclaredField("allLeaks"); + + field.setAccessible(true); + + allLeaksField = field; + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + static Set resolveTrackingInfo(ByteBuf byteBuf) throws Exception { + if (ResourceLeakDetector.getLevel().ordinal() + >= ResourceLeakDetector.Level.ADVANCED.ordinal()) { + if (simpleLeakAwareCompositeByteBufClass.isInstance(byteBuf)) { + return (Set) allLeaksField.get(leakFieldForComposite.get(byteBuf)); + } else if (simpleLeakAwareByteBufClass.isInstance(byteBuf)) { + return (Set) allLeaksField.get(leakFieldForNormal.get(byteBuf)); + } + } + + return Collections.emptySet(); + } } 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 4f316b02d..0bae8cd69 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -57,6 +57,7 @@ import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Disposable; +import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.publisher.Flux; import reactor.core.publisher.Hooks; @@ -99,11 +100,27 @@ default void setUp() { @AfterEach default void close() { + Hooks.resetOnOperatorDebug(); getTransportPair().responder.awaitAllInteractionTermination(getTimeout()); getTransportPair().dispose(); getTransportPair().awaitClosed(); - getTransportPair().byteBufAllocator.assertHasNoLeaks(); - Hooks.resetOnOperatorDebug(); + RuntimeException throwable = new RuntimeException(); + + 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; + } } default Payload createTestPayload(int metadataPresent) { @@ -217,6 +234,7 @@ default void requestChannel200_000() { getClient() .requestChannel(payloads) .doOnNext(Payload::release) + .limitRate(8) .as(StepVerifier::create) .expectNextCount(200_000) .expectComplete() @@ -260,6 +278,7 @@ default void requestChannel2_000_000() { getClient() .requestChannel(payloads) .doOnNext(Payload::release) + .limitRate(8) .as(StepVerifier::create) .expectNextCount(2_000_000) .expectComplete() @@ -287,7 +306,6 @@ default void requestChannel3() { @DisplayName("makes 1 requestChannel request with 256 payloads") @Test default void requestChannel256() { - Assumptions.assumeThat(getTransportPair().withResumability).isFalse(); AtomicInteger counter = new AtomicInteger(); Flux payloads = Flux.defer( @@ -297,7 +315,7 @@ default void requestChannel256() { .map(i -> "S{" + subscription + "}: Data{" + i + "}") .map(data -> ByteBufPayload.create(data)); }); - final Scheduler scheduler = Schedulers.fromExecutorService(Executors.newFixedThreadPool(13)); + final Scheduler scheduler = Schedulers.fromExecutorService(Executors.newFixedThreadPool(12)); Flux.range(0, 1024) .flatMap(v -> Mono.fromRunnable(() -> check(payloads)).subscribeOn(scheduler), 12) @@ -307,12 +325,8 @@ default void requestChannel256() { default void check(Flux payloads) { getClient() .requestChannel(payloads) - .map( - payload -> { - final String data = payload.getDataUtf8(); - payload.release(); - return data; - }) + .doOnNext(ReferenceCounted::release) + .limitRate(8) .as(StepVerifier::create) .expectNextCount(256) .as("expected 256 items") @@ -442,12 +456,17 @@ default void assertChannelPayload(Payload p) { class TransportPair implements Disposable { - private final boolean withResumability; private static final String data = "hello world"; private static final String metadata = "metadata"; - private final LeaksTrackingByteBufAllocator byteBufAllocator = - LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT, Duration.ofMinutes(1)); + private final boolean withResumability; + + private final LeaksTrackingByteBufAllocator byteBufAllocator1 = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofMinutes(1), "Client"); + private final LeaksTrackingByteBufAllocator byteBufAllocator2 = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofMinutes(1), "Server"); private final TestRSocket responder; @@ -488,13 +507,16 @@ public TransportPair( final boolean runClientWithAsyncInterceptors = ThreadLocalRandom.current().nextBoolean(); final boolean runServerWithAsyncInterceptors = ThreadLocalRandom.current().nextBoolean(); - ByteBufAllocator allocatorToSupply; + ByteBufAllocator allocatorToSupply1; + ByteBufAllocator allocatorToSupply2; if (ResourceLeakDetector.getLevel() == ResourceLeakDetector.Level.ADVANCED || ResourceLeakDetector.getLevel() == ResourceLeakDetector.Level.PARANOID) { logger.info("Using LeakTrackingByteBufAllocator"); - allocatorToSupply = byteBufAllocator; + allocatorToSupply1 = byteBufAllocator1; + allocatorToSupply2 = byteBufAllocator2; } else { - allocatorToSupply = ByteBufAllocator.DEFAULT; + allocatorToSupply1 = ByteBufAllocator.DEFAULT; + allocatorToSupply2 = ByteBufAllocator.DEFAULT; } responder = new TestRSocket(TransportPair.data, metadata); final RSocketServer rSocketServer = @@ -525,7 +547,7 @@ public TransportPair( "Server", duplexConnection, Duration.ofMillis( - ThreadLocalRandom.current().nextInt(200, 500))) + ThreadLocalRandom.current().nextInt(10, 1500))) : duplexConnection); } }); @@ -541,7 +563,7 @@ public TransportPair( } server = - rSocketServer.bind(serverTransportSupplier.apply(address, allocatorToSupply)).block(); + rSocketServer.bind(serverTransportSupplier.apply(address, allocatorToSupply2)).block(); final RSocketConnector rSocketConnector = RSocketConnector.create() @@ -572,7 +594,7 @@ public TransportPair( "Client", duplexConnection, Duration.ofMillis( - ThreadLocalRandom.current().nextInt(200, 500))) + ThreadLocalRandom.current().nextInt(1, 2000))) : duplexConnection); } }); @@ -589,7 +611,7 @@ public TransportPair( client = rSocketConnector - .connect(clientTransportSupplier.apply(address, server, allocatorToSupply)) + .connect(clientTransportSupplier.apply(address, server, allocatorToSupply1)) .doOnError(Throwable::printStackTrace) .block(); } @@ -716,9 +738,11 @@ public Flux receive() { if (!receivedFirst) { receivedFirst = true; Mono.delay(delay) + .takeUntilOther(source.onClose()) .subscribe( __ -> { - logger.warn("Tag {}. Disposing Connection", tag); + logger.warn( + "Tag {}. Disposing Connection[{}]", tag, source.hashCode()); source.dispose(); }); } 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 57ef63402..8bea7c682 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 @@ -21,7 +21,7 @@ import java.util.UUID; import org.junit.jupiter.api.Disabled; -@Disabled +@Disabled("leaking somewhere for no clear reason") final class LocalResumableTransportTest implements TransportTest { private final TransportPair transportPair = diff --git a/rsocket-transport-local/src/test/resources/logback-test.xml b/rsocket-transport-local/src/test/resources/logback-test.xml index 01a7fa4cd..5c92235c2 100644 --- a/rsocket-transport-local/src/test/resources/logback-test.xml +++ b/rsocket-transport-local/src/test/resources/logback-test.xml @@ -23,10 +23,27 @@ + + ./test-out.log + false + + %-5relative %-5level %logger{35} - %msg%n + + + + + + + + + + + - + + 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 18ef55d30..cf9e0540c 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 @@ -22,11 +22,9 @@ import io.rsocket.transport.netty.server.TcpServerTransport; import java.net.InetSocketAddress; import java.time.Duration; -import org.junit.jupiter.api.Disabled; import reactor.netty.tcp.TcpClient; import reactor.netty.tcp.TcpServer; -@Disabled final class TcpResumableTransportTest implements TransportTest { private final TransportPair transportPair = diff --git a/rsocket-transport-netty/src/test/resources/logback-test.xml b/rsocket-transport-netty/src/test/resources/logback-test.xml index f9dec2bbe..b42db6df6 100644 --- a/rsocket-transport-netty/src/test/resources/logback-test.xml +++ b/rsocket-transport-netty/src/test/resources/logback-test.xml @@ -26,6 +26,15 @@ + + + + + + + + + From 398f49d26edcfdf8079aad3d74c8823796c34bff Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sun, 27 Sep 2020 15:56:58 +0300 Subject: [PATCH 026/183] optimizes/fixes UnboundedProcessor Signed-off-by: Oleh Dokuka --- .../internal/BaseDuplexConnection.java | 2 +- .../rsocket/internal/UnboundedProcessor.java | 535 +++++++++++------- .../resume/ResumableDuplexConnection.java | 4 +- .../internal/UnboundedProcessorTest.java | 34 +- .../transport/local/LocalClientTransport.java | 5 +- .../local/LocalDuplexConnection.java | 4 +- 6 files changed, 356 insertions(+), 228 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java index acbcfcf39..9fd33591a 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java @@ -8,7 +8,7 @@ public abstract class BaseDuplexConnection implements DuplexConnection { protected MonoProcessor onClose = MonoProcessor.create(); - protected UnboundedProcessor sender = new UnboundedProcessor<>(); + protected UnboundedProcessor sender = new UnboundedProcessor(); public BaseDuplexConnection() { onClose.doFinally(s -> doOnClose()).subscribe(); diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java index cb8b5d63d..c0690d031 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -16,7 +16,7 @@ package io.rsocket.internal; -import io.netty.util.ReferenceCounted; +import io.netty.buffer.ByteBuf; import io.rsocket.internal.jctools.queues.MpscUnboundedArrayQueue; import java.util.Objects; import java.util.Queue; @@ -37,45 +37,38 @@ * A Processor implementation that takes a custom queue and allows only a single subscriber. * *

    The implementation keeps the order of signals. - * - * @param the input and output type */ -public final class UnboundedProcessor extends FluxProcessor - implements Fuseable.QueueSubscription, Fuseable { +public final class UnboundedProcessor extends FluxProcessor + implements Fuseable.QueueSubscription, Fuseable { - final Queue queue; - final Queue priorityQueue; + final Queue queue; + final Queue priorityQueue; - volatile boolean done; + boolean done; Throwable error; - // important to not loose the downstream too early and miss discard hook, while - // having relevant hasDownstreams() - boolean hasDownstream; - volatile CoreSubscriber actual; - - volatile boolean cancelled; - - volatile int once; + CoreSubscriber actual; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater ONCE = - AtomicIntegerFieldUpdater.newUpdater(UnboundedProcessor.class, "once"); + static final long STATE_TERMINATED = + 0b1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long FLAG_CANCELLED = + 0b0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long FLAG_SUBSCRIBED_ONCE = + 0b0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long MAX_VALUE = + 0b0001_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111L; - volatile int wip; + volatile long state; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater WIP = - AtomicIntegerFieldUpdater.newUpdater(UnboundedProcessor.class, "wip"); + static final AtomicLongFieldUpdater STATE = + AtomicLongFieldUpdater.newUpdater(UnboundedProcessor.class, "state"); volatile int discardGuard; - @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater DISCARD_GUARD = AtomicIntegerFieldUpdater.newUpdater(UnboundedProcessor.class, "discardGuard"); volatile long requested; - @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(UnboundedProcessor.class, "requested"); @@ -98,11 +91,123 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } - void drainRegular(Subscriber a) { - int missed = 1; + public void onNextPrioritized(ByteBuf t) { + if (done) { + release(t); + return; + } + + if (!priorityQueue.offer(t)) { + Throwable ex = + Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); + onError(Operators.onOperatorError(null, ex, t, currentContext())); + release(t); + return; + } + + drain(); + } + + @Override + public void onNext(ByteBuf t) { + if (done) { + release(t); + return; + } + + if (!queue.offer(t)) { + Throwable ex = + Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); + onError(Operators.onOperatorError(null, ex, t, currentContext())); + release(t); + return; + } + + drain(); + } + + @Override + public void onError(Throwable t) { + if (done) { + Operators.onErrorDropped(t, currentContext()); + return; + } + + error = t; + done = true; + + drain(); + } + + @Override + public void onComplete() { + if (done) { + return; + } + + done = true; + + drain(); + } + + @Override + public void subscribe(CoreSubscriber actual) { + Objects.requireNonNull(actual, "subscribe"); + if (markSubscribedOnce(this)) { + this.actual = actual; + actual.onSubscribe(this); + drain(); + } else { + Operators.error( + actual, + new IllegalStateException("UnboundedProcessor " + "allows only a single Subscriber")); + } + } + + void drain() { + long previousState = wipIncrement(this); + if (isTerminated(previousState)) { + this.clearSafely(); + return; + } + + if (isWorkInProgress(previousState)) { + return; + } + + final boolean outputFused = this.outputFused; + if (isCancelled(previousState) && !outputFused) { + clearAndTerminate(this); + return; + } + + long expectedState = previousState + 1; + for (; ; ) { + final Subscriber a = actual; + if (a != null) { + if (outputFused) { + drainFused(expectedState, a); + } else { + drainRegular(expectedState, a); + } + return; + } + + expectedState = wipRemoveMissing(this, expectedState); + if (isCancelled(expectedState)) { + clearAndTerminate(this); + return; + } + + if (!isWorkInProgress(expectedState)) { + return; + } + } + } - final Queue q = queue; - final Queue pq = priorityQueue; + void drainRegular(long expectedState, Subscriber a) { + final Queue q = queue; + final Queue pq = priorityQueue; for (; ; ) { @@ -110,9 +215,7 @@ void drainRegular(Subscriber a) { long e = 0L; while (r != e) { - boolean d = done; - - T t; + ByteBuf t; boolean empty; if (!pq.isEmpty()) { @@ -123,7 +226,7 @@ void drainRegular(Subscriber a) { empty = t == null; } - if (checkTerminated(d, empty, a)) { + if (checkTerminated(empty, a)) { return; } @@ -137,7 +240,7 @@ void drainRegular(Subscriber a) { } if (r == e) { - if (checkTerminated(done, q.isEmpty() && pq.isEmpty(), a)) { + if (checkTerminated(q.isEmpty() && pq.isEmpty(), a)) { return; } } @@ -146,31 +249,25 @@ void drainRegular(Subscriber a) { REQUESTED.addAndGet(this, -e); } - missed = WIP.addAndGet(this, -missed); - if (missed == 0) { + expectedState = wipRemoveMissing(this, expectedState); + if (isCancelled(expectedState)) { + clearAndTerminate(this); + return; + } + + if (!isWorkInProgress(expectedState)) { break; } } } - void drainFused(Subscriber a) { - int missed = 1; - + void drainFused(long expectedState, Subscriber a) { for (; ; ) { - - if (cancelled) { - this.clear(); - hasDownstream = false; - return; - } - boolean d = done; a.onNext(null); if (d) { - hasDownstream = false; - Throwable ex = error; if (ex != null) { a.onError(ex); @@ -180,56 +277,32 @@ void drainFused(Subscriber a) { return; } - missed = WIP.addAndGet(this, -missed); - if (missed == 0) { - break; - } - } - } - - public void drain() { - if (WIP.getAndIncrement(this) != 0) { - if (cancelled) { - this.clear(); - } - return; - } - - int missed = 1; - - for (; ; ) { - Subscriber a = actual; - if (a != null) { - - if (outputFused) { - drainFused(a); - } else { - drainRegular(a); - } + expectedState = wipRemoveMissing(this, expectedState); + if (isCancelled(expectedState)) { return; } - missed = WIP.addAndGet(this, -missed); - if (missed == 0) { + if (!isWorkInProgress(expectedState)) { break; } } } - boolean checkTerminated(boolean d, boolean empty, Subscriber a) { - if (cancelled) { - this.clear(); - hasDownstream = false; + boolean checkTerminated(boolean empty, Subscriber a) { + final long state = this.state; + if (isCancelled(state)) { + clearAndTerminate(this); return true; } - if (d && empty) { + + if (done && empty) { Throwable e = error; - hasDownstream = false; if (e != null) { a.onError(e); } else { a.onComplete(); } + clearAndTerminate(this); return true; } @@ -238,7 +311,8 @@ boolean checkTerminated(boolean d, boolean empty, Subscriber a) { @Override public void onSubscribe(Subscription s) { - if (done || cancelled) { + final long state = this.state; + if (done || isTerminated(state) || isCancelled(state)) { s.cancel(); } else { s.request(Long.MAX_VALUE); @@ -252,86 +326,13 @@ public int getPrefetch() { @Override public Context currentContext() { - CoreSubscriber actual = this.actual; - return actual != null ? actual.currentContext() : Context.empty(); - } - - public void onNextPrioritized(T t) { - if (done || cancelled) { - Operators.onNextDropped(t, currentContext()); - release(t); - return; - } - - if (!priorityQueue.offer(t)) { - Throwable ex = - Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); - onError(Operators.onOperatorError(null, ex, t, currentContext())); - release(t); - return; - } - drain(); - } - - @Override - public void onNext(T t) { - if (done || cancelled) { - Operators.onNextDropped(t, currentContext()); - release(t); - return; + final long state = this.state; + if (isSubscribedOnce(state) || isTerminated(state)) { + CoreSubscriber actual = this.actual; + return actual != null ? actual.currentContext() : Context.empty(); } - if (!queue.offer(t)) { - Throwable ex = - Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); - onError(Operators.onOperatorError(null, ex, t, currentContext())); - release(t); - return; - } - drain(); - } - - @Override - public void onError(Throwable t) { - if (done || cancelled) { - Operators.onErrorDropped(t, currentContext()); - return; - } - - error = t; - done = true; - - drain(); - } - - @Override - public void onComplete() { - if (done || cancelled) { - return; - } - - done = true; - - drain(); - } - - @Override - public void subscribe(CoreSubscriber actual) { - Objects.requireNonNull(actual, "subscribe"); - if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { - - actual.onSubscribe(this); - this.actual = actual; - if (cancelled) { - this.hasDownstream = false; - } else { - drain(); - } - } else { - Operators.error( - actual, - new IllegalStateException("UnboundedProcessor " + "allows only a single Subscriber")); - } + return Context.empty(); } @Override @@ -344,21 +345,26 @@ public void request(long n) { @Override public void cancel() { - if (cancelled) { + if (!markCancelled(this)) { + return; + } + + if (outputFused) { return; } - cancelled = true; - if (WIP.getAndIncrement(this) == 0) { - this.clear(); - hasDownstream = false; + final long state = wipIncrement(this); + if (isWorkInProgress(state)) { + return; } + + clearAndTerminate(this); } @Override @Nullable - public T poll() { - Queue pq = this.priorityQueue; + public ByteBuf poll() { + Queue pq = this.priorityQueue; if (!pq.isEmpty()) { return pq.poll(); } @@ -366,36 +372,18 @@ public T poll() { } @Override - public int size() { - return priorityQueue.size() + queue.size(); - } - - @Override - public boolean isEmpty() { - return priorityQueue.isEmpty() && queue.isEmpty(); + public void clear() { + clearAndTerminate(this); } - @Override - public void clear() { + void clearSafely() { if (DISCARD_GUARD.getAndIncrement(this) != 0) { return; } int missed = 1; - for (; ; ) { - while (!queue.isEmpty()) { - T t = queue.poll(); - if (t != null) { - release(t); - } - } - while (!priorityQueue.isEmpty()) { - T t = priorityQueue.poll(); - if (t != null) { - release(t); - } - } + clearUnsafely(); missed = DISCARD_GUARD.addAndGet(this, -missed); if (missed == 0) { @@ -404,6 +392,30 @@ public void clear() { } } + void clearUnsafely() { + final Queue queue = this.queue; + final Queue priorityQueue = this.priorityQueue; + + ByteBuf byteBuf; + while ((byteBuf = queue.poll()) != null) { + release(byteBuf); + } + + while ((byteBuf = priorityQueue.poll()) != null) { + release(byteBuf); + } + } + + @Override + public int size() { + return priorityQueue.size() + queue.size(); + } + + @Override + public boolean isEmpty() { + return priorityQueue.isEmpty() && queue.isEmpty(); + } + @Override public int requestFusion(int requestedMode) { if ((requestedMode & Fuseable.ASYNC) != 0) { @@ -420,18 +432,25 @@ public void dispose() { @Override public boolean isDisposed() { - return cancelled || done; + final long state = this.state; + return isTerminated(state) || isCancelled(state) || done; } @Override public boolean isTerminated() { - return done; + final long state = this.state; + return isTerminated(state) || done; } @Override @Nullable public Throwable getError() { - return error; + final long state = this.state; + if (isTerminated(state) || done) { + return error; + } else { + return null; + } } @Override @@ -441,19 +460,129 @@ public long downstreamCount() { @Override public boolean hasDownstreams() { - return hasDownstream; + return (state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE && actual != null; } - void release(T t) { - if (t instanceof ReferenceCounted) { - ReferenceCounted refCounted = (ReferenceCounted) t; - if (refCounted.refCnt() > 0) { - try { - refCounted.release(); - } catch (Throwable ex) { - // no ops - } + static void release(ByteBuf byteBuf) { + if (byteBuf.refCnt() > 0) { + try { + byteBuf.release(); + } catch (Throwable ex) { + // no ops + } + } + } + + static boolean markSubscribedOnce(UnboundedProcessor instance) { + for (; ; ) { + long state = instance.state; + + if (state == STATE_TERMINATED) { + return false; + } + + if ((state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE + || (state & FLAG_CANCELLED) == FLAG_CANCELLED) { + return false; + } + + if (STATE.compareAndSet(instance, state, state | FLAG_SUBSCRIBED_ONCE)) { + return true; + } + } + } + + static boolean markCancelled(UnboundedProcessor instance) { + for (; ; ) { + long state = instance.state; + + if (state == STATE_TERMINATED) { + return false; + } + + if ((state & FLAG_CANCELLED) == FLAG_CANCELLED) { + return false; + } + + if (STATE.compareAndSet(instance, state, state | FLAG_CANCELLED)) { + return true; + } + } + } + + static long wipIncrement(UnboundedProcessor instance) { + for (; ; ) { + long state = instance.state; + + if (state == STATE_TERMINATED) { + return STATE_TERMINATED; + } + + final long nextState = state + 1; + if ((nextState & MAX_VALUE) == 0) { + return state; + } + + if (STATE.compareAndSet(instance, state, nextState)) { + return state; + } + } + } + + static long wipRemoveMissing(UnboundedProcessor instance, long previousState) { + long missed = previousState & MAX_VALUE; + boolean outputFused = instance.outputFused; + for (; ; ) { + long state = instance.state; + + if (state == STATE_TERMINATED) { + return STATE_TERMINATED; + } + + if (!outputFused && (state & FLAG_CANCELLED) == FLAG_CANCELLED) { + return state; + } + + final long nextState = state - missed; + if (STATE.compareAndSet(instance, state, nextState)) { + return nextState; + } + } + } + + static void clearAndTerminate(UnboundedProcessor instance) { + for (; ; ) { + long state = instance.state; + + if (instance.outputFused) { + instance.clearSafely(); + } else { + instance.clearUnsafely(); + } + + if (state == STATE_TERMINATED) { + return; + } + + if (STATE.compareAndSet(instance, state, STATE_TERMINATED)) { + break; } } } + + static boolean isCancelled(long state) { + return (state & FLAG_CANCELLED) == FLAG_CANCELLED; + } + + static boolean isWorkInProgress(long state) { + return (state & MAX_VALUE) != 0; + } + + static boolean isTerminated(long state) { + return state == STATE_TERMINATED; + } + + static boolean isSubscribedOnce(long state) { + return (state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE; + } } 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 9ea889e0d..e0234d184 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -44,7 +44,7 @@ public class ResumableDuplexConnection extends Flux final String tag; final ResumableFramesStore resumableFramesStore; - final UnboundedProcessor savableFramesSender; + final UnboundedProcessor savableFramesSender; final Disposable framesSaverDisposable; final MonoProcessor onClose; final SocketAddress remoteAddress; @@ -70,7 +70,7 @@ public ResumableDuplexConnection( this.tag = tag; this.onConnectionClosedSink = Sinks.many().unsafe().unicast().onBackpressureBuffer(); this.resumableFramesStore = resumableFramesStore; - this.savableFramesSender = new UnboundedProcessor<>(); + this.savableFramesSender = new UnboundedProcessor(); this.framesSaverDisposable = resumableFramesStore.saveFrames(savableFramesSender).subscribe(); this.onClose = MonoProcessor.create(); this.remoteAddress = initialConnection.remoteAddress(); diff --git a/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java index 7bf975543..fcbef79ee 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java @@ -16,9 +16,9 @@ package io.rsocket.internal; -import io.rsocket.Payload; -import io.rsocket.util.ByteBufPayload; -import io.rsocket.util.EmptyPayload; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; import java.util.concurrent.CountDownLatch; import org.junit.Assert; import org.junit.Test; @@ -55,10 +55,10 @@ public void testOnNextBeforeSubscribe_10_000_000() { } public void testOnNextBeforeSubscribeN(int n) { - UnboundedProcessor processor = new UnboundedProcessor<>(); + UnboundedProcessor processor = new UnboundedProcessor(); for (int i = 0; i < n; i++) { - processor.onNext(EmptyPayload.INSTANCE); + processor.onNext(Unpooled.EMPTY_BUFFER); } processor.onComplete(); @@ -85,42 +85,42 @@ public void testOnNextAfterSubscribe_1000() throws Exception { @Test public void testPrioritizedSending() { - UnboundedProcessor processor = new UnboundedProcessor<>(); + UnboundedProcessor processor = new UnboundedProcessor(); for (int i = 0; i < 1000; i++) { - processor.onNext(EmptyPayload.INSTANCE); + processor.onNext(Unpooled.EMPTY_BUFFER); } - processor.onNextPrioritized(ByteBufPayload.create("test")); + processor.onNextPrioritized(Unpooled.wrappedBuffer("test".getBytes(CharsetUtil.UTF_8))); - Payload closestPayload = processor.next().block(); + ByteBuf byteBuf = processor.next().block(); - Assert.assertEquals(closestPayload.getDataUtf8(), "test"); + Assert.assertEquals(byteBuf.toString(CharsetUtil.UTF_8), "test"); } @Test public void testPrioritizedFused() { - UnboundedProcessor processor = new UnboundedProcessor<>(); + UnboundedProcessor processor = new UnboundedProcessor(); for (int i = 0; i < 1000; i++) { - processor.onNext(EmptyPayload.INSTANCE); + processor.onNext(Unpooled.EMPTY_BUFFER); } - processor.onNextPrioritized(ByteBufPayload.create("test")); + processor.onNextPrioritized(Unpooled.wrappedBuffer("test".getBytes(CharsetUtil.UTF_8))); - Payload closestPayload = processor.poll(); + ByteBuf byteBuf = processor.poll(); - Assert.assertEquals(closestPayload.getDataUtf8(), "test"); + Assert.assertEquals(byteBuf.toString(CharsetUtil.UTF_8), "test"); } public void testOnNextAfterSubscribeN(int n) throws Exception { CountDownLatch latch = new CountDownLatch(n); - UnboundedProcessor processor = new UnboundedProcessor<>(); + UnboundedProcessor processor = new UnboundedProcessor(); processor.log().doOnNext(integer -> latch.countDown()).subscribe(); for (int i = 0; i < n; i++) { System.out.println("onNexting -> " + i); - processor.onNext(EmptyPayload.INSTANCE); + processor.onNext(Unpooled.EMPTY_BUFFER); } processor.drain(); 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 a87636365..ef15c9a09 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 @@ -16,7 +16,6 @@ package io.rsocket.transport.local; -import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.internal.UnboundedProcessor; @@ -78,8 +77,8 @@ public Mono connect() { return Mono.error(new IllegalArgumentException("Could not find server: " + name)); } - UnboundedProcessor in = new UnboundedProcessor<>(); - UnboundedProcessor out = new UnboundedProcessor<>(); + UnboundedProcessor in = new UnboundedProcessor(); + UnboundedProcessor out = new UnboundedProcessor(); MonoProcessor closeNotifier = MonoProcessor.create(); server 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 00a133969..6c1782073 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 @@ -41,7 +41,7 @@ final class LocalDuplexConnection implements DuplexConnection { private final MonoProcessor onClose; - private final UnboundedProcessor out; + private final UnboundedProcessor out; /** * Creates a new instance. @@ -56,7 +56,7 @@ final class LocalDuplexConnection implements DuplexConnection { String name, ByteBufAllocator allocator, Flux in, - UnboundedProcessor out, + UnboundedProcessor out, MonoProcessor onClose) { this.address = new LocalSocketAddress(name); this.allocator = Objects.requireNonNull(allocator, "allocator must not be null"); From 7f3da28d607cd0604767b6dae00240bf9c095436 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 30 Sep 2020 00:02:49 +0300 Subject: [PATCH 027/183] adds docs and descriptions onto resumability related internals Co-authored-by: Rossen Stoyanchev Signed-off-by: Oleh Dokuka --- .../io/rsocket/core/RSocketConnector.java | 2 +- .../rsocket/resume/ClientRSocketSession.java | 2 +- .../resume/InMemoryResumableFramesStore.java | 20 +++++++++++++++---- .../resume/ResumableDuplexConnection.java | 6 +++++- .../rsocket/resume/ServerRSocketSession.java | 5 +++++ 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index 0236adf47..05860476d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -592,7 +592,7 @@ public Mono connect(Supplier transportSupplier) { new ClientRSocketSession( resumeToken, resumableDuplexConnection, - connectionMono, // supplies pure + connectionMono, resumableClientSetup::init, resumableFramesStore, resume.getSessionDuration(), 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 cf7d793a6..9fd95ad17 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -105,7 +105,7 @@ void reconnect(int index) { if (this.s == Operators.cancelledSubscription() && S.compareAndSet(this, Operators.cancelledSubscription(), null)) { keepAliveSupport.stop(); - logger.debug("Connection[" + index + "] is lost. Reconnecting..."); + logger.debug("Connection[" + index + "] is lost. Reconnecting to resume..."); connectionFactory.retryWhen(retry).timeout(resumeSessionDuration).subscribe(this); } } 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 36d2efdbc..189799315 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java @@ -61,8 +61,23 @@ public class InMemoryResumableFramesStore extends Flux CoreSubscriber actual; - // indicates whether there is active connection or not + /** + * Indicates whether there is an active connection or not. + * + *

      + *
    • 0 - no active connection + *
    • 1 - active connection + *
    • 2 - disposed + *
    + * + *
    +   * 0 <-----> 1
    +   * |         |
    +   * +--> 2 <--+
    +   * 
    + */ volatile int state; + static final AtomicIntegerFieldUpdater STATE = AtomicIntegerFieldUpdater.newUpdater(InMemoryResumableFramesStore.class, "state"); @@ -217,8 +232,6 @@ public void onComplete() { @Override public void onNext(ByteBuf frame) { - frame.touch("Tag : " + tag + ". InMemoryResumableFramesStore:onNext"); - final int state; final boolean isResumable = isResumableFrame(frame); if (isResumable) { @@ -289,7 +302,6 @@ public void subscribe(CoreSubscriber actual) { actual.onSubscribe(this); synchronized (this) { for (final ByteBuf frame : cachedFrames) { - frame.touch("Tag : " + tag + ". InMemoryResumableFramesStore:subscribe"); actual.onNext(frame.retain()); } } 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 e0234d184..a9301b660 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -135,6 +135,11 @@ public void sendFrame(int streamId, ByteBuf frame) { } } + /** + * Publisher for a sequence of integers starting at 1, with each next number emitted when the + * currently active connection is closed and should be resumed. The Publisher never emits an error + * and completes when the connection is disposed and not resumed. + */ Flux onActiveConnectionClosed() { return onConnectionClosedSink.asFlux(); } @@ -306,7 +311,6 @@ public void onSubscribe(Subscription s) { @Override public void onNext(ByteBuf frame) { - frame.touch("Tag : " + tag + ". FrameReceivingSubscriber#onNext"); if (cancelled || s == Operators.cancelledSubscription()) { return; } 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 18ca50401..b62c615f3 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -48,6 +48,11 @@ public class ServerRSocketSession final ByteBufAllocator allocator; final boolean cleanupStoreOnKeepAlive; + /** + * All incoming connections with the Resume intent are enqueued in this queue. Such an approach + * ensure that the new connection will affect the resumption state anyhow until the previous + * (active) connection is finally closed + */ final Queue connectionsQueue; volatile int wip; From 7af1848cc50e0816b0db90cbeebaaf2cc7aada60 Mon Sep 17 00:00:00 2001 From: freelancer1845 Date: Thu, 1 Oct 2020 13:01:21 +0200 Subject: [PATCH 028/183] updates username length to align with the spec (uint8 vs uint16) (#938) Co-authored-by: Jascha Riedel --- .../rsocket/metadata/AuthMetadataCodec.java | 35 +++++++++---------- .../security/AuthMetadataFlyweightTest.java | 22 +++++++----- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java b/rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java index d908abb3c..c16c4dc52 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java @@ -12,7 +12,7 @@ public class AuthMetadataCodec { static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 - static final int USERNAME_BYTES_LENGTH = 1; + static final int USERNAME_BYTES_LENGTH = 2; static final int AUTH_TYPE_ID_LENGTH = 1; static final char[] EMPTY_CHARS_ARRAY = new char[0]; @@ -81,7 +81,7 @@ public static ByteBuf encodeMetadata( /** * Encode a Authentication CompositeMetadata payload using Simple Authentication format * - * @throws IllegalArgumentException if the username length is greater than 255 + * @throws IllegalArgumentException if the username length is greater than 65535 * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. * @param username the char sequence which represents user name. * @param password the char sequence which represents user password. @@ -90,9 +90,9 @@ public static ByteBuf encodeSimpleMetadata( ByteBufAllocator allocator, char[] username, char[] password) { int usernameLength = CharByteBufUtil.utf8Bytes(username); - if (usernameLength > 255) { + if (usernameLength > 65535) { throw new IllegalArgumentException( - "Username should be shorter than or equal to 255 bytes length in UTF-8 encoding"); + "Username should be shorter than or equal to 65535 bytes length in UTF-8 encoding"); } int passwordLength = CharByteBufUtil.utf8Bytes(password); @@ -101,7 +101,7 @@ public static ByteBuf encodeSimpleMetadata( allocator .buffer(capacity, capacity) .writeByte(WellKnownAuthType.SIMPLE.getIdentifier() | STREAM_METADATA_KNOWN_MASK) - .writeByte(usernameLength); + .writeShort(usernameLength); CharByteBufUtil.writeUtf8(buffer, username); CharByteBufUtil.writeUtf8(buffer, password); @@ -235,15 +235,15 @@ public static ByteBuf readPayload(ByteBuf metadata) { } /** - * Read up to 257 {@code bytes} from the given {@link ByteBuf} where the first byte is username - * length and the subsequent number of bytes equal to decoded length + * Read up to 65537 {@code bytes} from the given {@link ByteBuf} where the first two bytes + * represent username length and the subsequent number of bytes equal to read length * * @param simpleAuthMetadata the given metadata to read username from. Please note, the {@code - * simpleAuthMetadata#readIndex} should be set to the username length byte + * simpleAuthMetadata#readIndex} should be set to the username length position * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if username length is zero */ public static ByteBuf readUsername(ByteBuf simpleAuthMetadata) { - short usernameLength = readUsernameLength(simpleAuthMetadata); + int usernameLength = readUsernameLength(simpleAuthMetadata); if (usernameLength == 0) { return Unpooled.EMPTY_BUFFER; @@ -268,15 +268,15 @@ public static ByteBuf readPassword(ByteBuf simpleAuthMetadata) { return simpleAuthMetadata.readSlice(simpleAuthMetadata.readableBytes()); } /** - * Read up to 257 {@code bytes} from the given {@link ByteBuf} where the first byte is username - * length and the subsequent number of bytes equal to decoded length + * Read up to 65537 {@code bytes} from the given {@link ByteBuf} where the first two bytes + * represent username length and the subsequent number of bytes equal to read length * * @param simpleAuthMetadata the given metadata to read username from. Please note, the {@code * simpleAuthMetadata#readIndex} should be set to the username length byte * @return {@code char[]} which represents UTF-8 username */ public static char[] readUsernameAsCharArray(ByteBuf simpleAuthMetadata) { - short usernameLength = readUsernameLength(simpleAuthMetadata); + int usernameLength = readUsernameLength(simpleAuthMetadata); if (usernameLength == 0) { return EMPTY_CHARS_ARRAY; @@ -302,11 +302,10 @@ public static char[] readPasswordAsCharArray(ByteBuf simpleAuthMetadata) { } /** - * Read all the remaining {@code bytes} from the given {@link ByteBuf} where the first byte is - * username length and the subsequent number of bytes equal to decoded length + * Read all the remaining {@code bytes} from the given {@link ByteBuf} * * @param bearerAuthMetadata the given metadata to read username from. Please note, the {@code - * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes + * bearerAuthMetadata#readIndex} should be set to the beginning of the password bytes * @return {@code char[]} which represents UTF-8 password */ public static char[] readBearerTokenAsCharArray(ByteBuf bearerAuthMetadata) { @@ -317,13 +316,13 @@ public static char[] readBearerTokenAsCharArray(ByteBuf bearerAuthMetadata) { return CharByteBufUtil.readUtf8(bearerAuthMetadata, bearerAuthMetadata.readableBytes()); } - private static short readUsernameLength(ByteBuf simpleAuthMetadata) { - if (simpleAuthMetadata.readableBytes() < 1) { + private static int readUsernameLength(ByteBuf simpleAuthMetadata) { + if (simpleAuthMetadata.readableBytes() < 2) { throw new IllegalStateException( "Unable to decode custom username. Not enough readable bytes"); } - short usernameLength = simpleAuthMetadata.readUnsignedByte(); + int usernameLength = simpleAuthMetadata.readUnsignedShort(); if (simpleAuthMetadata.readableBytes() < usernameLength) { throw new IllegalArgumentException( diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/security/AuthMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/security/AuthMetadataFlyweightTest.java index 13d910e15..93d0f8b12 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/security/AuthMetadataFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/security/AuthMetadataFlyweightTest.java @@ -11,7 +11,7 @@ class AuthMetadataFlyweightTest { public static final int AUTH_TYPE_ID_LENGTH = 1; - public static final int USER_NAME_BYTES_LENGTH = 1; + public static final int USER_NAME_BYTES_LENGTH = 2; public static final String TEST_BEARER_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJpYXQxIjoxNTE2MjM5MDIyLCJpYXQyIjoxNTE2MjM5MDIyLCJpYXQzIjoxNTE2MjM5MDIyLCJpYXQ0IjoxNTE2MjM5MDIyfQ.ljYuH-GNyyhhLcx-rHMchRkGbNsR2_4aSxo8XjrYrSM"; @@ -82,7 +82,7 @@ private static void checkSimpleAuthMetadataEncoding( Assertions.assertThat(byteBuf.readUnsignedByte() & ~0x80) .isEqualTo(WellKnownAuthType.SIMPLE.getIdentifier()); - Assertions.assertThat(byteBuf.readUnsignedByte()).isEqualTo((short) usernameLength); + Assertions.assertThat(byteBuf.readUnsignedShort()).isEqualTo((short) usernameLength); Assertions.assertThat(byteBuf.readCharSequence(usernameLength, CharsetUtil.UTF_8)) .isEqualTo(username); @@ -116,16 +116,22 @@ private static void checkSimpleAuthMetadataEncodingUsingDecoders( @Test void shouldThrowExceptionIfUsernameLengthExitsAllowedBounds() { - String username = + StringBuilder usernameBuilder = new StringBuilder(); + String usernamePart = "𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎𠸏𠹷𠺝𠺢𠻗𠻹𠻺𠼭𠼮𠽌𠾴𠾼𠿪𡁜𡁯𡁵𡁶𡁻𡃁𡃉𡇙𢃇𢞵𢫕𢭃𢯊𢱑𢱕𢳂𢴈𢵌𢵧𢺳𣲷𤓓𤶸𤷪𥄫𦉘𦟌𦧲𦧺𧨾𨅝𨈇𨋢𨳊𨳍𨳒𩶘𠜎𠜱𠝹"; + for (int i = 0; i < 65535 / usernamePart.length(); i++) { + usernameBuilder.append(usernamePart); + } String password = "tset1234"; Assertions.assertThatThrownBy( () -> AuthMetadataFlyweight.encodeSimpleMetadata( - ByteBufAllocator.DEFAULT, username.toCharArray(), password.toCharArray())) + ByteBufAllocator.DEFAULT, + usernameBuilder.toString().toCharArray(), + password.toCharArray())) .hasMessage( - "Username should be shorter than or equal to 255 bytes length in UTF-8 encoding"); + "Username should be shorter than or equal to 65535 bytes length in UTF-8 encoding"); } @Test @@ -243,7 +249,7 @@ void shouldEncodeUsingWellKnownAuthType() { AuthMetadataFlyweight.encodeMetadata( ByteBufAllocator.DEFAULT, WellKnownAuthType.SIMPLE, - ByteBufAllocator.DEFAULT.buffer(3, 3).writeByte(1).writeByte('u').writeByte('p')); + ByteBufAllocator.DEFAULT.buffer().writeShort(1).writeByte('u').writeByte('p')); checkSimpleAuthMetadataEncoding("u", "p", 1, 1, byteBuf); } @@ -254,7 +260,7 @@ void shouldEncodeUsingWellKnownAuthType1() { AuthMetadataFlyweight.encodeMetadata( ByteBufAllocator.DEFAULT, WellKnownAuthType.SIMPLE, - ByteBufAllocator.DEFAULT.buffer().writeByte(1).writeByte('u').writeByte('p')); + ByteBufAllocator.DEFAULT.buffer().writeShort(1).writeByte('u').writeByte('p')); checkSimpleAuthMetadataEncoding("u", "p", 1, 1, byteBuf); } @@ -298,7 +304,7 @@ void shouldCompressMetadata() { AuthMetadataFlyweight.encodeMetadataWithCompression( ByteBufAllocator.DEFAULT, "simple", - ByteBufAllocator.DEFAULT.buffer().writeByte(1).writeByte('u').writeByte('p')); + ByteBufAllocator.DEFAULT.buffer().writeShort(1).writeByte('u').writeByte('p')); checkSimpleAuthMetadataEncoding("u", "p", 1, 1, byteBuf); } From 1ca3b99de8eeb7c068b6afea80f69b9939b0ec74 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 6 Oct 2020 12:43:55 +0100 Subject: [PATCH 029/183] Save iteration to cancel subscriptions Closes gh-914 Signed-off-by: Rossen Stoyanchev --- .../io/rsocket/core/RSocketResponder.java | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) 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 3e2c06e92..bae523469 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -128,18 +128,7 @@ class RSocketResponder implements RSocket { } private void handleSendProcessorError(Throwable t) { - sendingSubscriptions - .values() - .forEach( - subscription -> { - try { - subscription.cancel(); - } catch (Throwable e) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Dropped exception", t); - } - } - }); + cleanUpSendingSubscriptions(); channelProcessors .values() @@ -275,7 +264,16 @@ private void cleanup(Throwable e) { } private synchronized void cleanUpSendingSubscriptions() { - sendingSubscriptions.values().forEach(Subscription::cancel); + // Iterate explicitly to handle collisions with concurrent removals + for (IntObjectMap.PrimitiveEntry entry : sendingSubscriptions.entries()) { + try { + entry.value().cancel(); + } catch (Throwable ex) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Dropped exception", ex); + } + } + } sendingSubscriptions.clear(); } From 83d7e28d2093ebd2f59aeb3e2c29a0430ae0f43d Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 6 Oct 2020 17:46:29 +0100 Subject: [PATCH 030/183] Additional cases of map iteration and removal A follow-up fix that extends the changes in 1ca3b99de8eeb7c068b6afea80f69b9939b0ec74 See gh-914 Signed-off-by: Rossen Stoyanchev --- .../io/rsocket/core/RSocketRequester.java | 46 +++++++++--------- .../io/rsocket/core/RSocketResponder.java | 48 +++++++++---------- 2 files changed, 45 insertions(+), 49 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index a249ea888..ae9bf6e97 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -771,30 +771,28 @@ private void terminate(Throwable e) { connection.dispose(); leaseHandler.dispose(); - receivers - .values() - .forEach( - receiver -> { - try { - receiver.onError(e); - } catch (Throwable t) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Dropped exception", t); - } - } - }); - senders - .values() - .forEach( - sender -> { - try { - sender.cancel(); - } catch (Throwable t) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Dropped exception", t); - } - } - }); + // Iterate explicitly to handle collisions with concurrent removals + for (IntObjectMap.PrimitiveEntry> entry : receivers.entries()) { + try { + entry.value().onError(e); + } catch (Throwable ex) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Dropped exception", ex); + } + } + } + + // Iterate explicitly to handle collisions with concurrent removals + for (IntObjectMap.PrimitiveEntry entry : senders.entries()) { + try { + entry.value().cancel(); + } catch (Throwable ex) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Dropped exception", ex); + } + } + } + senders.clear(); receivers.clear(); sendProcessor.dispose(); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index bae523469..93a8429ab 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -27,7 +27,14 @@ import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.frame.*; +import io.rsocket.frame.CancelFrameCodec; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestNFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.SynchronizedIntObjectHashMap; import io.rsocket.internal.UnboundedProcessor; @@ -46,7 +53,11 @@ import org.slf4j.LoggerFactory; import reactor.core.Disposable; import reactor.core.Exceptions; -import reactor.core.publisher.*; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; +import reactor.core.publisher.UnicastProcessor; import reactor.util.annotation.Nullable; import reactor.util.concurrent.Queues; @@ -129,19 +140,7 @@ class RSocketResponder implements RSocket { private void handleSendProcessorError(Throwable t) { cleanUpSendingSubscriptions(); - - channelProcessors - .values() - .forEach( - subscription -> { - try { - subscription.onError(t); - } catch (Throwable e) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Dropped exception", t); - } - } - }); + cleanUpChannelProcessors(t); } private void tryTerminateOnConnectionError(Throwable e) { @@ -278,16 +277,15 @@ private synchronized void cleanUpSendingSubscriptions() { } private synchronized void cleanUpChannelProcessors(Throwable e) { - channelProcessors - .values() - .forEach( - payloadPayloadProcessor -> { - try { - payloadPayloadProcessor.onError(e); - } catch (Throwable t) { - // noops - } - }); + // Iterate explicitly to handle collisions with concurrent removals + for (IntObjectMap.PrimitiveEntry> entry : + channelProcessors.entries()) { + try { + entry.value().onError(e); + } catch (Throwable ex) { + // noops + } + } channelProcessors.clear(); } From bd3c632df449223fb96fb888478a2f9732418624 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 6 Oct 2020 17:51:53 +0100 Subject: [PATCH 031/183] To squash Signed-off-by: Rossen Stoyanchev --- .../src/main/java/io/rsocket/core/RSocketResponder.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 93a8429ab..7b67009e8 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -283,7 +283,9 @@ private synchronized void cleanUpChannelProcessors(Throwable e) { try { entry.value().onError(e); } catch (Throwable ex) { - // noops + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Dropped exception", ex); + } } } channelProcessors.clear(); From 6e823a17c4e7bc1f79e01507196b45497f7b0e4b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 8 Oct 2020 16:39:49 +0100 Subject: [PATCH 032/183] Switch to Reactor snapshots See gh-943 Signed-off-by: Rossen Stoyanchev --- build.gradle | 4 ++-- .../rsocket/resume/ResumableDuplexConnection.java | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index c2642cdac..81aef063b 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = '2020.0.0-RC1' + ext['reactor-bom.version'] = '2020.0.0-SNAPSHOT' ext['logback.version'] = '1.2.3' ext['netty-bom.version'] = '4.1.52.Final' ext['netty-boringssl.version'] = '2.0.34.Final' @@ -96,7 +96,7 @@ subprojects { mavenCentral() maven { - url 'https://repo.spring.io/libs-snapshot' + url 'https://repo.spring.io/snapshot' content { includeGroup "io.projectreactor" includeGroup "io.projectreactor.netty" 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 a9301b660..5f2e84633 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -68,7 +68,7 @@ public class ResumableDuplexConnection extends Flux public ResumableDuplexConnection( String tag, DuplexConnection initialConnection, ResumableFramesStore resumableFramesStore) { this.tag = tag; - this.onConnectionClosedSink = Sinks.many().unsafe().unicast().onBackpressureBuffer(); + this.onConnectionClosedSink = Sinks.unsafe().many().unicast().onBackpressureBuffer(); this.resumableFramesStore = resumableFramesStore; this.savableFramesSender = new UnboundedProcessor(); this.framesSaverDisposable = resumableFramesStore.saveFrames(savableFramesSender).subscribe(); @@ -114,7 +114,10 @@ void initConnection(DuplexConnection nextConnection) { __ -> { frameReceivingSubscriber.dispose(); disposable.dispose(); - onConnectionClosedSink.emitNext(currentConnectionIndex); + Sinks.Emission emission = onConnectionClosedSink.tryEmitNext(currentConnectionIndex); + if (emission.equals(Sinks.Emission.OK)) { + logger.error("Failed to notify session of closed connection: {}", emission); + } }) .subscribe(); } @@ -160,13 +163,13 @@ public void sendErrorAndClose(RSocketErrorException rSocketErrorException) { t -> { framesSaverDisposable.dispose(); savableFramesSender.dispose(); - onConnectionClosedSink.emitComplete(); + onConnectionClosedSink.tryEmitComplete(); onClose.onError(t); }, () -> { framesSaverDisposable.dispose(); savableFramesSender.dispose(); - onConnectionClosedSink.emitComplete(); + onConnectionClosedSink.tryEmitComplete(); final Throwable cause = rSocketErrorException.getCause(); if (cause == null) { onClose.onComplete(); @@ -206,7 +209,7 @@ public void dispose() { framesSaverDisposable.dispose(); activeReceivingSubscriber.dispose(); savableFramesSender.dispose(); - onConnectionClosedSink.emitComplete(); + onConnectionClosedSink.tryEmitComplete(); onClose.onComplete(); } From e27d8d092a7eefd45917a11db3f13f3bc01d0f88 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 8 Oct 2020 17:22:48 +0100 Subject: [PATCH 033/183] Adapt to more Reactor API changes See gh-943 Signed-off-by: Rossen Stoyanchev --- .../java/io/rsocket/resume/ResumableDuplexConnection.java | 8 ++++---- .../TaskProcessingWithServerSideNotificationsExample.java | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java index 5f2e84633..0b064eb84 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -114,9 +114,9 @@ void initConnection(DuplexConnection nextConnection) { __ -> { frameReceivingSubscriber.dispose(); disposable.dispose(); - Sinks.Emission emission = onConnectionClosedSink.tryEmitNext(currentConnectionIndex); - if (emission.equals(Sinks.Emission.OK)) { - logger.error("Failed to notify session of closed connection: {}", emission); + Sinks.EmitResult result = onConnectionClosedSink.tryEmitNext(currentConnectionIndex); + if (result.equals(Sinks.EmitResult.OK)) { + logger.error("Failed to notify session of closed connection: {}", result); } }) .subscribe(); diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java index 89f9950c7..992308680 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java @@ -213,10 +213,10 @@ public RSocketTaskHandler( @Override public Mono fireAndForget(Payload payload) { logger.info("Received a Task[{}] from Client.ID[{}]", payload.getDataUtf8(), id); - Sinks.Emission emission = tasksToProcess.tryEmitNext(new Task(id, payload.getDataUtf8())); + Sinks.EmitResult result = tasksToProcess.tryEmitNext(new Task(id, payload.getDataUtf8())); payload.release(); - return emission.hasFailed() - ? Mono.error(new Sinks.EmissionException(emission)) + return result.isFailure() + ? Mono.error(new Sinks.EmissionException(result)) : Mono.empty(); } From 94c8d573c7d36f2732193bda77ae7fe478271036 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 8 Oct 2020 21:00:20 +0100 Subject: [PATCH 034/183] Fix formatting style error Signed-off-by: Rossen Stoyanchev --- .../fnf/TaskProcessingWithServerSideNotificationsExample.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java index 992308680..89b22749f 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java @@ -215,9 +215,7 @@ public Mono fireAndForget(Payload payload) { logger.info("Received a Task[{}] from Client.ID[{}]", payload.getDataUtf8(), id); Sinks.EmitResult result = tasksToProcess.tryEmitNext(new Task(id, payload.getDataUtf8())); payload.release(); - return result.isFailure() - ? Mono.error(new Sinks.EmissionException(result)) - : Mono.empty(); + return result.isFailure() ? Mono.error(new Sinks.EmissionException(result)) : Mono.empty(); } @Override From 792f2dd2150baeb7ca9a4784d6f7b248441d9647 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 9 Oct 2020 21:31:05 +0300 Subject: [PATCH 035/183] provides handling of requestChannel with complete flag Signed-off-by: Oleh Dokuka --- .../main/java/io/rsocket/core/RSocketResponder.java | 11 +++++++++-- .../src/main/java/io/rsocket/frame/FrameType.java | 11 +++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) 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 a4f4c9ef9..66eb414f2 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -221,7 +221,11 @@ private void handleFrame(ByteBuf frame) { break; case REQUEST_CHANNEL: long channelInitialRequestN = RequestChannelFrameCodec.initialRequestN(frame); - handleChannel(streamId, frame, channelInitialRequestN); + handleChannel(streamId, frame, channelInitialRequestN, false); + break; + case REQUEST_CHANNEL_COMPLETE: + long completeChannelInitialRequestN = RequestChannelFrameCodec.initialRequestN(frame); + handleChannel(streamId, frame, completeChannelInitialRequestN, true); break; case METADATA_PUSH: handleMetadataPush(metadataPush(super.getPayloadDecoder().apply(frame))); @@ -345,7 +349,7 @@ private void handleStream(int streamId, ByteBuf frame, long initialRequestN) { } } - private void handleChannel(int streamId, ByteBuf frame, long initialRequestN) { + private void handleChannel(int streamId, ByteBuf frame, long initialRequestN, boolean complete) { if (FrameHeaderCodec.hasFollows(frame)) { RequestChannelResponderSubscriber subscriber = new RequestChannelResponderSubscriber(streamId, initialRequestN, frame, this, this); @@ -358,6 +362,9 @@ private void handleChannel(int streamId, ByteBuf frame, long initialRequestN) { if (this.add(streamId, subscriber)) { this.requestChannel(firstPayload, subscriber).subscribe(subscriber); + if (complete) { + subscriber.handleComplete(); + } } } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameType.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameType.java index 8ac743f87..89ee0b5cc 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameType.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameType.java @@ -195,6 +195,17 @@ public enum FrameType { /** A {@link #PAYLOAD} frame with {@code NEXT} and {@code COMPLETE} flags set. */ NEXT_COMPLETE(0xC0, Flags.CAN_HAVE_DATA | Flags.CAN_HAVE_METADATA | Flags.IS_FRAGMENTABLE), + // SYNTHETIC REQUEST_CHANNEL WITH COMPLETION + + /** A {@link #REQUEST_CHANNEL} and {@code COMPLETE} flags set. */ + REQUEST_CHANNEL_COMPLETE( + 0xD7, + Flags.CAN_HAVE_METADATA + | Flags.CAN_HAVE_DATA + | Flags.HAS_INITIAL_REQUEST_N + | Flags.IS_FRAGMENTABLE + | Flags.IS_REQUEST_TYPE), + /** * Used To Extend more frame types as well as extensions. * From e26b8500a29fdd5147859d9f703d9eb69e458901 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 12 Oct 2020 14:36:26 +0100 Subject: [PATCH 036/183] Upgrade to Reactor 2020.0.0-RC2 Closes gh-943 Signed-off-by: Rossen Stoyanchev --- build.gradle | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 81aef063b..15850ffe1 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = '2020.0.0-SNAPSHOT' + ext['reactor-bom.version'] = '2020.0.0-RC2' ext['logback.version'] = '1.2.3' ext['netty-bom.version'] = '4.1.52.Final' ext['netty-boringssl.version'] = '2.0.34.Final' @@ -95,6 +95,14 @@ subprojects { repositories { mavenCentral() + maven { + url 'https://repo.spring.io/milestone' + content { + includeGroup "io.projectreactor" + includeGroup "io.projectreactor.netty" + } + } + maven { url 'https://repo.spring.io/snapshot' content { From 858d91329ed596d14a44312686c4d5f27f306aad Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 12 Oct 2020 17:59:49 +0100 Subject: [PATCH 037/183] Mention milestone/snapshot repos for Reactor Closes gh-940 Signed-off-by: Rossen Stoyanchev --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d54a42dad..99b11babc 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,14 @@ Learn more at http://rsocket.io [![Build Status](https://travis-ci.org/rsocket/rsocket-java.svg?branch=develop)](https://travis-ci.org/rsocket/rsocket-java) -Releases are available via Maven Central. +Releases and milestones are available via Maven Central. Example: ```groovy repositories { mavenCentral() + maven { url 'https://repo.spring.io/milestone' } // Reactor milestones (if needed) } dependencies { implementation 'io.rsocket:rsocket-core:1.0.2' @@ -38,6 +39,7 @@ Example: ```groovy repositories { maven { url 'https://oss.jfrog.org/oss-snapshot-local' } + maven { url 'https://repo.spring.io/milestone' } // Reactor snapshots (if needed) } dependencies { implementation 'io.rsocket:rsocket-core:1.0.3-SNAPSHOT' From 4e379f171947566c155924681ae32f33cd1ccca8 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 12 Oct 2020 18:07:45 +0100 Subject: [PATCH 038/183] Correct repository link in README.md Signed-off-by: Rossen Stoyanchev --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 99b11babc..4f36f6a11 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Example: ```groovy repositories { - mavenCentral() + mavenCentral() maven { url 'https://repo.spring.io/milestone' } // Reactor milestones (if needed) } dependencies { @@ -38,8 +38,8 @@ Example: ```groovy repositories { - maven { url 'https://oss.jfrog.org/oss-snapshot-local' } - maven { url 'https://repo.spring.io/milestone' } // Reactor snapshots (if needed) + maven { url 'https://oss.jfrog.org/oss-snapshot-local' } + maven { url 'https://repo.spring.io/snapshot' } // Reactor snapshots (if needed) } dependencies { implementation 'io.rsocket:rsocket-core:1.0.3-SNAPSHOT' From 088cad9f40dc63545ddcbd3486412fbcca7e7ccd Mon Sep 17 00:00:00 2001 From: Sergey Tselovalnikov Date: Tue, 13 Oct 2020 22:08:25 +1100 Subject: [PATCH 039/183] use heap buffers in the default payload decoder (#945) --- .../java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java b/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java index e6874c097..0d8063e0b 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java @@ -52,12 +52,12 @@ public Payload apply(ByteBuf byteBuf) { throw new IllegalArgumentException("unsupported frame type: " + type); } - ByteBuffer data = ByteBuffer.allocateDirect(d.readableBytes()); + ByteBuffer data = ByteBuffer.allocate(d.readableBytes()); data.put(d.nioBuffer()); data.flip(); if (m != null) { - ByteBuffer metadata = ByteBuffer.allocateDirect(m.readableBytes()); + ByteBuffer metadata = ByteBuffer.allocate(m.readableBytes()); metadata.put(m.nioBuffer()); metadata.flip(); From 6241b2915a1bb331118238b65abfb4dfb45d2684 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 16 Oct 2020 00:07:59 +0300 Subject: [PATCH 040/183] improves handling requestChannel with complete flag Signed-off-by: Oleh Dokuka --- .../main/java/io/rsocket/core/RSocketResponder.java | 7 ++----- .../main/java/io/rsocket/frame/FrameHeaderCodec.java | 4 ++++ .../src/main/java/io/rsocket/frame/FrameType.java | 11 ----------- 3 files changed, 6 insertions(+), 16 deletions(-) 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 66eb414f2..3be97760b 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -221,11 +221,8 @@ private void handleFrame(ByteBuf frame) { break; case REQUEST_CHANNEL: long channelInitialRequestN = RequestChannelFrameCodec.initialRequestN(frame); - handleChannel(streamId, frame, channelInitialRequestN, false); - break; - case REQUEST_CHANNEL_COMPLETE: - long completeChannelInitialRequestN = RequestChannelFrameCodec.initialRequestN(frame); - handleChannel(streamId, frame, completeChannelInitialRequestN, true); + handleChannel( + streamId, frame, channelInitialRequestN, FrameHeaderCodec.hasComplete(frame)); break; case METADATA_PUSH: handleMetadataPush(metadataPush(super.getPayloadDecoder().apply(frame))); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderCodec.java index 28f39459d..fc146c935 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderCodec.java @@ -60,6 +60,10 @@ public static boolean hasFollows(ByteBuf byteBuf) { return (flags(byteBuf) & FLAGS_F) == FLAGS_F; } + public static boolean hasComplete(ByteBuf byteBuf) { + return (flags(byteBuf) & FLAGS_C) == FLAGS_C; + } + public static int streamId(ByteBuf byteBuf) { byteBuf.markReaderIndex(); int streamId = byteBuf.readInt(); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameType.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameType.java index 89ee0b5cc..8ac743f87 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameType.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameType.java @@ -195,17 +195,6 @@ public enum FrameType { /** A {@link #PAYLOAD} frame with {@code NEXT} and {@code COMPLETE} flags set. */ NEXT_COMPLETE(0xC0, Flags.CAN_HAVE_DATA | Flags.CAN_HAVE_METADATA | Flags.IS_FRAGMENTABLE), - // SYNTHETIC REQUEST_CHANNEL WITH COMPLETION - - /** A {@link #REQUEST_CHANNEL} and {@code COMPLETE} flags set. */ - REQUEST_CHANNEL_COMPLETE( - 0xD7, - Flags.CAN_HAVE_METADATA - | Flags.CAN_HAVE_DATA - | Flags.HAS_INITIAL_REQUEST_N - | Flags.IS_FRAGMENTABLE - | Flags.IS_REQUEST_TYPE), - /** * Used To Extend more frame types as well as extensions. * From 1398cba05bcc31ae4fde8f0ac24e8fc1773d3b8c Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 16 Oct 2020 23:45:17 +0300 Subject: [PATCH 041/183] provides request intercepting api (#944) --- rsocket-core/build.gradle | 1 + .../core/FireAndForgetRequesterMono.java | 115 ++- .../FireAndForgetResponderSubscriber.java | 53 +- .../io/rsocket/core/RSocketConnector.java | 4 +- .../io/rsocket/core/RSocketRequester.java | 16 +- .../io/rsocket/core/RSocketResponder.java | 227 ++++-- .../java/io/rsocket/core/RSocketServer.java | 4 +- .../core/RequestChannelRequesterFlux.java | 159 +++- .../RequestChannelResponderSubscriber.java | 177 +++- .../core/RequestResponseRequesterMono.java | 84 +- .../RequestResponseResponderSubscriber.java | 74 +- .../core/RequestStreamRequesterFlux.java | 72 +- .../RequestStreamResponderSubscriber.java | 131 ++- .../core/RequesterResponderSupport.java | 13 +- .../plugins/CompositeRequestInterceptor.java | 151 ++++ .../InitializingInterceptorRegistry.java | 13 +- .../rsocket/plugins/InterceptorRegistry.java | 58 +- .../rsocket/plugins/RequestInterceptor.java | 73 ++ .../core/DefaultRSocketClientTests.java | 1 + .../core/FireAndForgetRequesterMonoTest.java | 59 +- .../io/rsocket/core/RSocketLeaseTest.java | 220 ++++- .../core/RSocketRequesterSubscribersTest.java | 1 + .../io/rsocket/core/RSocketRequesterTest.java | 1 + .../io/rsocket/core/RSocketResponderTest.java | 55 +- .../java/io/rsocket/core/RSocketTest.java | 4 +- ...RequestChannelResponderSubscriberTest.java | 52 +- .../core/RequesterOperatorsRacingTest.java | 338 +++++--- .../core/ResponderOperatorsCommonTest.java | 65 +- .../io/rsocket/core/SetupRejectionTest.java | 2 + .../core/TestRequesterResponderSupport.java | 55 +- .../plugins/RequestInterceptorTest.java | 754 ++++++++++++++++++ .../plugins/TestRequestInterceptor.java | 141 ++++ 32 files changed, 2774 insertions(+), 399 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/plugins/CompositeRequestInterceptor.java create mode 100644 rsocket-core/src/main/java/io/rsocket/plugins/RequestInterceptor.java create mode 100644 rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/plugins/TestRequestInterceptor.java diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index 41adbd7a8..53a896aea 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation 'org.slf4j:slf4j-api' + testImplementation (project(":rsocket-transport-local")) testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' testImplementation 'org.junit.jupiter:junit-jupiter-api' diff --git a/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java b/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java index e51c3e75f..dec946bab 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java +++ b/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java @@ -25,6 +25,7 @@ import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.frame.FrameType; +import io.rsocket.plugins.RequestInterceptor; import java.time.Duration; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Subscription; @@ -51,6 +52,8 @@ final class FireAndForgetRequesterMono extends Mono implements Subscriptio final RequesterResponderSupport requesterResponderSupport; final DuplexConnection connection; + @Nullable final RequestInterceptor requestInterceptor; + FireAndForgetRequesterMono(Payload payload, RequesterResponderSupport requesterResponderSupport) { this.allocator = requesterResponderSupport.getAllocator(); this.payload = payload; @@ -58,14 +61,22 @@ final class FireAndForgetRequesterMono extends Mono implements Subscriptio this.maxFrameLength = requesterResponderSupport.getMaxFrameLength(); this.requesterResponderSupport = requesterResponderSupport; this.connection = requesterResponderSupport.getDuplexConnection(); + this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); } @Override public void subscribe(CoreSubscriber actual) { long previousState = markSubscribed(STATE, this); if (isSubscribedOrTerminated(previousState)) { - Operators.error( - actual, new IllegalStateException("FireAndForgetMono allows only a single Subscriber")); + final IllegalStateException e = + new IllegalStateException("FireAndForgetMono allows only a single Subscriber"); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_FNF, null); + } + + Operators.error(actual, e); return; } @@ -76,14 +87,28 @@ public void subscribe(CoreSubscriber actual) { try { if (!isValid(mtu, this.maxFrameLength, p, false)) { lazyTerminate(STATE, this); - p.release(); - actual.onError( + + final IllegalArgumentException e = new IllegalArgumentException( - String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength))); + String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_FNF, p.metadata()); + } + + p.release(); + + actual.onError(e); return; } } catch (IllegalReferenceCountException e) { lazyTerminate(STATE, this); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_FNF, null); + } + actual.onError(e); return; } @@ -93,14 +118,32 @@ public void subscribe(CoreSubscriber actual) { streamId = this.requesterResponderSupport.getNextStreamId(); } catch (Throwable t) { lazyTerminate(STATE, this); + + final Throwable ut = Exceptions.unwrap(t); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(ut, FrameType.REQUEST_FNF, p.metadata()); + } + p.release(); - actual.onError(Exceptions.unwrap(t)); + + actual.onError(ut); return; } + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onStart(streamId, FrameType.REQUEST_FNF, p.metadata()); + } + try { if (isTerminated(this.state)) { p.release(); + + if (interceptor != null) { + interceptor.onCancel(streamId); + } + return; } @@ -108,11 +151,21 @@ public void subscribe(CoreSubscriber actual) { streamId, FrameType.REQUEST_FNF, mtu, p, this.connection, this.allocator, true); } catch (Throwable e) { lazyTerminate(STATE, this); + + if (interceptor != null) { + interceptor.onTerminate(streamId, e); + } + actual.onError(e); return; } lazyTerminate(STATE, this); + + if (interceptor != null) { + interceptor.onTerminate(streamId, null); + } + actual.onComplete(); } @@ -137,19 +190,41 @@ public Void block(Duration m) { public Void block() { long previousState = markSubscribed(STATE, this); if (isSubscribedOrTerminated(previousState)) { - throw new IllegalStateException("FireAndForgetMono allows only a single Subscriber"); + final IllegalStateException e = + new IllegalStateException("FireAndForgetMono allows only a single Subscriber"); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_FNF, null); + } + throw e; } final Payload p = this.payload; try { if (!isValid(this.mtu, this.maxFrameLength, p, false)) { lazyTerminate(STATE, this); + + final IllegalArgumentException e = + new IllegalArgumentException( + String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_FNF, p.metadata()); + } + p.release(); - throw new IllegalArgumentException( - String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)); + + throw e; } } catch (IllegalReferenceCountException e) { lazyTerminate(STATE, this); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_FNF, null); + } + throw Exceptions.propagate(e); } @@ -158,10 +233,22 @@ public Void block() { streamId = this.requesterResponderSupport.getNextStreamId(); } catch (Throwable t) { lazyTerminate(STATE, this); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(Exceptions.unwrap(t), FrameType.REQUEST_FNF, p.metadata()); + } + p.release(); + throw Exceptions.propagate(t); } + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onStart(streamId, FrameType.REQUEST_FNF, p.metadata()); + } + try { sendReleasingPayload( streamId, @@ -173,10 +260,20 @@ public Void block() { true); } catch (Throwable e) { lazyTerminate(STATE, this); + + if (interceptor != null) { + interceptor.onTerminate(streamId, e); + } + throw Exceptions.propagate(e); } lazyTerminate(STATE, this); + + if (interceptor != null) { + interceptor.onTerminate(streamId, null); + } + return null; } diff --git a/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetResponderSubscriber.java b/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetResponderSubscriber.java index 3a2363d47..889c98fde 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetResponderSubscriber.java @@ -22,11 +22,13 @@ import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.plugins.RequestInterceptor; import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.CoreSubscriber; import reactor.core.publisher.Mono; +import reactor.util.annotation.Nullable; final class FireAndForgetResponderSubscriber implements CoreSubscriber, ResponderFrameHandler { @@ -42,6 +44,8 @@ final class FireAndForgetResponderSubscriber final RSocket handler; final int maxInboundPayloadSize; + @Nullable final RequestInterceptor requestInterceptor; + CompositeByteBuf frames; private FireAndForgetResponderSubscriber() { @@ -51,6 +55,19 @@ private FireAndForgetResponderSubscriber() { this.maxInboundPayloadSize = 0; this.requesterResponderSupport = null; this.handler = null; + this.requestInterceptor = null; + this.frames = null; + } + + FireAndForgetResponderSubscriber( + int streamId, RequesterResponderSupport requesterResponderSupport) { + this.streamId = streamId; + this.allocator = null; + this.payloadDecoder = null; + this.maxInboundPayloadSize = 0; + this.requesterResponderSupport = null; + this.handler = null; + this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); this.frames = null; } @@ -65,6 +82,7 @@ private FireAndForgetResponderSubscriber() { this.maxInboundPayloadSize = requesterResponderSupport.getMaxInboundPayloadSize(); this.requesterResponderSupport = requesterResponderSupport; this.handler = handler; + this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); this.frames = ReassemblyUtils.addFollowingFrame( @@ -81,11 +99,21 @@ public void onNext(Void voidVal) {} @Override public void onError(Throwable t) { + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(this.streamId, t); + } + logger.debug("Dropped Outbound error", t); } @Override - public void onComplete() {} + public void onComplete() { + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(this.streamId, null); + } + } @Override public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLastPayload) { @@ -95,11 +123,17 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas ReassemblyUtils.addFollowingFrame( frames, followingFrame, hasFollows, this.maxInboundPayloadSize); } catch (IllegalStateException t) { - this.requesterResponderSupport.remove(this.streamId, this); + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); this.frames = null; frames.release(); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, t); + } + logger.debug("Reassembly has failed", t); return; } @@ -114,6 +148,12 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas frames.release(); } catch (Throwable t) { ReferenceCountUtil.safeRelease(frames); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(this.streamId, t); + } + logger.debug("Reassembly has failed", t); return; } @@ -127,9 +167,16 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas public final void handleCancel() { final CompositeByteBuf frames = this.frames; if (frames != null) { - this.requesterResponderSupport.remove(this.streamId, this); + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); + this.frames = null; frames.release(); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onCancel(streamId); + } } } } 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 05860476d..342fd9480 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -631,6 +631,7 @@ public Mono connect(Supplier transportSupplier) { (int) keepAliveInterval.toMillis(), (int) keepAliveMaxLifeTime.toMillis(), keepAliveHandler, + interceptors::initRequesterRequestInterceptor, requesterLeaseHandler); RSocket wrappedRSocketRequester = @@ -669,7 +670,8 @@ public Mono connect(Supplier transportSupplier) { responderLeaseHandler, mtu, maxFrameLength, - maxInboundPayloadSize); + maxInboundPayloadSize, + interceptors::initResponderRequestInterceptor); return wrappedRSocketRequester; }) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 044204225..f51c14a6d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -33,8 +33,10 @@ import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.keepalive.KeepAliveSupport; import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.plugins.RequestInterceptor; import java.nio.channels.ClosedChannelException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.Function; import java.util.function.Supplier; import org.reactivestreams.Publisher; import org.slf4j.Logger; @@ -75,8 +77,16 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { int keepAliveTickPeriod, int keepAliveAckTimeout, @Nullable KeepAliveHandler keepAliveHandler, + Function requestInterceptorFunction, RequesterLeaseHandler leaseHandler) { - super(mtu, maxFrameLength, maxInboundPayloadSize, payloadDecoder, connection, streamIdSupplier); + super( + mtu, + maxFrameLength, + maxInboundPayloadSize, + payloadDecoder, + connection, + streamIdSupplier, + requestInterceptorFunction); this.leaseHandler = leaseHandler; this.onClose = MonoProcessor.create(); @@ -319,6 +329,10 @@ private void terminate(Throwable e) { keepAliveFramesAcceptor.dispose(); } getDuplexConnection().dispose(); + final RequestInterceptor requestInterceptor = getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.dispose(); + } leaseHandler.dispose(); synchronized (this) { 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 3be97760b..b8f356493 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -25,13 +25,17 @@ import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; import io.rsocket.frame.RequestNFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; import io.rsocket.frame.RequestStreamFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.plugins.RequestInterceptor; import java.nio.channels.ClosedChannelException; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.Function; import java.util.function.Supplier; import org.reactivestreams.Publisher; import org.slf4j.Logger; @@ -64,8 +68,16 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { ResponderLeaseHandler leaseHandler, int mtu, int maxFrameLength, - int maxInboundPayloadSize) { - super(mtu, maxFrameLength, maxInboundPayloadSize, payloadDecoder, connection, null); + int maxInboundPayloadSize, + Function requestInterceptorFunction) { + super( + mtu, + maxFrameLength, + maxInboundPayloadSize, + payloadDecoder, + connection, + null, + requestInterceptorFunction); this.requestHandler = requestHandler; @@ -93,7 +105,7 @@ private void tryTerminate(Supplier errorSupplier) { if (terminationError == null) { Throwable e = errorSupplier.get(); if (TERMINATION_ERROR.compareAndSet(this, null, e)) { - cleanup(); + doOnDispose(); } } } @@ -101,12 +113,7 @@ private void tryTerminate(Supplier errorSupplier) { @Override public Mono fireAndForget(Payload payload) { try { - if (leaseHandler.useLease()) { - return requestHandler.fireAndForget(payload); - } else { - payload.release(); - return Mono.error(leaseHandler.leaseError()); - } + return requestHandler.fireAndForget(payload); } catch (Throwable t) { return Mono.error(t); } @@ -115,12 +122,7 @@ public Mono fireAndForget(Payload payload) { @Override public Mono requestResponse(Payload payload) { try { - if (leaseHandler.useLease()) { - return requestHandler.requestResponse(payload); - } else { - payload.release(); - return Mono.error(leaseHandler.leaseError()); - } + return requestHandler.requestResponse(payload); } catch (Throwable t) { return Mono.error(t); } @@ -129,12 +131,7 @@ public Mono requestResponse(Payload payload) { @Override public Flux requestStream(Payload payload) { try { - if (leaseHandler.useLease()) { - return requestHandler.requestStream(payload); - } else { - payload.release(); - return Flux.error(leaseHandler.leaseError()); - } + return requestHandler.requestStream(payload); } catch (Throwable t) { return Flux.error(t); } @@ -143,24 +140,7 @@ public Flux requestStream(Payload payload) { @Override public Flux requestChannel(Publisher payloads) { try { - if (leaseHandler.useLease()) { - return requestHandler.requestChannel(payloads); - } else { - return Flux.error(leaseHandler.leaseError()); - } - } catch (Throwable t) { - return Flux.error(t); - } - } - - private Flux requestChannel(Payload payload, Publisher payloads) { - try { - if (leaseHandler.useLease()) { - return requestHandler.requestChannel(payloads); - } else { - payload.release(); - return Flux.error(leaseHandler.leaseError()); - } + return requestHandler.requestChannel(payloads); } catch (Throwable t) { return Flux.error(t); } @@ -190,10 +170,14 @@ public Mono onClose() { return getDuplexConnection().onClose(); } - private void cleanup() { + final void doOnDispose() { cleanUpSendingSubscriptions(); getDuplexConnection().dispose(); + final RequestInterceptor requestInterceptor = getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.dispose(); + } leaseHandlerDisposable.dispose(); requestHandler.dispose(); } @@ -203,7 +187,7 @@ private synchronized void cleanUpSendingSubscriptions() { activeStreams.clear(); } - private void handleFrame(ByteBuf frame) { + final void handleFrame(ByteBuf frame) { try { int streamId = FrameHeaderCodec.streamId(frame); FrameHandler receiver; @@ -302,70 +286,149 @@ private void handleFrame(ByteBuf frame) { } } - private void handleFireAndForget(int streamId, ByteBuf frame) { - if (FrameHeaderCodec.hasFollows(frame)) { - FireAndForgetResponderSubscriber subscriber = - new FireAndForgetResponderSubscriber(streamId, frame, this, this); + final void handleFireAndForget(int streamId, ByteBuf frame) { + if (leaseHandler.useLease()) { + + if (FrameHeaderCodec.hasFollows(frame)) { + final RequestInterceptor requestInterceptor = this.getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.onStart( + streamId, FrameType.REQUEST_FNF, RequestFireAndForgetFrameCodec.metadata(frame)); + } + + FireAndForgetResponderSubscriber subscriber = + new FireAndForgetResponderSubscriber(streamId, frame, this, this); - this.add(streamId, subscriber); + this.add(streamId, subscriber); + } else { + final RequestInterceptor requestInterceptor = this.getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.onStart( + streamId, FrameType.REQUEST_FNF, RequestFireAndForgetFrameCodec.metadata(frame)); + + fireAndForget(super.getPayloadDecoder().apply(frame)) + .subscribe(new FireAndForgetResponderSubscriber(streamId, this)); + } else { + fireAndForget(super.getPayloadDecoder().apply(frame)) + .subscribe(FireAndForgetResponderSubscriber.INSTANCE); + } + } } else { - fireAndForget(super.getPayloadDecoder().apply(frame)) - .subscribe(FireAndForgetResponderSubscriber.INSTANCE); + final RequestInterceptor requestTracker = this.getRequestInterceptor(); + if (requestTracker != null) { + requestTracker.onReject( + leaseHandler.leaseError(), + FrameType.REQUEST_FNF, + RequestFireAndForgetFrameCodec.metadata(frame)); + } } } - private void handleRequestResponse(int streamId, ByteBuf frame) { - if (FrameHeaderCodec.hasFollows(frame)) { - RequestResponseResponderSubscriber subscriber = - new RequestResponseResponderSubscriber(streamId, frame, this, this); + final void handleRequestResponse(int streamId, ByteBuf frame) { + if (leaseHandler.useLease()) { + final RequestInterceptor requestInterceptor = this.getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.onStart( + streamId, FrameType.REQUEST_RESPONSE, RequestResponseFrameCodec.metadata(frame)); + } - this.add(streamId, subscriber); - } else { - RequestResponseResponderSubscriber subscriber = - new RequestResponseResponderSubscriber(streamId, this); + if (FrameHeaderCodec.hasFollows(frame)) { + RequestResponseResponderSubscriber subscriber = + new RequestResponseResponderSubscriber(streamId, frame, this, this); + + this.add(streamId, subscriber); + } else { + RequestResponseResponderSubscriber subscriber = + new RequestResponseResponderSubscriber(streamId, this); - if (this.add(streamId, subscriber)) { - this.requestResponse(super.getPayloadDecoder().apply(frame)).subscribe(subscriber); + if (this.add(streamId, subscriber)) { + this.requestResponse(super.getPayloadDecoder().apply(frame)).subscribe(subscriber); + } + } + } else { + final Exception leaseError = leaseHandler.leaseError(); + final RequestInterceptor requestInterceptor = this.getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.onReject( + leaseError, FrameType.REQUEST_RESPONSE, RequestResponseFrameCodec.metadata(frame)); } + sendLeaseRejection(streamId, leaseError); } } - private void handleStream(int streamId, ByteBuf frame, long initialRequestN) { - if (FrameHeaderCodec.hasFollows(frame)) { - RequestStreamResponderSubscriber subscriber = - new RequestStreamResponderSubscriber(streamId, initialRequestN, frame, this, this); + final void handleStream(int streamId, ByteBuf frame, long initialRequestN) { + if (leaseHandler.useLease()) { + final RequestInterceptor requestInterceptor = this.getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.onStart( + streamId, FrameType.REQUEST_STREAM, RequestStreamFrameCodec.metadata(frame)); + } - this.add(streamId, subscriber); - } else { - RequestStreamResponderSubscriber subscriber = - new RequestStreamResponderSubscriber(streamId, initialRequestN, this); + if (FrameHeaderCodec.hasFollows(frame)) { + RequestStreamResponderSubscriber subscriber = + new RequestStreamResponderSubscriber(streamId, initialRequestN, frame, this, this); - if (this.add(streamId, subscriber)) { - this.requestStream(super.getPayloadDecoder().apply(frame)).subscribe(subscriber); + this.add(streamId, subscriber); + } else { + RequestStreamResponderSubscriber subscriber = + new RequestStreamResponderSubscriber(streamId, initialRequestN, this); + + if (this.add(streamId, subscriber)) { + this.requestStream(super.getPayloadDecoder().apply(frame)).subscribe(subscriber); + } } + } else { + final Exception leaseError = leaseHandler.leaseError(); + final RequestInterceptor requestInterceptor = this.getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.onReject( + leaseError, FrameType.REQUEST_STREAM, RequestStreamFrameCodec.metadata(frame)); + } + sendLeaseRejection(streamId, leaseError); } } - private void handleChannel(int streamId, ByteBuf frame, long initialRequestN, boolean complete) { - if (FrameHeaderCodec.hasFollows(frame)) { - RequestChannelResponderSubscriber subscriber = - new RequestChannelResponderSubscriber(streamId, initialRequestN, frame, this, this); + final void handleChannel(int streamId, ByteBuf frame, long initialRequestN, boolean complete) { + if (leaseHandler.useLease()) { + final RequestInterceptor requestInterceptor = this.getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.onStart( + streamId, FrameType.REQUEST_CHANNEL, RequestChannelFrameCodec.metadata(frame)); + } + + if (FrameHeaderCodec.hasFollows(frame)) { + RequestChannelResponderSubscriber subscriber = + new RequestChannelResponderSubscriber(streamId, initialRequestN, frame, this, this); - this.add(streamId, subscriber); - } else { - final Payload firstPayload = super.getPayloadDecoder().apply(frame); - RequestChannelResponderSubscriber subscriber = - new RequestChannelResponderSubscriber(streamId, initialRequestN, firstPayload, this); - - if (this.add(streamId, subscriber)) { - this.requestChannel(firstPayload, subscriber).subscribe(subscriber); - if (complete) { - subscriber.handleComplete(); + this.add(streamId, subscriber); + } else { + final Payload firstPayload = super.getPayloadDecoder().apply(frame); + RequestChannelResponderSubscriber subscriber = + new RequestChannelResponderSubscriber(streamId, initialRequestN, firstPayload, this); + + if (this.add(streamId, subscriber)) { + this.requestChannel(subscriber).subscribe(subscriber); + if (complete) { + subscriber.handleComplete(); + } } } + } else { + final Exception leaseError = leaseHandler.leaseError(); + final RequestInterceptor requestTracker = this.getRequestInterceptor(); + if (requestTracker != null) { + requestTracker.onReject( + leaseError, FrameType.REQUEST_CHANNEL, RequestChannelFrameCodec.metadata(frame)); + } + sendLeaseRejection(streamId, leaseError); } } + private void sendLeaseRejection(int streamId, Throwable leaseError) { + getDuplexConnection() + .sendFrame(streamId, ErrorFrameCodec.encode(getAllocator(), streamId, leaseError)); + } + private void handleMetadataPush(Mono result) { result.subscribe(MetadataPushResponderSubscriber.INSTANCE); } 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 258306cd2..b1c93f206 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -420,6 +420,7 @@ private Mono acceptSetup( setupPayload.keepAliveInterval(), setupPayload.keepAliveMaxLifetime(), keepAliveHandler, + interceptors::initRequesterRequestInterceptor, requesterLeaseHandler); RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); @@ -451,7 +452,8 @@ private Mono acceptSetup( responderLeaseHandler, mtu, maxFrameLength, - maxInboundPayloadSize); + maxInboundPayloadSize, + interceptors::initResponderRequestInterceptor); }) .doFinally(signalType -> setupPayload.release()) .then(); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java index 722a7c2c5..8a57820c5 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java @@ -34,12 +34,15 @@ import io.rsocket.frame.PayloadFrameCodec; import io.rsocket.frame.RequestNFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.plugins.RequestInterceptor; import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; -import reactor.core.*; +import reactor.core.CoreSubscriber; +import reactor.core.Exceptions; +import reactor.core.Scannable; import reactor.core.publisher.Flux; import reactor.core.publisher.Operators; import reactor.util.annotation.NonNull; @@ -59,6 +62,8 @@ final class RequestChannelRequesterFlux extends Flux final Publisher payloadsPublisher; + @Nullable final RequestInterceptor requestInterceptor; + volatile long state; static final AtomicLongFieldUpdater STATE = AtomicLongFieldUpdater.newUpdater(RequestChannelRequesterFlux.class, "state"); @@ -86,6 +91,7 @@ final class RequestChannelRequesterFlux extends Flux this.requesterResponderSupport = requesterResponderSupport; this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); + this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); } @Override @@ -94,8 +100,14 @@ public void subscribe(CoreSubscriber actual) { long previousState = markSubscribed(STATE, this); if (isSubscribedOrTerminated(previousState)) { - Operators.error( - actual, new IllegalStateException("RequestChannelFlux allows only a single Subscriber")); + final IllegalStateException e = + new IllegalStateException("RequestChannelFlux allows only a single Subscriber"); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_CHANNEL, null); + } + + Operators.error(actual, e); return; } @@ -163,13 +175,20 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { if (!isValid(mtu, this.maxFrameLength, firstPayload, true)) { lazyTerminate(STATE, this); - firstPayload.release(); this.outboundSubscription.cancel(); - this.inboundDone = true; - this.inboundSubscriber.onError( + final IllegalArgumentException e = new IllegalArgumentException( - String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength))); + String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_CHANNEL, firstPayload.metadata()); + } + + firstPayload.release(); + + this.inboundDone = true; + this.inboundSubscriber.onError(e); return; } } catch (IllegalReferenceCountException e) { @@ -177,6 +196,11 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { this.outboundSubscription.cancel(); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_CHANNEL, null); + } + this.inboundDone = true; this.inboundSubscriber.onError(e); return; @@ -194,15 +218,27 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { this.inboundDone = true; final long previousState = markTerminated(STATE, this); - firstPayload.release(); this.outboundSubscription.cancel(); + final Throwable ut = Exceptions.unwrap(t); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(ut, FrameType.REQUEST_CHANNEL, firstPayload.metadata()); + } + + firstPayload.release(); + if (!isTerminated(previousState)) { - this.inboundSubscriber.onError(Exceptions.unwrap(t)); + this.inboundSubscriber.onError(ut); } return; } + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onStart(streamId, FrameType.REQUEST_CHANNEL, firstPayload.metadata()); + } + try { sendReleasingPayload( streamId, @@ -215,14 +251,19 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { // TODO: Should be a different flag in case of the scalar // source or if we know in advance upstream is mono false); - } catch (Throwable e) { + } catch (Throwable t) { lazyTerminate(STATE, this); sm.remove(streamId, this); this.outboundSubscription.cancel(); this.inboundDone = true; - this.inboundSubscriber.onError(e); + + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, t); + } + + this.inboundSubscriber.onError(t); return; } @@ -239,6 +280,9 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { final ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, streamId); connection.sendFrame(streamId, cancelFrame); + if (requestInterceptor != null) { + requestInterceptor.onCancel(streamId); + } return; } @@ -268,16 +312,22 @@ final void sendFollowingPayload(Payload followingPayload) { if (!isValid(mtu, this.maxFrameLength, followingPayload, true)) { followingPayload.release(); - this.cancel(); - final IllegalArgumentException e = new IllegalArgumentException( String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)); + if (!this.tryCancel()) { + Operators.onErrorDropped(e, this.inboundSubscriber.currentContext()); + return; + } + this.propagateErrorSafely(e); return; } } catch (IllegalReferenceCountException e) { - this.cancel(); + if (!this.tryCancel()) { + Operators.onErrorDropped(e, this.inboundSubscriber.currentContext()); + return; + } this.propagateErrorSafely(e); @@ -297,41 +347,60 @@ final void sendFollowingPayload(Payload followingPayload) { allocator, true); } catch (Throwable e) { - this.cancel(); + if (!this.tryCancel()) { + Operators.onErrorDropped(e, this.inboundSubscriber.currentContext()); + return; + } this.propagateErrorSafely(e); } } - void propagateErrorSafely(Throwable e) { + void propagateErrorSafely(Throwable t) { // FIXME: must be scheduled on the connection event-loop to achieve serial // behaviour on the inbound subscriber if (!this.inboundDone) { synchronized (this) { if (!this.inboundDone) { + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(this.streamId, t); + } + this.inboundDone = true; - this.inboundSubscriber.onError(e); + this.inboundSubscriber.onError(t); } else { - Operators.onErrorDropped(e, this.inboundSubscriber.currentContext()); + Operators.onErrorDropped(t, this.inboundSubscriber.currentContext()); } } } else { - Operators.onErrorDropped(e, this.inboundSubscriber.currentContext()); + Operators.onErrorDropped(t, this.inboundSubscriber.currentContext()); } } @Override public final void cancel() { + if (!tryCancel()) { + return; + } + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onCancel(this.streamId); + } + } + + boolean tryCancel() { long previousState = markTerminated(STATE, this); if (isTerminated(previousState)) { - return; + return false; } this.outboundSubscription.cancel(); if (!isFirstFrameSent(previousState)) { // no need to send anything, since we have not started a stream yet (no logical wire) - return; + return false; } final int streamId = this.streamId; @@ -341,6 +410,8 @@ public final void cancel() { final ByteBuf cancelFrame = CancelFrameCodec.encode(this.allocator, streamId); this.connection.sendFrame(streamId, cancelFrame); + + return true; } @Override @@ -376,6 +447,11 @@ public void onError(Throwable t) { // FIXME: must be scheduled on the connection event-loop to achieve serial // behaviour on the inbound subscriber synchronized (this) { + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, t); + } + this.inboundDone = true; this.inboundSubscriber.onError(t); } @@ -405,12 +481,20 @@ public void onComplete() { final int streamId = this.streamId; - if (isInboundTerminated(previousState)) { + final boolean isInboundTerminated = isInboundTerminated(previousState); + if (isInboundTerminated) { this.requesterResponderSupport.remove(streamId, this); } final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(this.allocator, streamId); this.connection.sendFrame(streamId, completeFrame); + + if (isInboundTerminated) { + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, null); + } + } } @Override @@ -428,6 +512,11 @@ public final void handleComplete() { if (isOutboundTerminated(previousState)) { this.requesterResponderSupport.remove(this.streamId, this); + + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, null); + } } this.inboundSubscriber.onComplete(); @@ -443,7 +532,15 @@ public final void handleError(Throwable cause) { this.inboundDone = true; long previousState = markTerminated(STATE, this); - if (isTerminated(previousState) || isInboundTerminated(previousState)) { + if (isTerminated(previousState)) { + Operators.onErrorDropped(cause, this.inboundSubscriber.currentContext()); + return; + } else if (isInboundTerminated(previousState)) { + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(this.streamId, cause); + } + Operators.onErrorDropped(cause, this.inboundSubscriber.currentContext()); return; } @@ -455,6 +552,12 @@ public final void handleError(Throwable cause) { this.requesterResponderSupport.remove(streamId, this); this.outboundSubscription.cancel(); + + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, cause); + } + this.inboundSubscriber.onError(cause); } @@ -486,11 +589,19 @@ public void handleCancel() { return; } - if (isInboundTerminated(previousState)) { + final boolean inboundTerminated = isInboundTerminated(previousState); + if (inboundTerminated) { this.requesterResponderSupport.remove(this.streamId, this); } this.outboundSubscription.cancel(); + + if (inboundTerminated) { + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(this.streamId, null); + } + } } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java index 67816407c..9d4cd5f1e 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java @@ -36,6 +36,7 @@ import io.rsocket.frame.PayloadFrameCodec; import io.rsocket.frame.RequestNFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.plugins.RequestInterceptor; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; @@ -46,6 +47,7 @@ import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Operators; +import reactor.util.annotation.Nullable; import reactor.util.context.Context; final class RequestChannelResponderSubscriber extends Flux @@ -63,6 +65,8 @@ final class RequestChannelResponderSubscriber extends Flux final DuplexConnection connection; final long firstRequest; + @Nullable final RequestInterceptor requestInterceptor; + final RSocket handler; volatile long state; @@ -99,6 +103,7 @@ public RequestChannelResponderSubscriber( this.requesterResponderSupport = requesterResponderSupport; this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); + this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); this.handler = handler; this.firstRequest = firstRequestN; @@ -121,6 +126,7 @@ public RequestChannelResponderSubscriber( this.requesterResponderSupport = requesterResponderSupport; this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); + this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); this.firstRequest = firstRequestN; this.firstPayload = firstPayload; @@ -293,12 +299,20 @@ public void cancel() { final int streamId = this.streamId; - if (isOutboundTerminated(previousState)) { + final boolean isOutboundTerminated = isOutboundTerminated(previousState); + if (isOutboundTerminated) { this.requesterResponderSupport.remove(streamId, this); } final ByteBuf cancelFrame = CancelFrameCodec.encode(this.allocator, streamId); this.connection.sendFrame(streamId, cancelFrame); + + if (isOutboundTerminated) { + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, null); + } + } } @Override @@ -320,10 +334,23 @@ public final void handleCancel() { this.firstPayload = null; firstPayload.release(); } + + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onCancel(this.streamId); + } + return; + } + + long previousState = this.tryTerminate(true); + if (isTerminated(previousState)) { return; } - this.tryTerminate(true); + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onCancel(this.streamId); + } } final long tryTerminate(boolean isFromInbound) { @@ -434,6 +461,11 @@ public final void handleError(Throwable t) { // reached it // needs for disconnected upstream and downstream case this.outboundSubscription.cancel(); + + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(this.streamId, t); + } } @Override @@ -446,13 +478,21 @@ public void handleComplete() { long previousState = markInboundTerminated(STATE, this); - if (isOutboundTerminated(previousState)) { + final boolean isOutboundTerminated = isOutboundTerminated(previousState); + if (isOutboundTerminated) { this.requesterResponderSupport.remove(this.streamId, this); } if (isFirstFrameSent(previousState)) { this.inboundSubscriber.onComplete(); } + + if (isOutboundTerminated) { + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(this.streamId, null); + } + } } @Override @@ -468,7 +508,15 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) payload = this.payloadDecoder.apply(frame); } catch (Throwable t) { long previousState = this.tryTerminate(true); - if (isTerminated(previousState) || isOutboundTerminated(previousState)) { + if (isTerminated(previousState)) { + Operators.onErrorDropped(t, this.inboundSubscriber.currentContext()); + return; + } else if (isOutboundTerminated(previousState)) { + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(this.streamId, t); + } + Operators.onErrorDropped(t, this.inboundSubscriber.currentContext()); return; } @@ -480,6 +528,10 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) ErrorFrameCodec.encode(this.allocator, streamId, new CanceledException(t.getMessage())); this.connection.sendFrame(streamId, errorFrame); + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, t); + } return; } @@ -514,7 +566,15 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) } long previousState = this.tryTerminate(true); - if (isTerminated(previousState) || isOutboundTerminated(previousState)) { + if (isTerminated(previousState)) { + Operators.onErrorDropped(e, this.inboundSubscriber.currentContext()); + return; + } else if (isOutboundTerminated(previousState)) { + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(this.streamId, e); + } + Operators.onErrorDropped(e, this.inboundSubscriber.currentContext()); return; } @@ -529,6 +589,11 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) new CanceledException("Failed to reassemble payload. Cause: " + e.getMessage())); this.connection.sendFrame(streamId, errorFrame); + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, e); + } + return; } } @@ -549,7 +614,15 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) ReferenceCountUtil.safeRelease(frames); previousState = this.tryTerminate(true); - if (isTerminated(previousState) || isOutboundTerminated(previousState)) { + if (isTerminated(previousState)) { + Operators.onErrorDropped(t, this.inboundSubscriber.currentContext()); + return; + } else if (isOutboundTerminated(previousState)) { + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(this.streamId, t); + } + Operators.onErrorDropped(t, this.inboundSubscriber.currentContext()); return; } @@ -563,6 +636,11 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) new CanceledException("Failed to reassemble payload. Cause: " + t.getMessage())); this.connection.sendFrame(streamId, errorFrame); + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, t); + } + return; } @@ -591,12 +669,6 @@ public void onNext(Payload p) { final DuplexConnection connection = this.connection; final ByteBufAllocator allocator = this.allocator; - if (p == null) { - final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(allocator, streamId); - connection.sendFrame(streamId, completeFrame); - return; - } - final int mtu = this.mtu; try { if (!isValid(mtu, this.maxFrameLength, p, false)) { @@ -605,21 +677,36 @@ public void onNext(Payload p) { // FIXME: must be scheduled on the connection event-loop to achieve serial // behaviour on the inbound subscriber long previousState = this.tryTerminate(false); - if (isTerminated(previousState) || isOutboundTerminated(previousState)) { + if (isTerminated(previousState)) { Operators.onErrorDropped( new IllegalArgumentException( String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)), this.inboundSubscriber.currentContext()); return; + } else if (isOutboundTerminated(previousState)) { + final IllegalArgumentException e = + new IllegalArgumentException( + String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)); + + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, e); + } + + Operators.onErrorDropped(e, this.inboundSubscriber.currentContext()); + return; } - final ByteBuf errorFrame = - ErrorFrameCodec.encode( - allocator, - streamId, - new CanceledException( - String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength))); + final CanceledException e = + new CanceledException( + String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)); + final ByteBuf errorFrame = ErrorFrameCodec.encode(allocator, streamId, e); connection.sendFrame(streamId, errorFrame); + + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, e); + } return; } } catch (IllegalReferenceCountException e) { @@ -627,7 +714,15 @@ public void onNext(Payload p) { // FIXME: must be scheduled on the connection event-loop to achieve serial // behaviour on the inbound subscriber long previousState = this.tryTerminate(false); - if (isTerminated(previousState) || isOutboundTerminated(previousState)) { + if (isTerminated(previousState)) { + Operators.onErrorDropped(e, this.inboundSubscriber.currentContext()); + return; + } else if (isOutboundTerminated(previousState)) { + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, e); + } + Operators.onErrorDropped(e, this.inboundSubscriber.currentContext()); return; } @@ -638,6 +733,11 @@ public void onNext(Payload p) { streamId, new CanceledException("Failed to validate payload. Cause:" + e.getMessage())); connection.sendFrame(streamId, errorFrame); + + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, e); + } return; } @@ -646,7 +746,11 @@ public void onNext(Payload p) { } catch (Throwable t) { // FIXME: must be scheduled on the connection event-loop to achieve serial // behaviour on the inbound subscriber - this.tryTerminate(false); + long previousState = this.tryTerminate(false); + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null && !isTerminated(previousState)) { + interceptor.onTerminate(streamId, t); + } } } @@ -682,15 +786,13 @@ public void onError(Throwable t) { } } - if (!isFirstFrameSent(previousState)) { - if (!hasRequested(previousState)) { - final Payload firstPayload = this.firstPayload; - this.firstPayload = null; - firstPayload.release(); - } - } - - if (wasThrowableAdded && !isInboundTerminated(previousState)) { + if (!isSubscribed(previousState)) { + final Payload firstPayload = this.firstPayload; + this.firstPayload = null; + firstPayload.release(); + } else if (wasThrowableAdded + && isFirstFrameSent(previousState) + && !isInboundTerminated(previousState)) { Throwable inboundError = Exceptions.terminate(INBOUND_ERROR, this); if (inboundError != TERMINATED) { // FIXME: must be scheduled on the connection event-loop to achieve serial @@ -705,6 +807,11 @@ public void onError(Throwable t) { final ByteBuf errorFrame = ErrorFrameCodec.encode(this.allocator, streamId, t); this.connection.sendFrame(streamId, errorFrame); + + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, t); + } } @Override @@ -722,12 +829,20 @@ public void onComplete() { final int streamId = this.streamId; - if (isInboundTerminated(previousState)) { + final boolean isInboundTerminated = isInboundTerminated(previousState); + if (isInboundTerminated) { this.requesterResponderSupport.remove(streamId, this); } final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(this.allocator, streamId); this.connection.sendFrame(streamId, completeFrame); + + if (isInboundTerminated) { + final RequestInterceptor interceptor = this.requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(streamId, null); + } + } } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java index 1706ece32..f3c52f648 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java @@ -30,6 +30,7 @@ import io.rsocket.frame.CancelFrameCodec; import io.rsocket.frame.FrameType; import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.plugins.RequestInterceptor; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; @@ -52,6 +53,8 @@ final class RequestResponseRequesterMono extends Mono final DuplexConnection connection; final PayloadDecoder payloadDecoder; + @Nullable final RequestInterceptor requestInterceptor; + volatile long state; static final AtomicLongFieldUpdater STATE = AtomicLongFieldUpdater.newUpdater(RequestResponseRequesterMono.class, "state"); @@ -72,6 +75,7 @@ final class RequestResponseRequesterMono extends Mono this.requesterResponderSupport = requesterResponderSupport; this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); + this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); } @Override @@ -79,8 +83,14 @@ public void subscribe(CoreSubscriber actual) { long previousState = markSubscribed(STATE, this); if (isSubscribedOrTerminated(previousState)) { - Operators.error( - actual, new IllegalStateException("RequestResponseMono allows only a single Subscriber")); + final IllegalStateException e = + new IllegalStateException("RequestResponseMono allows only a single " + "Subscriber"); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_RESPONSE, null); + } + + Operators.error(actual, e); return; } @@ -88,15 +98,28 @@ public void subscribe(CoreSubscriber actual) { try { if (!isValid(this.mtu, this.maxFrameLength, p, false)) { lazyTerminate(STATE, this); - Operators.error( - actual, + + final IllegalArgumentException e = new IllegalArgumentException( - String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength))); + String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_RESPONSE, p.metadata()); + } + p.release(); + + Operators.error(actual, e); return; } } catch (IllegalReferenceCountException e) { lazyTerminate(STATE, this); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_RESPONSE, null); + } + Operators.error(actual, e); return; } @@ -133,14 +156,25 @@ void sendFirstPayload(Payload payload, long initialRequestN) { this.done = true; final long previousState = markTerminated(STATE, this); + final Throwable ut = Exceptions.unwrap(t); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(ut, FrameType.REQUEST_RESPONSE, payload.metadata()); + } + payload.release(); if (!isTerminated(previousState)) { - this.actual.onError(Exceptions.unwrap(t)); + this.actual.onError(ut); } return; } + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onStart(streamId, FrameType.REQUEST_RESPONSE, payload.metadata()); + } + try { sendReleasingPayload( streamId, FrameType.REQUEST_RESPONSE, this.mtu, payload, connection, allocator, true); @@ -150,6 +184,10 @@ void sendFirstPayload(Payload payload, long initialRequestN) { sm.remove(streamId, this); + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, e); + } + this.actual.onError(e); return; } @@ -164,6 +202,10 @@ void sendFirstPayload(Payload payload, long initialRequestN) { final ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, streamId); connection.sendFrame(streamId, cancelFrame); + + if (requestInterceptor != null) { + requestInterceptor.onCancel(streamId); + } } } @@ -181,6 +223,11 @@ public final void cancel() { ReassemblyUtils.synchronizedRelease(this, previousState); this.connection.sendFrame(streamId, CancelFrameCodec.encode(this.allocator, streamId)); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onCancel(streamId); + } } else if (!hasRequested(previousState)) { this.payload.release(); } @@ -201,10 +248,15 @@ public final void handlePayload(Payload value) { return; } - final CoreSubscriber a = this.actual; + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); - this.requesterResponderSupport.remove(this.streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, null); + } + final CoreSubscriber a = this.actual; a.onNext(value); a.onComplete(); } @@ -222,7 +274,13 @@ public final void handleComplete() { return; } - this.requesterResponderSupport.remove(this.streamId, this); + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, null); + } this.actual.onComplete(); } @@ -244,7 +302,13 @@ public final void handleError(Throwable cause) { ReassemblyUtils.synchronizedRelease(this, previousState); - this.requesterResponderSupport.remove(this.streamId, this); + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, cause); + } this.actual.onError(cause); } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java index f36211c7d..648afff13 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java @@ -32,6 +32,7 @@ import io.rsocket.frame.FrameType; import io.rsocket.frame.PayloadFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.plugins.RequestInterceptor; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.reactivestreams.Subscription; import org.slf4j.Logger; @@ -55,9 +56,10 @@ final class RequestResponseResponderSubscriber final int maxInboundPayloadSize; final RequesterResponderSupport requesterResponderSupport; final DuplexConnection connection; - final RSocket handler; + @Nullable final RequestInterceptor requestInterceptor; + boolean done; CompositeByteBuf frames; @@ -79,7 +81,9 @@ public RequestResponseResponderSubscriber( this.requesterResponderSupport = requesterResponderSupport; this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); + this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); this.handler = handler; + this.frames = ReassemblyUtils.addFollowingFrame( allocator.compositeBuffer(), firstFrame, true, maxInboundPayloadSize); @@ -94,6 +98,7 @@ public RequestResponseResponderSubscriber( this.maxInboundPayloadSize = requesterResponderSupport.getMaxInboundPayloadSize(); this.requesterResponderSupport = requesterResponderSupport; this.connection = requesterResponderSupport.getDuplexConnection(); + this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); this.payloadDecoder = null; this.handler = null; @@ -137,6 +142,11 @@ public void onNext(@Nullable Payload p) { if (p == null) { final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(allocator, streamId); connection.sendFrame(streamId, completeFrame); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, null); + } return; } @@ -147,13 +157,16 @@ public void onNext(@Nullable Payload p) { p.release(); - final ByteBuf errorFrame = - ErrorFrameCodec.encode( - allocator, - streamId, - new CanceledException( - String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength))); + final CanceledException e = + new CanceledException( + String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)); + final ByteBuf errorFrame = ErrorFrameCodec.encode(allocator, streamId, e); connection.sendFrame(streamId, errorFrame); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, e); + } return; } } catch (IllegalReferenceCountException e) { @@ -165,13 +178,28 @@ public void onNext(@Nullable Payload p) { streamId, new CanceledException("Failed to validate payload. Cause" + e.getMessage())); connection.sendFrame(streamId, errorFrame); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, e); + } return; } try { sendReleasingPayload(streamId, FrameType.NEXT_COMPLETE, mtu, p, connection, allocator, false); - } catch (Throwable ignored) { + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, null); + } + } catch (Throwable t) { currentSubscription.cancel(); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, t); + } } } @@ -197,6 +225,11 @@ public void onError(Throwable t) { final ByteBuf errorFrame = ErrorFrameCodec.encode(this.allocator, streamId, t); this.connection.sendFrame(streamId, errorFrame); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, t); + } } @Override @@ -216,7 +249,8 @@ public void handleCancel() { // and fragmentation of the first frame was cancelled before S.lazySet(this, Operators.cancelledSubscription()); - this.requesterResponderSupport.remove(this.streamId, this); + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); final CompositeByteBuf frames = this.frames; if (frames != null) { @@ -224,6 +258,10 @@ public void handleCancel() { frames.release(); } + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onCancel(streamId); + } return; } @@ -231,9 +269,15 @@ public void handleCancel() { return; } - this.requesterResponderSupport.remove(this.streamId, this); + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); currentSubscription.cancel(); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onCancel(streamId); + } } @Override @@ -263,6 +307,11 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) streamId, new CanceledException("Failed to reassemble payload. Cause: " + t.getMessage())); this.connection.sendFrame(streamId, errorFrame); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, t); + } return; } @@ -289,6 +338,11 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) streamId, new CanceledException("Failed to reassemble payload. Cause: " + t.getMessage())); this.connection.sendFrame(streamId, errorFrame); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, t); + } return; } 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 a3107d4d6..3608eaf52 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java @@ -31,6 +31,7 @@ import io.rsocket.frame.FrameType; import io.rsocket.frame.RequestNFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.plugins.RequestInterceptor; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; @@ -53,6 +54,8 @@ final class RequestStreamRequesterFlux extends Flux final DuplexConnection connection; final PayloadDecoder payloadDecoder; + @Nullable final RequestInterceptor requestInterceptor; + volatile long state; static final AtomicLongFieldUpdater STATE = AtomicLongFieldUpdater.newUpdater(RequestStreamRequesterFlux.class, "state"); @@ -71,14 +74,21 @@ final class RequestStreamRequesterFlux extends Flux this.requesterResponderSupport = requesterResponderSupport; this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); + this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); } @Override public void subscribe(CoreSubscriber actual) { long previousState = markSubscribed(STATE, this); if (isSubscribedOrTerminated(previousState)) { - Operators.error( - actual, new IllegalStateException("RequestStreamFlux allows only a single Subscriber")); + final IllegalStateException e = + new IllegalStateException("RequestStreamFlux allows only a single Subscriber"); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_STREAM, null); + } + + Operators.error(actual, e); return; } @@ -86,15 +96,28 @@ public void subscribe(CoreSubscriber actual) { try { if (!isValid(this.mtu, this.maxFrameLength, p, false)) { lazyTerminate(STATE, this); - Operators.error( - actual, + + final IllegalArgumentException e = new IllegalArgumentException( - String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength))); + String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_STREAM, p.metadata()); + } + p.release(); + + Operators.error(actual, e); return; } } catch (IllegalReferenceCountException e) { lazyTerminate(STATE, this); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(e, FrameType.REQUEST_STREAM, null); + } + Operators.error(actual, e); return; } @@ -141,14 +164,25 @@ void sendFirstPayload(Payload payload, long initialRequestN) { this.done = true; final long previousState = markTerminated(STATE, this); + final Throwable ut = Exceptions.unwrap(t); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onReject(ut, FrameType.REQUEST_STREAM, payload.metadata()); + } + payload.release(); if (!isTerminated(previousState)) { - this.inboundSubscriber.onError(Exceptions.unwrap(t)); + this.inboundSubscriber.onError(ut); } return; } + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onStart(streamId, FrameType.REQUEST_STREAM, payload.metadata()); + } + try { sendReleasingPayload( streamId, @@ -159,13 +193,17 @@ void sendFirstPayload(Payload payload, long initialRequestN) { connection, allocator, false); - } catch (Throwable e) { + } catch (Throwable t) { this.done = true; lazyTerminate(STATE, this); sm.remove(streamId, this); - this.inboundSubscriber.onError(e); + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, t); + } + + this.inboundSubscriber.onError(t); return; } @@ -180,6 +218,9 @@ void sendFirstPayload(Payload payload, long initialRequestN) { final ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, streamId); connection.sendFrame(streamId, cancelFrame); + if (requestInterceptor != null) { + requestInterceptor.onCancel(streamId); + } return; } @@ -215,6 +256,11 @@ public final void cancel() { ReassemblyUtils.synchronizedRelease(this, previousState); this.connection.sendFrame(streamId, CancelFrameCodec.encode(this.allocator, streamId)); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onCancel(streamId); + } } else if (!hasRequested(previousState)) { // no need to send anything, since the first request has not happened this.payload.release(); @@ -246,6 +292,11 @@ public final void handleComplete() { this.requesterResponderSupport.remove(this.streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, null); + } + this.inboundSubscriber.onComplete(); } @@ -268,6 +319,11 @@ public final void handleError(Throwable cause) { ReassemblyUtils.synchronizedRelease(this, previousState); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, cause); + } + this.inboundSubscriber.onError(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 620638d9c..6b06bc119 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamResponderSubscriber.java @@ -32,6 +32,7 @@ import io.rsocket.frame.FrameType; import io.rsocket.frame.PayloadFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.plugins.RequestInterceptor; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.reactivestreams.Subscription; import org.slf4j.Logger; @@ -39,6 +40,7 @@ import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Operators; +import reactor.util.annotation.Nullable; import reactor.util.context.Context; final class RequestStreamResponderSubscriber @@ -56,6 +58,8 @@ final class RequestStreamResponderSubscriber final RequesterResponderSupport requesterResponderSupport; final DuplexConnection connection; + @Nullable final RequestInterceptor requestInterceptor; + final RSocket handler; volatile Subscription s; @@ -81,6 +85,7 @@ public RequestStreamResponderSubscriber( this.requesterResponderSupport = requesterResponderSupport; this.connection = requesterResponderSupport.getDuplexConnection(); this.payloadDecoder = requesterResponderSupport.getPayloadDecoder(); + this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); this.handler = handler; this.frames = ReassemblyUtils.addFollowingFrame( @@ -97,6 +102,7 @@ public RequestStreamResponderSubscriber( this.maxInboundPayloadSize = requesterResponderSupport.getMaxInboundPayloadSize(); this.requesterResponderSupport = requesterResponderSupport; this.connection = requesterResponderSupport.getDuplexConnection(); + this.requestInterceptor = requesterResponderSupport.getRequestInterceptor(); this.payloadDecoder = null; this.handler = null; @@ -123,47 +129,77 @@ public void onNext(Payload p) { final DuplexConnection sender = this.connection; final ByteBufAllocator allocator = this.allocator; - if (p == null) { - final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(allocator, streamId); - sender.sendFrame(streamId, completeFrame); - return; - } - final int mtu = this.mtu; try { if (!isValid(mtu, this.maxFrameLength, p, false)) { p.release(); - this.handleCancel(); + if (!this.tryTerminateOnError()) { + return; + } - this.done = true; - final ByteBuf errorFrame = - ErrorFrameCodec.encode( - allocator, - streamId, - new CanceledException( - String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength))); + final CanceledException e = + new CanceledException( + String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)); + final ByteBuf errorFrame = ErrorFrameCodec.encode(allocator, streamId, e); sender.sendFrame(streamId, errorFrame); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, e); + } return; } } catch (IllegalReferenceCountException e) { - this.handleCancel(); - this.done = true; + if (!this.tryTerminateOnError()) { + return; + } + final ByteBuf errorFrame = ErrorFrameCodec.encode( allocator, streamId, new CanceledException("Failed to validate payload. Cause" + e.getMessage())); sender.sendFrame(streamId, errorFrame); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, e); + } return; } try { sendReleasingPayload(streamId, FrameType.NEXT, mtu, p, sender, allocator, false); } catch (Throwable t) { - this.handleCancel(); - this.done = true; + if (!this.tryTerminateOnError()) { + return; + } + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, t); + } + } + } + + boolean tryTerminateOnError() { + final Subscription currentSubscription = this.s; + if (currentSubscription == Operators.cancelledSubscription()) { + return false; + } + + this.done = true; + + if (!S.compareAndSet(this, currentSubscription, Operators.cancelledSubscription())) { + return false; } + + this.requesterResponderSupport.remove(this.streamId, this); + + currentSubscription.cancel(); + + return true; } @Override @@ -186,11 +222,15 @@ 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); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, t); + } } @Override @@ -206,11 +246,15 @@ 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); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, null); + } } @Override @@ -230,7 +274,8 @@ public final void handleCancel() { // and fragmentation of the first frame was cancelled before S.lazySet(this, Operators.cancelledSubscription()); - this.requesterResponderSupport.remove(this.streamId, this); + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); final CompositeByteBuf frames = this.frames; if (frames != null) { @@ -238,6 +283,10 @@ public final void handleCancel() { frames.release(); } + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onCancel(streamId); + } return; } @@ -245,9 +294,15 @@ public final void handleCancel() { return; } - this.requesterResponderSupport.remove(this.streamId, this); + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); currentSubscription.cancel(); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onCancel(streamId); + } } @Override @@ -260,25 +315,31 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas try { ReassemblyUtils.addFollowingFrame( frames, followingFrame, hasFollows, this.maxInboundPayloadSize); - } catch (IllegalStateException t) { + } catch (IllegalStateException e) { // if subscription is null, it means that streams has not yet reassembled all the fragments // and fragmentation of the first frame was cancelled before S.lazySet(this, Operators.cancelledSubscription()); - this.requesterResponderSupport.remove(this.streamId, this); + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); this.frames = null; frames.release(); - logger.debug("Reassembly has failed", t); - // sends error frame from the responder side to tell that something went wrong final ByteBuf errorFrame = ErrorFrameCodec.encode( this.allocator, - this.streamId, - new CanceledException("Failed to reassemble payload. Cause: " + t.getMessage())); + streamId, + new CanceledException("Failed to reassemble payload. Cause: " + e.getMessage())); this.connection.sendFrame(streamId, errorFrame); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, e); + } + + logger.debug("Reassembly has failed", e); return; } @@ -292,19 +353,25 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas S.lazySet(this, Operators.cancelledSubscription()); this.done = true; - this.requesterResponderSupport.remove(this.streamId, this); + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); ReferenceCountUtil.safeRelease(frames); - logger.debug("Reassembly has failed", t); - // sends error frame from the responder side to tell that something went wrong final ByteBuf errorFrame = ErrorFrameCodec.encode( this.allocator, - this.streamId, + streamId, new CanceledException("Failed to reassemble payload. Cause: " + t.getMessage())); this.connection.sendFrame(streamId, errorFrame); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, t); + } + + logger.debug("Reassembly has failed", t); return; } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java b/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java index e3f70cede..2272ceb5f 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequesterResponderSupport.java @@ -4,7 +4,10 @@ import io.netty.util.collection.IntObjectHashMap; import io.netty.util.collection.IntObjectMap; import io.rsocket.DuplexConnection; +import io.rsocket.RSocket; import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.plugins.RequestInterceptor; +import java.util.function.Function; import reactor.util.annotation.Nullable; class RequesterResponderSupport { @@ -15,6 +18,7 @@ class RequesterResponderSupport { private final PayloadDecoder payloadDecoder; private final ByteBufAllocator allocator; private final DuplexConnection connection; + @Nullable private final RequestInterceptor requestInterceptor; @Nullable final StreamIdSupplier streamIdSupplier; final IntObjectMap activeStreams; @@ -25,7 +29,8 @@ public RequesterResponderSupport( int maxInboundPayloadSize, PayloadDecoder payloadDecoder, DuplexConnection connection, - @Nullable StreamIdSupplier streamIdSupplier) { + @Nullable StreamIdSupplier streamIdSupplier, + Function requestInterceptorFunction) { this.activeStreams = new IntObjectHashMap<>(); this.mtu = mtu; @@ -35,6 +40,7 @@ public RequesterResponderSupport( this.allocator = connection.alloc(); this.streamIdSupplier = streamIdSupplier; this.connection = connection; + this.requestInterceptor = requestInterceptorFunction.apply((RSocket) this); } public int getMtu() { @@ -61,6 +67,11 @@ public DuplexConnection getDuplexConnection() { return connection; } + @Nullable + public RequestInterceptor getRequestInterceptor() { + return requestInterceptor; + } + /** * Issues next {@code streamId} * diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/CompositeRequestInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/CompositeRequestInterceptor.java new file mode 100644 index 000000000..b4e1a1ba3 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/plugins/CompositeRequestInterceptor.java @@ -0,0 +1,151 @@ +package io.rsocket.plugins; + +import io.netty.buffer.ByteBuf; +import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; +import java.util.List; +import java.util.function.Function; +import reactor.core.publisher.Operators; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +class CompositeRequestInterceptor implements RequestInterceptor { + + final RequestInterceptor[] requestInterceptors; + + public CompositeRequestInterceptor(RequestInterceptor[] requestInterceptors) { + this.requestInterceptors = requestInterceptors; + } + + @Override + public void dispose() { + final RequestInterceptor[] requestInterceptors = this.requestInterceptors; + for (int i = 0; i < requestInterceptors.length; i++) { + final RequestInterceptor requestInterceptor = requestInterceptors[i]; + requestInterceptor.dispose(); + } + } + + @Override + public void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metadata) { + final RequestInterceptor[] requestInterceptors = this.requestInterceptors; + for (int i = 0; i < requestInterceptors.length; i++) { + final RequestInterceptor requestInterceptor = requestInterceptors[i]; + try { + requestInterceptor.onStart(streamId, requestType, metadata); + } catch (Throwable t) { + Operators.onErrorDropped(t, Context.empty()); + } + } + } + + @Override + public void onTerminate(int streamId, @Nullable Throwable cause) { + final RequestInterceptor[] requestInterceptors = this.requestInterceptors; + for (int i = 0; i < requestInterceptors.length; i++) { + final RequestInterceptor requestInterceptor = requestInterceptors[i]; + try { + requestInterceptor.onTerminate(streamId, cause); + } catch (Throwable t) { + Operators.onErrorDropped(t, Context.empty()); + } + } + } + + @Override + public void onCancel(int streamId) { + final RequestInterceptor[] requestInterceptors = this.requestInterceptors; + for (int i = 0; i < requestInterceptors.length; i++) { + final RequestInterceptor requestInterceptor = requestInterceptors[i]; + try { + requestInterceptor.onCancel(streamId); + } catch (Throwable t) { + Operators.onErrorDropped(t, Context.empty()); + } + } + } + + @Override + public void onReject( + Throwable rejectionReason, FrameType requestType, @Nullable ByteBuf metadata) { + final RequestInterceptor[] requestInterceptors = this.requestInterceptors; + for (int i = 0; i < requestInterceptors.length; i++) { + final RequestInterceptor requestInterceptor = requestInterceptors[i]; + try { + requestInterceptor.onReject(rejectionReason, requestType, metadata); + } catch (Throwable t) { + Operators.onErrorDropped(t, Context.empty()); + } + } + } + + @Nullable + static RequestInterceptor create( + RSocket rSocket, List> interceptors) { + switch (interceptors.size()) { + case 0: + return null; + case 1: + return new SafeRequestInterceptor(interceptors.get(0).apply(rSocket)); + default: + return new CompositeRequestInterceptor( + interceptors.stream().map(f -> f.apply(rSocket)).toArray(RequestInterceptor[]::new)); + } + } + + static class SafeRequestInterceptor implements RequestInterceptor { + + final RequestInterceptor requestInterceptor; + + public SafeRequestInterceptor(RequestInterceptor requestInterceptor) { + this.requestInterceptor = requestInterceptor; + } + + @Override + public void dispose() { + requestInterceptor.dispose(); + } + + @Override + public boolean isDisposed() { + return requestInterceptor.isDisposed(); + } + + @Override + public void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metadata) { + try { + requestInterceptor.onStart(streamId, requestType, metadata); + } catch (Throwable t) { + Operators.onErrorDropped(t, Context.empty()); + } + } + + @Override + public void onTerminate(int streamId, @Nullable Throwable cause) { + try { + requestInterceptor.onTerminate(streamId, cause); + } catch (Throwable t) { + Operators.onErrorDropped(t, Context.empty()); + } + } + + @Override + public void onCancel(int streamId) { + try { + requestInterceptor.onCancel(streamId); + } catch (Throwable t) { + Operators.onErrorDropped(t, Context.empty()); + } + } + + @Override + public void onReject( + Throwable rejectionReason, FrameType requestType, @Nullable ByteBuf metadata) { + try { + requestInterceptor.onReject(rejectionReason, requestType, metadata); + } catch (Throwable t) { + Operators.onErrorDropped(t, Context.empty()); + } + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java b/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java index fc032847c..be0d8278f 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java @@ -18,6 +18,7 @@ import io.rsocket.DuplexConnection; import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; +import reactor.util.annotation.Nullable; /** * Extends {@link InterceptorRegistry} with methods for building a chain of registered interceptors. @@ -25,6 +26,16 @@ */ public class InitializingInterceptorRegistry extends InterceptorRegistry { + @Nullable + public RequestInterceptor initRequesterRequestInterceptor(RSocket rSocketRequester) { + return CompositeRequestInterceptor.create(rSocketRequester, getRequesterRequestInterceptors()); + } + + @Nullable + public RequestInterceptor initResponderRequestInterceptor(RSocket rSocketResponder) { + return CompositeRequestInterceptor.create(rSocketResponder, getResponderRequestInterceptors()); + } + public DuplexConnection initConnection( DuplexConnectionInterceptor.Type type, DuplexConnection connection) { for (DuplexConnectionInterceptor interceptor : getConnectionInterceptors()) { @@ -34,7 +45,7 @@ public DuplexConnection initConnection( } public RSocket initRequester(RSocket rsocket) { - for (RSocketInterceptor interceptor : getRequesterInteceptors()) { + for (RSocketInterceptor interceptor : getRequesterInterceptors()) { rsocket = interceptor.apply(rsocket); } return rsocket; diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java b/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java index 427fa15ae..0ccc4cb92 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java @@ -15,9 +15,11 @@ */ package io.rsocket.plugins; +import io.rsocket.RSocket; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import java.util.function.Function; /** * Provides support for registering interceptors at the following levels: @@ -30,16 +32,46 @@ * */ public class InterceptorRegistry { - private List requesterInteceptors = new ArrayList<>(); - private List responderInterceptors = new ArrayList<>(); + private List> requesterRequestInterceptors = + new ArrayList<>(); + private List> responderRequestInterceptors = + new ArrayList<>(); + private List requesterRSocketInterceptors = new ArrayList<>(); + private List responderRSocketInterceptors = new ArrayList<>(); private List socketAcceptorInterceptors = new ArrayList<>(); private List connectionInterceptors = new ArrayList<>(); + /** + * Add an {@link RequestInterceptor} that will hook into Requester RSocket requests' phases. + * + * @param interceptor a function which accepts an {@link RSocket} and returns a new {@link + * RequestInterceptor} + * @since 1.1 + */ + public InterceptorRegistry forRequester( + Function interceptor) { + requesterRequestInterceptors.add(interceptor); + return this; + } + + /** + * Add an {@link RequestInterceptor} that will hook into Requester RSocket requests' phases. + * + * @param interceptor a function which accepts an {@link RSocket} and returns a new {@link + * RequestInterceptor} + * @since 1.1 + */ + public InterceptorRegistry forResponder( + Function interceptor) { + responderRequestInterceptors.add(interceptor); + return this; + } + /** * Add an {@link RSocketInterceptor} that will decorate the RSocket used for performing requests. */ public InterceptorRegistry forRequester(RSocketInterceptor interceptor) { - requesterInteceptors.add(interceptor); + requesterRSocketInterceptors.add(interceptor); return this; } @@ -48,7 +80,7 @@ public InterceptorRegistry forRequester(RSocketInterceptor interceptor) { * registrations. */ public InterceptorRegistry forRequester(Consumer> consumer) { - consumer.accept(requesterInteceptors); + consumer.accept(requesterRSocketInterceptors); return this; } @@ -57,7 +89,7 @@ public InterceptorRegistry forRequester(Consumer> consu * requests. */ public InterceptorRegistry forResponder(RSocketInterceptor interceptor) { - responderInterceptors.add(interceptor); + responderRSocketInterceptors.add(interceptor); return this; } @@ -66,7 +98,7 @@ public InterceptorRegistry forResponder(RSocketInterceptor interceptor) { * registrations. */ public InterceptorRegistry forResponder(Consumer> consumer) { - consumer.accept(responderInterceptors); + consumer.accept(responderRSocketInterceptors); return this; } @@ -102,12 +134,20 @@ public InterceptorRegistry forConnection(Consumer getRequesterInteceptors() { - return requesterInteceptors; + List> getRequesterRequestInterceptors() { + return requesterRequestInterceptors; + } + + List> getResponderRequestInterceptors() { + return responderRequestInterceptors; + } + + List getRequesterInterceptors() { + return requesterRSocketInterceptors; } List getResponderInterceptors() { - return responderInterceptors; + return responderRSocketInterceptors; } List getConnectionInterceptors() { diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/RequestInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/RequestInterceptor.java new file mode 100644 index 000000000..5da850837 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/plugins/RequestInterceptor.java @@ -0,0 +1,73 @@ +package io.rsocket.plugins; + +import io.netty.buffer.ByteBuf; +import io.rsocket.frame.FrameType; +import reactor.core.Disposable; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +/** + * Class used to track the RSocket requests lifecycles. The main difference and advantage of this + * interceptor compares to {@link RSocketInterceptor} is that it allows intercepting the initial and + * terminal phases on every individual request. + * + *

    Note, if any of the invocations will rise a runtime exception, this exception will be + * caught and be propagated to {@link reactor.core.publisher.Operators#onErrorDropped(Throwable, + * Context)} + * + * @since 1.1 + */ +public interface RequestInterceptor extends Disposable { + + /** + * Method which is being invoked on successful acceptance and start of a request. + * + * @param streamId used for the request + * @param requestType of the request. Must be one of the following types {@link + * FrameType#REQUEST_FNF}, {@link FrameType#REQUEST_RESPONSE}, {@link + * FrameType#REQUEST_STREAM} or {@link FrameType#REQUEST_CHANNEL} + * @param metadata taken from the initial frame + */ + void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metadata); + + /** + * Method which is being invoked once a successfully accepted request is terminated. This method + * can be invoked only after the {@link #onStart(int, FrameType, ByteBuf)} method. This method is + * exclusive with {@link #onCancel(int)}. + * + * @param streamId used by this request + * @param t with which this finished has terminated. Must be one of the following signals + */ + void onTerminate(int streamId, @Nullable Throwable t); + + /** + * Method which is being invoked once a successfully accepted request is cancelled. This method + * can be invoked only after the {@link #onStart(int, FrameType, ByteBuf)} method. This method is + * exclusive with {@link #onTerminate(int, Throwable)}. + * + * @param streamId used by this request + */ + void onCancel(int streamId); + + /** + * Method which is being invoked on the request rejection. This method is being called only if the + * actual request can not be started and is called instead of the {@link #onStart(int, FrameType, + * ByteBuf)} method. The reason for rejection can be one of the following: + * + *

    + * + *

      + *
    • No available {@link io.rsocket.lease.Lease} on the requester or the responder sides + *
    • Invalid {@link io.rsocket.Payload} size or format on the Requester side, so the request + * is being rejected before the actual streamId is generated + *
    • A second subscription on the ongoing Request + *
    + * + * @param rejectionReason exception which causes rejection of a particular request + * @param requestType of the request. Must be one of the following types {@link + * FrameType#REQUEST_FNF}, {@link FrameType#REQUEST_RESPONSE}, {@link + * FrameType#REQUEST_STREAM} or {@link FrameType#REQUEST_CHANNEL} + * @param metadata taken from the initial frame + */ + void onReject(Throwable rejectionReason, FrameType requestType, @Nullable ByteBuf metadata); +} 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 d080b166d..b77e51537 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -543,6 +543,7 @@ protected RSocketRequester newRSocket() { Integer.MAX_VALUE, Integer.MAX_VALUE, null, + __ -> null, RequesterLeaseHandler.None); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/FireAndForgetRequesterMonoTest.java b/rsocket-core/src/test/java/io/rsocket/core/FireAndForgetRequesterMonoTest.java index 0857a2de8..f5422a4bf 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/FireAndForgetRequesterMonoTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/FireAndForgetRequesterMonoTest.java @@ -14,6 +14,7 @@ import io.rsocket.Payload; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.FrameType; +import io.rsocket.plugins.TestRequestInterceptor; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.ByteBufPayload; import java.time.Duration; @@ -46,7 +47,9 @@ public static void setUp() { @ParameterizedTest @MethodSource("frameSent") public void frameShouldBeSentOnSubscription(Consumer monoConsumer) { - final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final TestRequesterResponderSupport activeStreams = + TestRequesterResponderSupport.client(testRequestInterceptor); final Payload payload = genericPayload(activeStreams.getAllocator()); final FireAndForgetRequesterMono fireAndForgetRequesterMono = new FireAndForgetRequesterMono(payload, activeStreams); @@ -62,7 +65,6 @@ public void frameShouldBeSentOnSubscription(Consumer // should not add anything to map stateAssert.isTerminated(); activeStreams.assertNoActiveStreams(); - final ByteBuf frame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(frame) .isNotNull() @@ -79,6 +81,10 @@ public void frameShouldBeSentOnSubscription(Consumer Assertions.assertThat(activeStreams.getDuplexConnection().isEmpty()).isTrue(); activeStreams.getAllocator().assertHasNoLeaks(); + testRequestInterceptor + .expectOnStart(1, FrameType.REQUEST_FNF) + .expectOnComplete(1) + .expectNothing(); } /** @@ -189,7 +195,9 @@ static Stream> frameSent() { @MethodSource("shouldErrorOnIncorrectRefCntInGivenPayloadSource") public void shouldErrorOnIncorrectRefCntInGivenPayload( Consumer monoConsumer) { - final TestRequesterResponderSupport streamManager = TestRequesterResponderSupport.client(); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final TestRequesterResponderSupport streamManager = + TestRequesterResponderSupport.client(testRequestInterceptor); final LeaksTrackingByteBufAllocator allocator = streamManager.getAllocator(); final TestDuplexConnection sender = streamManager.getDuplexConnection(); final Payload payload = ByteBufPayload.create(""); @@ -210,6 +218,9 @@ public void shouldErrorOnIncorrectRefCntInGivenPayload( Assertions.assertThat(sender.isEmpty()).isTrue(); allocator.assertHasNoLeaks(); + testRequestInterceptor + .expectOnReject(FrameType.REQUEST_FNF, new IllegalReferenceCountException("refCnt: 0")) + .expectNothing(); } static Stream> @@ -233,7 +244,9 @@ public void shouldErrorOnIncorrectRefCntInGivenPayload( @MethodSource("shouldErrorIfFragmentExitsAllowanceIfFragmentationDisabledSource") public void shouldErrorIfFragmentExitsAllowanceIfFragmentationDisabled( Consumer monoConsumer) { - final TestRequesterResponderSupport streamManager = TestRequesterResponderSupport.client(); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final TestRequesterResponderSupport streamManager = + TestRequesterResponderSupport.client(testRequestInterceptor); final LeaksTrackingByteBufAllocator allocator = streamManager.getAllocator(); final TestDuplexConnection sender = streamManager.getDuplexConnection(); @@ -260,6 +273,12 @@ public void shouldErrorIfFragmentExitsAllowanceIfFragmentationDisabled( streamManager.assertNoActiveStreams(); Assertions.assertThat(sender.isEmpty()).isTrue(); allocator.assertHasNoLeaks(); + testRequestInterceptor + .expectOnReject( + FrameType.REQUEST_FNF, + new IllegalArgumentException( + String.format(INVALID_PAYLOAD_ERROR_MESSAGE, FRAME_LENGTH_MASK))) + .expectNothing(); } static Stream> @@ -289,8 +308,10 @@ public void shouldErrorIfFragmentExitsAllowanceIfFragmentationDisabled( @ParameterizedTest @MethodSource("shouldErrorIfNoAvailabilitySource") public void shouldErrorIfNoAvailability(Consumer monoConsumer) { + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final RuntimeException exception = new RuntimeException("test"); final TestRequesterResponderSupport streamManager = - TestRequesterResponderSupport.client(new RuntimeException("test")); + TestRequesterResponderSupport.client(exception, testRequestInterceptor); final LeaksTrackingByteBufAllocator allocator = streamManager.getAllocator(); final TestDuplexConnection sender = streamManager.getDuplexConnection(); final Payload payload = genericPayload(allocator); @@ -311,6 +332,7 @@ public void shouldErrorIfNoAvailability(Consumer mon streamManager.assertNoActiveStreams(); Assertions.assertThat(sender.isEmpty()).isTrue(); allocator.assertHasNoLeaks(); + testRequestInterceptor.expectOnReject(FrameType.REQUEST_FNF, exception).expectNothing(); } static Stream> shouldErrorIfNoAvailabilitySource() { @@ -333,7 +355,9 @@ static Stream> shouldErrorIfNoAvailabilityS /** Ensures single subscription happens in case of racing */ @Test public void shouldSubscribeExactlyOnce1() { - final TestRequesterResponderSupport streamManager = TestRequesterResponderSupport.client(); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final TestRequesterResponderSupport streamManager = + TestRequesterResponderSupport.client(testRequestInterceptor); final LeaksTrackingByteBufAllocator allocator = streamManager.getAllocator(); final TestDuplexConnection sender = streamManager.getDuplexConnection(); @@ -349,7 +373,7 @@ public void shouldSubscribeExactlyOnce1() { () -> RaceTestUtils.race( () -> { - AtomicReference atomicReference = new AtomicReference(); + AtomicReference atomicReference = new AtomicReference<>(); fireAndForgetRequesterMono.subscribe(null, atomicReference::set); Throwable throwable = atomicReference.get(); if (throwable != null) { @@ -380,6 +404,27 @@ public void shouldSubscribeExactlyOnce1() { stateAssert.isTerminated(); streamManager.assertNoActiveStreams(); + testRequestInterceptor + .assertNext( + event -> + Assertions.assertThat(event.eventType) + .isIn( + TestRequestInterceptor.EventType.ON_START, + TestRequestInterceptor.EventType.ON_REJECT)) + .assertNext( + event -> + Assertions.assertThat(event.eventType) + .isIn( + TestRequestInterceptor.EventType.ON_START, + TestRequestInterceptor.EventType.ON_COMPLETE, + TestRequestInterceptor.EventType.ON_REJECT)) + .assertNext( + event -> + Assertions.assertThat(event.eventType) + .isIn( + TestRequestInterceptor.EventType.ON_COMPLETE, + TestRequestInterceptor.EventType.ON_REJECT)) + .expectNothing(); } Assertions.assertThat(sender.isEmpty()).isTrue(); diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java index ae1282c1e..a36415cb1 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -33,10 +33,15 @@ import io.rsocket.RSocket; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.Exceptions; +import io.rsocket.exceptions.RejectedException; import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; import io.rsocket.frame.LeaseFrameCodec; import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; import io.rsocket.frame.SetupFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.subscriber.AssertSubscriber; @@ -64,9 +69,12 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.EmitterProcessor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; import reactor.test.StepVerifier; class RSocketLeaseTest { @@ -107,6 +115,7 @@ void setUp() { 0, 0, null, + __ -> null, requesterLeaseHandler); mockRSocketHandler = mock(RSocket.class); @@ -144,7 +153,25 @@ void setUp() { Publisher payloadPublisher = a.getArgument(0); return Flux.from(payloadPublisher) .doOnNext(ReferenceCounted::release) - .thenMany(Flux.empty()); + .transform( + Operators.lift( + (__, actual) -> + new BaseSubscriber() { + @Override + protected void hookOnSubscribe(Subscription subscription) { + actual.onSubscribe(this); + } + + @Override + protected void hookOnComplete() { + actual.onComplete(); + } + + @Override + protected void hookOnError(Throwable throwable) { + actual.onError(throwable); + } + })); }); rSocketResponder = @@ -155,7 +182,8 @@ void setUp() { responderLeaseHandler, 0, FRAME_LENGTH_MASK, - Integer.MAX_VALUE); + Integer.MAX_VALUE, + __ -> null); } @Test @@ -355,32 +383,86 @@ void requesterAvailabilityRespectsTransport() { } @ParameterizedTest - @MethodSource("interactions") - void responderMissingLeaseRequestsAreRejected( - BiFunction> interaction) { + @MethodSource("responderInteractions") + void responderMissingLeaseRequestsAreRejected(FrameType frameType) { ByteBuf buffer = byteBufAllocator.buffer(); buffer.writeCharSequence("test", CharsetUtil.UTF_8); Payload payload1 = ByteBufPayload.create(buffer); - StepVerifier.create(interaction.apply(rSocketResponder, payload1)) - .expectError(MissingLeaseException.class) - .verify(Duration.ofSeconds(5)); + switch (frameType) { + case REQUEST_FNF: + final ByteBuf fnfFrame = + RequestFireAndForgetFrameCodec.encodeReleasingPayload(byteBufAllocator, 1, payload1); + rSocketResponder.handleFrame(fnfFrame); + fnfFrame.release(); + break; + case REQUEST_RESPONSE: + final ByteBuf requestResponseFrame = + RequestResponseFrameCodec.encodeReleasingPayload(byteBufAllocator, 1, payload1); + rSocketResponder.handleFrame(requestResponseFrame); + requestResponseFrame.release(); + break; + case REQUEST_STREAM: + final ByteBuf requestStreamFrame = + RequestStreamFrameCodec.encodeReleasingPayload(byteBufAllocator, 1, 1, payload1); + rSocketResponder.handleFrame(requestStreamFrame); + requestStreamFrame.release(); + break; + case REQUEST_CHANNEL: + final ByteBuf requestChannelFrame = + RequestChannelFrameCodec.encodeReleasingPayload(byteBufAllocator, 1, true, 1, payload1); + rSocketResponder.handleFrame(requestChannelFrame); + requestChannelFrame.release(); + break; + } + + if (frameType != REQUEST_FNF) { + Assertions.assertThat(connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> FrameHeaderCodec.frameType(bb) == ERROR) + .matches(bb -> Exceptions.from(1, bb) instanceof RejectedException) + .matches(ReferenceCounted::release); + } + + byteBufAllocator.assertHasNoLeaks(); } @ParameterizedTest - @MethodSource("interactions") - void responderPresentLeaseRequestsAreAccepted( - BiFunction> interaction, FrameType frameType) { + @MethodSource("responderInteractions") + void responderPresentLeaseRequestsAreAccepted(FrameType frameType) { leaseSender.onNext(Lease.create(5_000, 2)); ByteBuf buffer = byteBufAllocator.buffer(); buffer.writeCharSequence("test", CharsetUtil.UTF_8); Payload payload1 = ByteBufPayload.create(buffer); - Flux.from(interaction.apply(rSocketResponder, payload1)) - .as(StepVerifier::create) - .expectComplete() - .verify(Duration.ofSeconds(5)); + switch (frameType) { + case REQUEST_FNF: + final ByteBuf fnfFrame = + RequestFireAndForgetFrameCodec.encodeReleasingPayload(byteBufAllocator, 1, payload1); + rSocketResponder.handleFireAndForget(1, fnfFrame); + fnfFrame.release(); + break; + case REQUEST_RESPONSE: + final ByteBuf requestResponseFrame = + RequestResponseFrameCodec.encodeReleasingPayload(byteBufAllocator, 1, payload1); + rSocketResponder.handleFrame(requestResponseFrame); + requestResponseFrame.release(); + break; + case REQUEST_STREAM: + final ByteBuf requestStreamFrame = + RequestStreamFrameCodec.encodeReleasingPayload(byteBufAllocator, 1, 1, payload1); + rSocketResponder.handleFrame(requestStreamFrame); + requestStreamFrame.release(); + break; + case REQUEST_CHANNEL: + final ByteBuf requestChannelFrame = + RequestChannelFrameCodec.encodeReleasingPayload(byteBufAllocator, 1, true, 1, payload1); + rSocketResponder.handleFrame(requestChannelFrame); + requestChannelFrame.release(); + break; + } switch (frameType) { case REQUEST_FNF: @@ -398,41 +480,113 @@ void responderPresentLeaseRequestsAreAccepted( } Assertions.assertThat(connection.getSent()) - .hasSize(1) .first() .matches(bb -> FrameHeaderCodec.frameType(bb) == LEASE) .matches(ReferenceCounted::release); + if (frameType != REQUEST_FNF) { + Assertions.assertThat(connection.getSent()) + .hasSize(2) + .element(1) + .matches(bb -> FrameHeaderCodec.frameType(bb) == COMPLETE) + .matches(ReferenceCounted::release); + } + byteBufAllocator.assertHasNoLeaks(); } @ParameterizedTest - @MethodSource("interactions") - void responderDepletedAllowedLeaseRequestsAreRejected( - BiFunction> interaction) { + @MethodSource("responderInteractions") + void responderDepletedAllowedLeaseRequestsAreRejected(FrameType frameType) { leaseSender.onNext(Lease.create(5_000, 1)); ByteBuf buffer = byteBufAllocator.buffer(); buffer.writeCharSequence("test", CharsetUtil.UTF_8); Payload payload1 = ByteBufPayload.create(buffer); - Flux responder = Flux.from(interaction.apply(rSocketResponder, payload1)); - responder.subscribe(); + ByteBuf buffer2 = byteBufAllocator.buffer(); + buffer2.writeCharSequence("test2", CharsetUtil.UTF_8); + Payload payload2 = ByteBufPayload.create(buffer2); + + switch (frameType) { + case REQUEST_FNF: + final ByteBuf fnfFrame = + RequestFireAndForgetFrameCodec.encodeReleasingPayload(byteBufAllocator, 1, payload1); + final ByteBuf fnfFrame2 = + RequestFireAndForgetFrameCodec.encodeReleasingPayload(byteBufAllocator, 3, payload2); + rSocketResponder.handleFrame(fnfFrame); + rSocketResponder.handleFrame(fnfFrame2); + fnfFrame.release(); + fnfFrame2.release(); + break; + case REQUEST_RESPONSE: + final ByteBuf requestResponseFrame = + RequestResponseFrameCodec.encodeReleasingPayload(byteBufAllocator, 1, payload1); + final ByteBuf requestResponseFrame2 = + RequestResponseFrameCodec.encodeReleasingPayload(byteBufAllocator, 3, payload2); + rSocketResponder.handleFrame(requestResponseFrame); + rSocketResponder.handleFrame(requestResponseFrame2); + requestResponseFrame.release(); + requestResponseFrame2.release(); + break; + case REQUEST_STREAM: + final ByteBuf requestStreamFrame = + RequestStreamFrameCodec.encodeReleasingPayload(byteBufAllocator, 1, 1, payload1); + final ByteBuf requestStreamFrame2 = + RequestStreamFrameCodec.encodeReleasingPayload(byteBufAllocator, 3, 1, payload2); + rSocketResponder.handleFrame(requestStreamFrame); + rSocketResponder.handleFrame(requestStreamFrame2); + requestStreamFrame.release(); + requestStreamFrame2.release(); + break; + case REQUEST_CHANNEL: + final ByteBuf requestChannelFrame = + RequestChannelFrameCodec.encodeReleasingPayload(byteBufAllocator, 1, true, 1, payload1); + final ByteBuf requestChannelFrame2 = + RequestChannelFrameCodec.encodeReleasingPayload(byteBufAllocator, 3, true, 1, payload2); + rSocketResponder.handleFrame(requestChannelFrame); + rSocketResponder.handleFrame(requestChannelFrame2); + requestChannelFrame.release(); + requestChannelFrame2.release(); + break; + } + + switch (frameType) { + case REQUEST_FNF: + Mockito.verify(mockRSocketHandler).fireAndForget(any()); + break; + case REQUEST_RESPONSE: + Mockito.verify(mockRSocketHandler).requestResponse(any()); + break; + case REQUEST_STREAM: + Mockito.verify(mockRSocketHandler).requestStream(any()); + break; + case REQUEST_CHANNEL: + Mockito.verify(mockRSocketHandler).requestChannel(any()); + break; + } Assertions.assertThat(connection.getSent()) - .hasSize(1) .first() .matches(bb -> FrameHeaderCodec.frameType(bb) == LEASE) .matches(ReferenceCounted::release); - ByteBuf buffer2 = byteBufAllocator.buffer(); - buffer2.writeCharSequence("test", CharsetUtil.UTF_8); - Payload payload2 = ByteBufPayload.create(buffer2); + if (frameType != REQUEST_FNF) { + Assertions.assertThat(connection.getSent()) + .hasSize(3) + .element(1) + .matches(bb -> FrameHeaderCodec.frameType(bb) == COMPLETE) + .matches(ReferenceCounted::release); - Flux.from(interaction.apply(rSocketResponder, payload2)) - .as(StepVerifier::create) - .expectError(MissingLeaseException.class) - .verify(Duration.ofSeconds(5)); + Assertions.assertThat(connection.getSent()) + .hasSize(3) + .element(2) + .matches(bb -> FrameHeaderCodec.frameType(bb) == ERROR) + .matches(bb -> Exceptions.from(1, bb) instanceof RejectedException) + .matches(ReferenceCounted::release); + } + + byteBufAllocator.assertHasNoLeaks(); } @ParameterizedTest @@ -528,4 +682,12 @@ static Stream interactions() { (rSocket, payload) -> rSocket.requestChannel(Mono.just(payload)), FrameType.REQUEST_CHANNEL)); } + + static Stream responderInteractions() { + return Stream.of( + FrameType.REQUEST_FNF, + FrameType.REQUEST_RESPONSE, + FrameType.REQUEST_STREAM, + FrameType.REQUEST_CHANNEL); + } } 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 fda6b61ee..4c7921db1 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -77,6 +77,7 @@ void setUp() { 0, 0, null, + __ -> null, RequesterLeaseHandler.None); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index a0b3ef3f2..9aa8442d9 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -1375,6 +1375,7 @@ protected RSocketRequester newRSocket() { Integer.MAX_VALUE, Integer.MAX_VALUE, null, + (__) -> null, RequesterLeaseHandler.None); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index 0d0b0f093..d796d45e5 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -63,6 +63,8 @@ import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.subscriber.AssertSubscriber; import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.plugins.RequestInterceptor; +import io.rsocket.plugins.TestRequestInterceptor; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestSubscriber; import io.rsocket.util.ByteBufPayload; @@ -269,15 +271,18 @@ protected void hookOnSubscribe(Subscription subscription) { @Test public void checkNoLeaksOnRacingCancelFromRequestChannelAndNextFromUpstream() { ByteBufAllocator allocator = rule.alloc(); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + rule.setRequestInterceptor(testRequestInterceptor); for (int i = 0; i < 10000; i++) { AssertSubscriber assertSubscriber = AssertSubscriber.create(); + final MonoProcessor monoProcessor = MonoProcessor.create(); rule.setAcceptingSocket( new RSocket() { @Override public Flux requestChannel(Publisher payloads) { payloads.subscribe(assertSubscriber); - return Flux.never(); + return monoProcessor.flux(); } }, Integer.MAX_VALUE); @@ -303,19 +308,23 @@ public Flux requestChannel(Publisher payloads) { ByteBuf data3 = allocator.buffer(); data3.writeCharSequence("def3", CharsetUtil.UTF_8); ByteBuf nextFrame3 = - PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata3, data3); + PayloadFrameCodec.encode(allocator, 1, false, true, true, metadata3, data3); RaceTestUtils.race( () -> { rule.connection.addToReceivedBuffer(nextFrame1, nextFrame2, nextFrame3); }, - assertSubscriber::cancel); + () -> { + assertSubscriber.cancel(); + monoProcessor.onComplete(); + }); Assertions.assertThat(assertSubscriber.values()).allMatch(ReferenceCounted::release); Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); + testRequestInterceptor.expectOnStart(1, REQUEST_CHANNEL).expectOnComplete(1).expectNothing(); } } @@ -323,6 +332,8 @@ public Flux requestChannel(Publisher payloads) { public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChannelTest() { Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + rule.setRequestInterceptor(testRequestInterceptor); for (int i = 0; i < 10000; i++) { AssertSubscriber assertSubscriber = AssertSubscriber.create(); @@ -350,11 +361,13 @@ public Flux requestChannel(Publisher payloads) { sink.next(ByteBufPayload.create("d1", "m1")); sink.next(ByteBufPayload.create("d2", "m2")); sink.next(ByteBufPayload.create("d3", "m3")); + sink.complete(); }); Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); + testRequestInterceptor.expectOnStart(1, REQUEST_CHANNEL).expectOnCancel(1).expectNothing(); } } @@ -363,6 +376,8 @@ public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChann Scheduler parallel = Schedulers.parallel(); Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + rule.setRequestInterceptor(testRequestInterceptor); for (int i = 0; i < 10000; i++) { AssertSubscriber assertSubscriber = AssertSubscriber.create(); @@ -395,11 +410,12 @@ public Flux requestChannel(Publisher payloads) { sink.next(ByteBufPayload.create("d1", "m1")); sink.next(ByteBufPayload.create("d2", "m2")); sink.next(ByteBufPayload.create("d3", "m3")); + sink.complete(); }, parallel); Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); - + testRequestInterceptor.expectOnStart(1, REQUEST_CHANNEL).expectOnCancel(1).expectNothing(); rule.assertHasNoLeaks(); } } @@ -410,6 +426,8 @@ public Flux requestChannel(Publisher payloads) { Scheduler parallel = Schedulers.parallel(); Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + rule.setRequestInterceptor(testRequestInterceptor); for (int i = 0; i < 10000; i++) { FluxSink[] sinks = new FluxSink[1]; AssertSubscriber assertSubscriber = AssertSubscriber.create(); @@ -499,6 +517,7 @@ public Flux requestChannel(Publisher payloads) { return msg.refCnt() == 0; }); rule.assertHasNoLeaks(); + testRequestInterceptor.expectOnStart(1, REQUEST_CHANNEL).expectOnError(1).expectNothing(); } } @@ -507,6 +526,8 @@ public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestStrea Scheduler parallel = Schedulers.parallel(); Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + rule.setRequestInterceptor(testRequestInterceptor); for (int i = 0; i < 10000; i++) { FluxSink[] sinks = new FluxSink[1]; @@ -536,6 +557,8 @@ public Flux requestStream(Payload payload) { Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); + + testRequestInterceptor.expectOnStart(1, REQUEST_STREAM).expectOnCancel(1).expectNothing(); } } @@ -544,6 +567,8 @@ public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestRespo Scheduler parallel = Schedulers.parallel(); Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + rule.setRequestInterceptor(testRequestInterceptor); for (int i = 0; i < 10000; i++) { Operators.MonoSubscriber[] sources = new Operators.MonoSubscriber[1]; @@ -576,6 +601,16 @@ public void subscribe(CoreSubscriber actual) { Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); + + testRequestInterceptor + .expectOnStart(1, REQUEST_RESPONSE) + .assertNext( + e -> + Assertions.assertThat(e.eventType) + .isIn( + TestRequestInterceptor.EventType.ON_COMPLETE, + TestRequestInterceptor.EventType.ON_CANCEL)) + .expectNothing(); } } @@ -795,7 +830,8 @@ public Flux requestChannel(Publisher payloads) { + ERROR + "} but was {" + frameType(rule.connection.getSent().iterator().next()) - + "}"); + + "}") + .matches(ByteBuf::release); } private static Stream refCntCases() { @@ -1178,6 +1214,7 @@ public static class ServerSocketRule extends AbstractSocketRule requestInterceptor); } 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 38745327e..785532bcf 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -569,7 +569,8 @@ public Flux requestChannel(Publisher payloads) { ResponderLeaseHandler.None, 0, FRAME_LENGTH_MASK, - Integer.MAX_VALUE); + Integer.MAX_VALUE, + __ -> null); crs = new RSocketRequester( @@ -582,6 +583,7 @@ public Flux requestChannel(Publisher payloads) { 0, 0, null, + __ -> null, RequesterLeaseHandler.None); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java index 4b4311a00..54033e249 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java @@ -72,7 +72,7 @@ public static void setUp() { @ValueSource(strings = {"inbound", "outbound", "inboundCancel"}) public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String completionCase) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); - LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); + final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); final TestDuplexConnection sender = activeStreams.getDuplexConnection(); final Payload firstPayload = TestRequesterResponderSupport.genericPayload(allocator); final TestPublisher publisher = TestPublisher.create(); @@ -373,6 +373,56 @@ public void streamShouldWorkCorrectlyWhenRacingHandleErrorWithSubscription() { } } + @Test + public void streamShouldWorkCorrectlyWhenRacingOutboundErrorWithSubscription() { + RuntimeException exception = new RuntimeException("test"); + + for (int i = 0; i < 10000; i++) { + final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); + final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); + final Payload firstPayload = TestRequesterResponderSupport.randomPayload(allocator); + final TestPublisher publisher = TestPublisher.create(); + + final RequestChannelResponderSubscriber requestChannelResponderSubscriber = + new RequestChannelResponderSubscriber(1, 1, firstPayload, activeStreams); + final StateAssert stateAssert = + StateAssert.assertThat(requestChannelResponderSubscriber); + activeStreams.activeStreams.put(1, requestChannelResponderSubscriber); + + // state machine check + stateAssert.isUnsubscribed(); + activeStreams.assertHasStream(1, requestChannelResponderSubscriber); + + publisher.subscribe(requestChannelResponderSubscriber); + + final AssertSubscriber assertSubscriber = AssertSubscriber.create(1); + + RaceTestUtils.race( + () -> requestChannelResponderSubscriber.subscribe(assertSubscriber), + () -> publisher.error(exception)); + + stateAssert.isTerminated(); + + FrameAssert.assertThat(activeStreams.getDuplexConnection().awaitFrame()) + .typeOf(ERROR) + .hasData("test") + .hasStreamId(1) + .hasNoLeaks(); + + if (!assertSubscriber.values().isEmpty()) { + assertSubscriber.assertValuesWith( + p -> PayloadAssert.assertThat(p).isSameAs(p).hasNoLeaks()); + } + + assertSubscriber + .assertTerminated() + .assertError(CancellationException.class) + .assertErrorMessage("Outbound has terminated with an error"); + + allocator.assertHasNoLeaks(); + } + } + @Test public void streamShouldWorkCorrectlyWhenRacingHandleCancelWithSubscription() { for (int i = 0; i < 10000; i++) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RequesterOperatorsRacingTest.java b/rsocket-core/src/test/java/io/rsocket/core/RequesterOperatorsRacingTest.java index 520dd0196..1396774c4 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequesterOperatorsRacingTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequesterOperatorsRacingTest.java @@ -30,6 +30,7 @@ import io.rsocket.frame.FrameType; import io.rsocket.frame.RequestStreamFrameCodec; import io.rsocket.internal.subscriber.AssertSubscriber; +import io.rsocket.plugins.TestRequestInterceptor; import io.rsocket.util.ByteBufPayload; import java.time.Duration; import java.util.ArrayList; @@ -39,11 +40,10 @@ import java.util.stream.Stream; import org.assertj.core.api.Assertions; import org.assertj.core.api.Assumptions; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Hooks; @@ -169,8 +169,9 @@ public String toString() { @MethodSource("scenarios") public void shouldSubscribeExactlyOnce(Scenario scenario) { for (int i = 0; i < 10000; i++) { + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); final TestRequesterResponderSupport requesterResponderSupport = - TestRequesterResponderSupport.client(); + TestRequesterResponderSupport.client(testRequestInterceptor); final Supplier payloadSupplier = () -> TestRequesterResponderSupport.genericPayload( @@ -214,6 +215,9 @@ public void shouldSubscribeExactlyOnce(Scenario scenario) { if (requestOperator instanceof FrameHandler) { ((FrameHandler) requestOperator).handleComplete(); + if (scenario.requestType() == REQUEST_CHANNEL) { + ((FrameHandler) requestOperator).handleCancel(); + } } }) .thenCancel() @@ -240,6 +244,29 @@ public void shouldSubscribeExactlyOnce(Scenario scenario) { stepVerifier.verify(Duration.ofSeconds(1)); requesterResponderSupport.getAllocator().assertHasNoLeaks(); + if (scenario.requestType() != METADATA_PUSH) { + testRequestInterceptor + .assertNext( + event -> + Assertions.assertThat(event.eventType) + .isIn( + TestRequestInterceptor.EventType.ON_START, + TestRequestInterceptor.EventType.ON_REJECT)) + .assertNext( + event -> + Assertions.assertThat(event.eventType) + .isIn( + TestRequestInterceptor.EventType.ON_START, + TestRequestInterceptor.EventType.ON_COMPLETE, + TestRequestInterceptor.EventType.ON_REJECT)) + .assertNext( + event -> + Assertions.assertThat(event.eventType) + .isIn( + TestRequestInterceptor.EventType.ON_COMPLETE, + TestRequestInterceptor.EventType.ON_REJECT)) + .expectNothing(); + } } } @@ -251,7 +278,9 @@ public void shouldSentRequestFrameOnceInCaseOfRequestRacing(Scenario scenario) { .isIn(REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL); for (int i = 0; i < 10000; i++) { - final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final TestRequesterResponderSupport activeStreams = + TestRequesterResponderSupport.client(testRequestInterceptor); final Supplier payloadSupplier = () -> TestRequesterResponderSupport.genericPayload(activeStreams.getAllocator()); @@ -316,6 +345,12 @@ public void shouldSentRequestFrameOnceInCaseOfRequestRacing(Scenario scenario) { activeStreams.assertNoActiveStreams(); Assertions.assertThat(activeStreams.getDuplexConnection().isEmpty()).isTrue(); activeStreams.getAllocator().assertHasNoLeaks(); + if (scenario.requestType() != METADATA_PUSH) { + testRequestInterceptor + .expectOnStart(1, scenario.requestType()) + .expectOnComplete(1) + .expectNothing(); + } } } @@ -330,7 +365,9 @@ public void shouldHaveNoLeaksOnReassemblyAndCancelRacing(Scenario scenario) { .isIn(REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL); for (int i = 0; i < 10000; i++) { - final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final TestRequesterResponderSupport activeStreams = + TestRequesterResponderSupport.client(testRequestInterceptor); final Supplier payloadSupplier = () -> TestRequesterResponderSupport.genericPayload(activeStreams.getAllocator()); @@ -404,6 +441,16 @@ public void shouldHaveNoLeaksOnReassemblyAndCancelRacing(Scenario scenario) { .hasClientSideStreamId() .hasStreamId(1) .hasNoLeaks(); + + testRequestInterceptor + .expectOnStart(1, scenario.requestType()) + .expectOnCancel(1) + .expectNothing(); + } else { + testRequestInterceptor + .expectOnStart(1, scenario.requestType()) + .expectOnComplete(1) + .expectNothing(); } Assertions.assertThat(responsePayload.release()).isTrue(); @@ -419,22 +466,24 @@ public void shouldHaveNoLeaksOnReassemblyAndCancelRacing(Scenario scenario) { * Ensures that in case of racing between next element and cancel we will not have any memory * leaks */ - @Test - public void shouldHaveNoLeaksOnNextAndCancelRacing() { + @ParameterizedTest(name = "Should have no leaks when {0} is canceled during reassembly") + @MethodSource("scenarios") + public void shouldHaveNoLeaksOnNextAndCancelRacing(Scenario scenario) { + Assumptions.assumeThat(scenario.requestType()) + .isIn(REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL); + for (int i = 0; i < 10000; i++) { - final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); - final Payload payload = - TestRequesterResponderSupport.genericPayload(activeStreams.getAllocator()); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final TestRequesterResponderSupport activeStreams = + TestRequesterResponderSupport.client(testRequestInterceptor); + final Supplier payloadSupplier = + () -> TestRequesterResponderSupport.genericPayload(activeStreams.getAllocator()); - final RequestResponseRequesterMono requestResponseRequesterMono = - new RequestResponseRequesterMono(payload, activeStreams); + final Publisher requestOperator = scenario.requestOperator(payloadSupplier, activeStreams); Payload response = ByteBufPayload.create("test", "test"); - - StepVerifier.create(requestResponseRequesterMono.doOnNext(Payload::release)) - .expectSubscription() - .expectComplete() - .verifyLater(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + requestOperator.subscribe((AssertSubscriber) assertSubscriber); final ByteBuf sentFrame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(sentFrame) @@ -446,16 +495,16 @@ public void shouldHaveNoLeaksOnNextAndCancelRacing() { .hasMetadata(TestRequesterResponderSupport.METADATA_CONTENT) .hasData(TestRequesterResponderSupport.DATA_CONTENT) .hasNoFragmentsFollow() - .typeOf(FrameType.REQUEST_RESPONSE) + .typeOf(scenario.requestType()) .hasClientSideStreamId() .hasStreamId(1) .hasNoLeaks(); RaceTestUtils.race( - requestResponseRequesterMono::cancel, - () -> requestResponseRequesterMono.handlePayload(response)); + ((Subscription) requestOperator)::cancel, + () -> ((RequesterFrameHandler) requestOperator).handlePayload(response)); - Assertions.assertThat(payload.refCnt()).isZero(); + assertSubscriber.values().forEach(Payload::release); Assertions.assertThat(response.refCnt()).isZero(); activeStreams.assertNoActiveStreams(); @@ -468,10 +517,19 @@ public void shouldHaveNoLeaksOnNextAndCancelRacing() { .hasClientSideStreamId() .hasStreamId(1) .hasNoLeaks(); + + testRequestInterceptor + .expectOnStart(1, scenario.requestType()) + .expectOnCancel(1) + .expectNothing(); + } else { + assertSubscriber.assertTerminated(); + testRequestInterceptor + .expectOnStart(1, scenario.requestType()) + .expectOnComplete(1) + .expectNothing(); } Assertions.assertThat(activeStreams.getDuplexConnection().isEmpty()).isTrue(); - - StateAssert.assertThat(requestResponseRequesterMono).isTerminated(); activeStreams.getAllocator().assertHasNoLeaks(); } } @@ -482,84 +540,106 @@ public void shouldHaveNoLeaksOnNextAndCancelRacing() { * cancel we will not have any memory leaks */ @ParameterizedTest - @ValueSource(booleans = {false, true}) - public void shouldHaveNoUnexpectedErrorDuringOnErrorAndCancelRacing(boolean withReassembly) { + @MethodSource("scenarios") + public void shouldHaveNoUnexpectedErrorDuringOnErrorAndCancelRacing(Scenario scenario) { + Assumptions.assumeThat(scenario.requestType()) + .isIn(REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL); + boolean[] withReassemblyOptions = new boolean[] {true, false}; final ArrayList droppedErrors = new ArrayList<>(); Hooks.onErrorDropped(droppedErrors::add); - try { - for (int i = 0; i < 10000; i++) { - final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); - final Payload payload = - TestRequesterResponderSupport.genericPayload(activeStreams.getAllocator()); - - final RequestResponseRequesterMono requestResponseRequesterMono = - new RequestResponseRequesterMono(payload, activeStreams); - - final StateAssert stateAssert = - StateAssert.assertThat(requestResponseRequesterMono); - - stateAssert.isUnsubscribed(); - final AssertSubscriber assertSubscriber = - requestResponseRequesterMono.subscribeWith(AssertSubscriber.create(0)); - stateAssert.hasSubscribedFlagOnly(); - - assertSubscriber.request(1); - - stateAssert.hasSubscribedFlag().hasRequestN(1).hasFirstFrameSentFlag(); + try { + for (boolean withReassembly : withReassemblyOptions) { + for (int i = 0; i < 10000; i++) { + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final TestRequesterResponderSupport activeStreams = + TestRequesterResponderSupport.client(testRequestInterceptor); + final Supplier payloadSupplier = + () -> TestRequesterResponderSupport.genericPayload(activeStreams.getAllocator()); + + final Publisher requestOperator = + scenario.requestOperator(payloadSupplier, activeStreams); + + final StateAssert stateAssert; + if (requestOperator instanceof RequestResponseRequesterMono) { + stateAssert = StateAssert.assertThat((RequestResponseRequesterMono) requestOperator); + } else if (requestOperator instanceof RequestStreamRequesterFlux) { + stateAssert = StateAssert.assertThat((RequestStreamRequesterFlux) requestOperator); + } else { + stateAssert = StateAssert.assertThat((RequestChannelRequesterFlux) requestOperator); + } - final ByteBuf sentFrame = activeStreams.getDuplexConnection().awaitFrame(); - FrameAssert.assertThat(sentFrame) - .isNotNull() - .hasPayloadSize( - TestRequesterResponderSupport.DATA_CONTENT.getBytes(CharsetUtil.UTF_8).length - + TestRequesterResponderSupport.METADATA_CONTENT.getBytes(CharsetUtil.UTF_8) - .length) - .hasMetadata(TestRequesterResponderSupport.METADATA_CONTENT) - .hasData(TestRequesterResponderSupport.DATA_CONTENT) - .hasNoFragmentsFollow() - .typeOf(FrameType.REQUEST_RESPONSE) - .hasClientSideStreamId() - .hasStreamId(1) - .hasNoLeaks(); + stateAssert.isUnsubscribed(); + final AssertSubscriber assertSubscriber = AssertSubscriber.create(0); - if (withReassembly) { - final ByteBuf fragmentBuf = - activeStreams.getAllocator().buffer().writeBytes(new byte[] {1, 2, 3}); - requestResponseRequesterMono.handleNext(fragmentBuf, true, false); - // mimic frameHandler behaviour - fragmentBuf.release(); - } + requestOperator.subscribe((AssertSubscriber) assertSubscriber); - final RuntimeException testException = new RuntimeException("test"); - RaceTestUtils.race( - requestResponseRequesterMono::cancel, - () -> requestResponseRequesterMono.handleError(testException)); + stateAssert.hasSubscribedFlagOnly(); - Assertions.assertThat(payload.refCnt()).isZero(); + assertSubscriber.request(1); - activeStreams.assertNoActiveStreams(); - stateAssert.isTerminated(); + stateAssert.hasSubscribedFlag().hasRequestN(1).hasFirstFrameSentFlag(); - final boolean isEmpty = activeStreams.getDuplexConnection().isEmpty(); - if (!isEmpty) { - final ByteBuf cancellationFrame = activeStreams.getDuplexConnection().awaitFrame(); - FrameAssert.assertThat(cancellationFrame) + final ByteBuf sentFrame = activeStreams.getDuplexConnection().awaitFrame(); + FrameAssert.assertThat(sentFrame) .isNotNull() - .typeOf(FrameType.CANCEL) + .hasPayloadSize( + TestRequesterResponderSupport.DATA_CONTENT.getBytes(CharsetUtil.UTF_8).length + + TestRequesterResponderSupport.METADATA_CONTENT.getBytes(CharsetUtil.UTF_8) + .length) + .hasMetadata(TestRequesterResponderSupport.METADATA_CONTENT) + .hasData(TestRequesterResponderSupport.DATA_CONTENT) + .hasNoFragmentsFollow() + .typeOf(scenario.requestType()) .hasClientSideStreamId() .hasStreamId(1) .hasNoLeaks(); - Assertions.assertThat(droppedErrors).containsExactly(testException); - } else { - assertSubscriber.assertTerminated().assertErrorMessage("test"); - } - Assertions.assertThat(activeStreams.getDuplexConnection().isEmpty()).isTrue(); + if (withReassembly) { + final ByteBuf fragmentBuf = + activeStreams.getAllocator().buffer().writeBytes(new byte[] {1, 2, 3}); + ((RequesterFrameHandler) requestOperator).handleNext(fragmentBuf, true, false); + // mimic frameHandler behaviour + fragmentBuf.release(); + } - stateAssert.isTerminated(); - droppedErrors.clear(); - activeStreams.getAllocator().assertHasNoLeaks(); + final RuntimeException testException = new RuntimeException("test"); + RaceTestUtils.race( + ((Subscription) requestOperator)::cancel, + () -> ((RequesterFrameHandler) requestOperator).handleError(testException)); + + activeStreams.assertNoActiveStreams(); + stateAssert.isTerminated(); + + final boolean isEmpty = activeStreams.getDuplexConnection().isEmpty(); + if (!isEmpty) { + final ByteBuf cancellationFrame = activeStreams.getDuplexConnection().awaitFrame(); + FrameAssert.assertThat(cancellationFrame) + .isNotNull() + .typeOf(FrameType.CANCEL) + .hasClientSideStreamId() + .hasStreamId(1) + .hasNoLeaks(); + + Assertions.assertThat(droppedErrors).containsExactly(testException); + testRequestInterceptor + .expectOnStart(1, scenario.requestType()) + .expectOnCancel(1) + .expectNothing(); + } else { + testRequestInterceptor + .expectOnStart(1, scenario.requestType()) + .expectOnError(1) + .expectNothing(); + + assertSubscriber.assertTerminated().assertErrorMessage("test"); + } + Assertions.assertThat(activeStreams.getDuplexConnection().isEmpty()).isTrue(); + + stateAssert.isTerminated(); + droppedErrors.clear(); + activeStreams.getAllocator().assertHasNoLeaks(); + } } } finally { Hooks.resetOnErrorDropped(); @@ -583,20 +663,25 @@ public void shouldHaveNoUnexpectedErrorDuringOnErrorAndCancelRacing(boolean with * *

    Ensures full serialization of outgoing signal (frames) */ - @Test - public void shouldBeConsistentInCaseOfRacingOfCancellationAndRequest() { + @ParameterizedTest + @MethodSource("scenarios") + public void shouldBeConsistentInCaseOfRacingOfCancellationAndRequest(Scenario scenario) { + Assumptions.assumeThat(scenario.requestType()) + .isIn(REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL); for (int i = 0; i < 10000; i++) { - final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); - final Payload payload = - TestRequesterResponderSupport.genericPayload(activeStreams.getAllocator()); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final TestRequesterResponderSupport activeStreams = + TestRequesterResponderSupport.client(testRequestInterceptor); + final Supplier payloadSupplier = + () -> TestRequesterResponderSupport.genericPayload(activeStreams.getAllocator()); - final RequestResponseRequesterMono requestResponseRequesterMono = - new RequestResponseRequesterMono(payload, activeStreams); + final Publisher requestOperator = scenario.requestOperator(payloadSupplier, activeStreams); Payload response = ByteBufPayload.create("test", "test"); - final AssertSubscriber assertSubscriber = - requestResponseRequesterMono.subscribeWith(new AssertSubscriber<>(0)); + final AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + + requestOperator.subscribe((AssertSubscriber) assertSubscriber); RaceTestUtils.race(() -> assertSubscriber.cancel(), () -> assertSubscriber.request(1)); @@ -604,11 +689,7 @@ public void shouldBeConsistentInCaseOfRacingOfCancellationAndRequest() { final ByteBuf sentFrame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(sentFrame) .isNotNull() - .typeOf(FrameType.REQUEST_RESPONSE) - .hasPayloadSize( - TestRequesterResponderSupport.DATA_CONTENT.getBytes(CharsetUtil.UTF_8).length - + TestRequesterResponderSupport.METADATA_CONTENT.getBytes(CharsetUtil.UTF_8) - .length) + .typeOf(scenario.requestType()) .hasMetadata(TestRequesterResponderSupport.METADATA_CONTENT) .hasData(TestRequesterResponderSupport.DATA_CONTENT) .hasNoFragmentsFollow() @@ -623,15 +704,17 @@ public void shouldBeConsistentInCaseOfRacingOfCancellationAndRequest() { .hasClientSideStreamId() .hasStreamId(1) .hasNoLeaks(); - } - Assertions.assertThat(payload.refCnt()).isZero(); + testRequestInterceptor + .expectOnStart(1, scenario.requestType()) + .expectOnCancel(1) + .expectNothing(); + } - StateAssert.assertThat(requestResponseRequesterMono).isTerminated(); + ((RequesterFrameHandler) requestOperator).handlePayload(response); + assertSubscriber.values().forEach(Payload::release); - requestResponseRequesterMono.handlePayload(response); Assertions.assertThat(response.refCnt()).isZero(); - activeStreams.assertNoActiveStreams(); Assertions.assertThat(activeStreams.getDuplexConnection().isEmpty()).isTrue(); activeStreams.getAllocator().assertHasNoLeaks(); @@ -639,20 +722,26 @@ public void shouldBeConsistentInCaseOfRacingOfCancellationAndRequest() { } /** Ensures that CancelFrame is sent exactly once in case of racing between cancel() methods */ - @Test - public void shouldSentCancelFrameExactlyOnce() { + @ParameterizedTest + @MethodSource("scenarios") + public void shouldSentCancelFrameExactlyOnce(Scenario scenario) { + Assumptions.assumeThat(scenario.requestType()) + .isIn(REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL); for (int i = 0; i < 10000; i++) { - final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); - final Payload payload = - TestRequesterResponderSupport.genericPayload(activeStreams.getAllocator()); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final TestRequesterResponderSupport activeStreams = + TestRequesterResponderSupport.client(testRequestInterceptor); + final Supplier payloadSupplier = + () -> TestRequesterResponderSupport.genericPayload(activeStreams.getAllocator()); - final RequestResponseRequesterMono requestResponseRequesterMono = - new RequestResponseRequesterMono(payload, activeStreams); + final Publisher requesterOperator = + scenario.requestOperator(payloadSupplier, activeStreams); Payload response = ByteBufPayload.create("test", "test"); - final AssertSubscriber assertSubscriber = - requestResponseRequesterMono.subscribeWith(new AssertSubscriber<>(0)); + final AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + + requesterOperator.subscribe((AssertSubscriber) assertSubscriber); assertSubscriber.request(1); @@ -660,19 +749,15 @@ public void shouldSentCancelFrameExactlyOnce() { FrameAssert.assertThat(sentFrame) .isNotNull() .hasNoFragmentsFollow() - .typeOf(FrameType.REQUEST_RESPONSE) + .typeOf(scenario.requestType()) .hasClientSideStreamId() - .hasPayloadSize( - TestRequesterResponderSupport.DATA_CONTENT.getBytes(CharsetUtil.UTF_8).length - + TestRequesterResponderSupport.METADATA_CONTENT.getBytes(CharsetUtil.UTF_8) - .length) .hasMetadata(TestRequesterResponderSupport.METADATA_CONTENT) .hasData(TestRequesterResponderSupport.DATA_CONTENT) .hasStreamId(1) .hasNoLeaks(); RaceTestUtils.race( - requestResponseRequesterMono::cancel, requestResponseRequesterMono::cancel); + ((Subscription) requesterOperator)::cancel, ((Subscription) requesterOperator)::cancel); final ByteBuf cancelFrame = activeStreams.getDuplexConnection().awaitFrame(); FrameAssert.assertThat(cancelFrame) @@ -682,15 +767,18 @@ public void shouldSentCancelFrameExactlyOnce() { .hasStreamId(1) .hasNoLeaks(); - Assertions.assertThat(payload.refCnt()).isZero(); - activeStreams.assertNoActiveStreams(); + testRequestInterceptor + .expectOnStart(1, scenario.requestType()) + .expectOnCancel(1) + .expectNothing(); - StateAssert.assertThat(requestResponseRequesterMono).isTerminated(); + activeStreams.assertNoActiveStreams(); - requestResponseRequesterMono.handlePayload(response); + ((RequesterFrameHandler) requesterOperator).handlePayload(response); + assertSubscriber.values().forEach(Payload::release); Assertions.assertThat(response.refCnt()).isZero(); - requestResponseRequesterMono.handleComplete(); + ((RequesterFrameHandler) requesterOperator).handleComplete(); assertSubscriber.assertNotTerminated(); activeStreams.assertNoActiveStreams(); diff --git a/rsocket-core/src/test/java/io/rsocket/core/ResponderOperatorsCommonTest.java b/rsocket-core/src/test/java/io/rsocket/core/ResponderOperatorsCommonTest.java index 270bc4a05..4f7821e4a 100755 --- a/rsocket-core/src/test/java/io/rsocket/core/ResponderOperatorsCommonTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ResponderOperatorsCommonTest.java @@ -20,6 +20,7 @@ import static io.rsocket.frame.FrameType.REQUEST_CHANNEL; import static io.rsocket.frame.FrameType.REQUEST_FNF; import static io.rsocket.frame.FrameType.REQUEST_RESPONSE; +import static io.rsocket.frame.FrameType.REQUEST_STREAM; import io.netty.buffer.ByteBuf; import io.rsocket.FrameAssert; @@ -29,6 +30,8 @@ import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.FrameType; import io.rsocket.internal.subscriber.AssertSubscriber; +import io.rsocket.plugins.RequestInterceptor; +import io.rsocket.plugins.TestRequestInterceptor; import io.rsocket.test.util.TestDuplexConnection; import java.util.ArrayList; import java.util.concurrent.ThreadLocalRandom; @@ -86,6 +89,12 @@ public ResponderFrameHandler responseOperator( new RequestResponseResponderSubscriber( streamId, firstFragment, streamManager, handler); streamManager.activeStreams.put(streamId, subscriber); + + final RequestInterceptor requestInterceptor = streamManager.getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.onStart(streamId, REQUEST_RESPONSE, null); + } + return subscriber; } @@ -99,6 +108,12 @@ public ResponderFrameHandler responseOperator( RequestResponseResponderSubscriber subscriber = new RequestResponseResponderSubscriber(streamId, streamManager); streamManager.activeStreams.put(streamId, subscriber); + + final RequestInterceptor requestInterceptor = streamManager.getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.onStart(streamId, REQUEST_RESPONSE, null); + } + return handler.requestResponse(firstPayload).subscribeWith(subscriber); } @@ -128,6 +143,12 @@ public ResponderFrameHandler responseOperator( RequestStreamResponderSubscriber subscriber = new RequestStreamResponderSubscriber( streamId, initialRequestN, firstFragment, streamManager, handler); + + final RequestInterceptor requestInterceptor = streamManager.getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.onStart(streamId, REQUEST_STREAM, null); + } + streamManager.activeStreams.put(streamId, subscriber); return subscriber; } @@ -142,6 +163,12 @@ public ResponderFrameHandler responseOperator( RequestStreamResponderSubscriber subscriber = new RequestStreamResponderSubscriber(streamId, initialRequestN, streamManager); streamManager.activeStreams.put(streamId, subscriber); + + final RequestInterceptor requestInterceptor = streamManager.getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.onStart(streamId, REQUEST_STREAM, null); + } + return handler.requestStream(firstPayload).subscribeWith(subscriber); } @@ -172,6 +199,12 @@ public ResponderFrameHandler responseOperator( new RequestChannelResponderSubscriber( streamId, initialRequestN, firstFragment, streamManager, handler); streamManager.activeStreams.put(streamId, subscriber); + + final RequestInterceptor requestInterceptor = streamManager.getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.onStart(streamId, REQUEST_CHANNEL, null); + } + return subscriber; } @@ -186,6 +219,12 @@ public ResponderFrameHandler responseOperator( new RequestChannelResponderSubscriber( streamId, initialRequestN, firstPayload, streamManager); streamManager.activeStreams.put(streamId, responderSubscriber); + + final RequestInterceptor requestInterceptor = streamManager.getRequestInterceptor(); + if (requestInterceptor != null) { + requestInterceptor.onStart(streamId, REQUEST_CHANNEL, null); + } + return handler.requestChannel(responderSubscriber).subscribeWith(responderSubscriber); } @@ -242,8 +281,9 @@ public Flux requestChannel(Publisher payloads) { void shouldHandleRequest(Scenario scenario) { Assumptions.assumeThat(scenario.requestType()).isNotIn(REQUEST_FNF, METADATA_PUSH); + TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); TestRequesterResponderSupport testRequesterResponderSupport = - TestRequesterResponderSupport.client(); + TestRequesterResponderSupport.client(testRequestInterceptor); final LeaksTrackingByteBufAllocator allocator = testRequesterResponderSupport.getAllocator(); final TestDuplexConnection sender = testRequesterResponderSupport.getDuplexConnection(); TestPublisher testPublisher = TestPublisher.create(); @@ -286,6 +326,9 @@ void shouldHandleRequest(Scenario scenario) { .hasStreamId(1) .hasRequestN(1) .hasNoLeaks(); + + responderFrameHandler.handleComplete(); + testHandler.consumer.assertComplete(); } } @@ -294,6 +337,10 @@ void shouldHandleRequest(Scenario scenario) { .assertValueCount(1) .assertValuesWith(p -> PayloadAssert.assertThat(p).hasNoLeaks()); + testRequestInterceptor + .expectOnStart(1, scenario.requestType()) + .expectOnComplete(1) + .expectNothing(); allocator.assertHasNoLeaks(); } @@ -302,8 +349,9 @@ void shouldHandleRequest(Scenario scenario) { void shouldHandleFragmentedRequest(Scenario scenario) { Assumptions.assumeThat(scenario.requestType()).isNotIn(REQUEST_FNF, METADATA_PUSH); + TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); TestRequesterResponderSupport testRequesterResponderSupport = - TestRequesterResponderSupport.client(); + TestRequesterResponderSupport.client(testRequestInterceptor); final LeaksTrackingByteBufAllocator allocator = testRequesterResponderSupport.getAllocator(); final TestDuplexConnection sender = testRequesterResponderSupport.getDuplexConnection(); TestPublisher testPublisher = TestPublisher.create(); @@ -370,6 +418,11 @@ void shouldHandleFragmentedRequest(Scenario scenario) { firstPayload.release(); + testRequestInterceptor + .expectOnStart(1, scenario.requestType()) + .expectOnComplete(1) + .expectNothing(); + allocator.assertHasNoLeaks(); } @@ -378,8 +431,9 @@ void shouldHandleFragmentedRequest(Scenario scenario) { void shouldHandleInterruptedFragmentation(Scenario scenario) { Assumptions.assumeThat(scenario.requestType()).isNotIn(REQUEST_FNF, METADATA_PUSH); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); TestRequesterResponderSupport testRequesterResponderSupport = - TestRequesterResponderSupport.client(); + TestRequesterResponderSupport.client(testRequestInterceptor); final LeaksTrackingByteBufAllocator allocator = testRequesterResponderSupport.getAllocator(); TestPublisher testPublisher = TestPublisher.create(); TestHandler testHandler = new TestHandler(testPublisher, new AssertSubscriber<>(0)); @@ -413,6 +467,11 @@ void shouldHandleInterruptedFragmentation(Scenario scenario) { testPublisher.assertWasNotSubscribed(); testRequesterResponderSupport.assertNoActiveStreams(); + testRequestInterceptor + .expectOnStart(1, scenario.requestType()) + .expectOnCancel(1) + .expectNothing(); + allocator.assertHasNoLeaks(); } } 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 b96139fb5..fe3f75e1b 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -61,6 +61,7 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { 0, 0, null, + __ -> null, RequesterLeaseHandler.None); String errorMsg = "error"; @@ -98,6 +99,7 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { 0, 0, null, + __ -> null, RequesterLeaseHandler.None); conn.addToReceivedBuffer( diff --git a/rsocket-core/src/test/java/io/rsocket/core/TestRequesterResponderSupport.java b/rsocket-core/src/test/java/io/rsocket/core/TestRequesterResponderSupport.java index f81e8a610..e282d72d5 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/TestRequesterResponderSupport.java +++ b/rsocket-core/src/test/java/io/rsocket/core/TestRequesterResponderSupport.java @@ -23,9 +23,11 @@ import io.netty.util.CharsetUtil; import io.rsocket.DuplexConnection; import io.rsocket.Payload; +import io.rsocket.RSocket; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.FrameType; import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.plugins.RequestInterceptor; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.ByteBufPayload; import java.util.ArrayList; @@ -34,7 +36,7 @@ import reactor.core.Exceptions; import reactor.util.annotation.Nullable; -final class TestRequesterResponderSupport extends RequesterResponderSupport { +final class TestRequesterResponderSupport extends RequesterResponderSupport implements RSocket { static final String DATA_CONTENT = "testData"; static final String METADATA_CONTENT = "testMetadata"; @@ -47,14 +49,16 @@ final class TestRequesterResponderSupport extends RequesterResponderSupport { DuplexConnection connection, int mtu, int maxFrameLength, - int maxInboundPayloadSize) { + int maxInboundPayloadSize, + @Nullable RequestInterceptor requestInterceptor) { super( mtu, maxFrameLength, maxInboundPayloadSize, PayloadDecoder.ZERO_COPY, connection, - streamIdSupplier); + streamIdSupplier, + (__) -> requestInterceptor); this.error = error; } @@ -170,6 +174,18 @@ public synchronized int addAndGetNextStreamId(FrameHandler frameHandler) { return nextStreamId; } + public static TestRequesterResponderSupport client( + @Nullable Throwable e, @Nullable RequestInterceptor requestInterceptor) { + return client( + new TestDuplexConnection( + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT)), + 0, + FRAME_LENGTH_MASK, + Integer.MAX_VALUE, + requestInterceptor, + e); + } + public static TestRequesterResponderSupport client(@Nullable Throwable e) { return client(0, FRAME_LENGTH_MASK, Integer.MAX_VALUE, e); } @@ -182,14 +198,34 @@ public static TestRequesterResponderSupport client( mtu, maxFrameLength, maxInboundPayloadSize, + null, e); } + public static TestRequesterResponderSupport client( + TestDuplexConnection duplexConnection, + int mtu, + int maxFrameLength, + int maxInboundPayloadSize) { + return client(duplexConnection, mtu, maxFrameLength, maxInboundPayloadSize, null); + } + public static TestRequesterResponderSupport client( TestDuplexConnection duplexConnection, int mtu, int maxFrameLength, int maxInboundPayloadSize, + @Nullable RequestInterceptor requestInterceptor) { + return client( + duplexConnection, mtu, maxFrameLength, maxInboundPayloadSize, requestInterceptor, null); + } + + public static TestRequesterResponderSupport client( + TestDuplexConnection duplexConnection, + int mtu, + int maxFrameLength, + int maxInboundPayloadSize, + @Nullable RequestInterceptor requestInterceptor, @Nullable Throwable e) { return new TestRequesterResponderSupport( e, @@ -197,7 +233,8 @@ public static TestRequesterResponderSupport client( duplexConnection, mtu, maxFrameLength, - maxInboundPayloadSize); + maxInboundPayloadSize, + requestInterceptor); } public static TestRequesterResponderSupport client( @@ -217,6 +254,16 @@ public static TestRequesterResponderSupport client() { return client(0); } + public static TestRequesterResponderSupport client(RequestInterceptor requestInterceptor) { + return client( + new TestDuplexConnection( + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT)), + 0, + FRAME_LENGTH_MASK, + Integer.MAX_VALUE, + requestInterceptor); + } + public TestRequesterResponderSupport assertNoActiveStreams() { Assertions.assertThat(activeStreams).isEmpty(); return this; diff --git a/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java b/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java new file mode 100644 index 000000000..24a035b78 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java @@ -0,0 +1,754 @@ +package io.rsocket.plugins; + +import io.netty.buffer.ByteBuf; +import io.rsocket.Closeable; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.frame.FrameType; +import io.rsocket.transport.local.LocalClientTransport; +import io.rsocket.transport.local.LocalServerTransport; +import io.rsocket.util.DefaultPayload; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.util.annotation.Nullable; + +public class RequestInterceptorTest { + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void interceptorShouldBeInstalledProperlyOnTheClientRequesterSide(boolean errorOutcome) { + final Closeable closeable = + RSocketServer.create( + SocketAcceptor.with( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + return Mono.empty(); + } + + @Override + public Mono requestResponse(Payload payload) { + return errorOutcome + ? Mono.error(new RuntimeException("test")) + : Mono.just(payload); + } + + @Override + public Flux requestStream(Payload payload) { + return errorOutcome + ? Flux.error(new RuntimeException("test")) + : Flux.just(payload); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return errorOutcome + ? Flux.error(new RuntimeException("test")) + : Flux.from(payloads); + } + })) + .bindNow(LocalServerTransport.create("test")); + + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final RSocket rSocket = + RSocketConnector.create() + .interceptors( + ir -> + ir.forRequester( + (Function) + (__) -> testRequestInterceptor)) + .connect(LocalClientTransport.create("test")) + .block(); + + try { + rSocket + .fireAndForget(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .block(); + + rSocket + .requestResponse(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .block(); + + rSocket + .requestStream(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .blockLast(); + + rSocket + .requestChannel(Flux.just(DefaultPayload.create("test"))) + .onErrorResume(__ -> Mono.empty()) + .blockLast(); + + testRequestInterceptor + .expectOnStart(1, FrameType.REQUEST_FNF) + .expectOnComplete(1) + .expectOnStart(3, FrameType.REQUEST_RESPONSE) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 3) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectOnStart(5, FrameType.REQUEST_STREAM) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 5) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectOnStart(7, FrameType.REQUEST_CHANNEL) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 7) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectNothing(); + } finally { + rSocket.dispose(); + closeable.dispose(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void interceptorShouldBeInstalledProperlyOnTheClientResponderSide(boolean errorOutcome) + throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + final Closeable closeable = + RSocketServer.create( + (setup, rSocket) -> + Mono.just(new RSocket() {}) + .doAfterTerminate( + () -> { + new Thread( + () -> { + rSocket + .fireAndForget(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .block(); + + rSocket + .requestResponse(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .block(); + + rSocket + .requestStream(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .blockLast(); + + rSocket + .requestChannel( + Flux.just(DefaultPayload.create("test"))) + .onErrorResume(__ -> Mono.empty()) + .blockLast(); + latch.countDown(); + }) + .start(); + })) + .bindNow(LocalServerTransport.create("test")); + + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final RSocket rSocket = + RSocketConnector.create() + .acceptor( + SocketAcceptor.with( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + return Mono.empty(); + } + + @Override + public Mono requestResponse(Payload payload) { + return errorOutcome + ? Mono.error(new RuntimeException("test")) + : Mono.just(payload); + } + + @Override + public Flux requestStream(Payload payload) { + return errorOutcome + ? Flux.error(new RuntimeException("test")) + : Flux.just(payload); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return errorOutcome + ? Flux.error(new RuntimeException("test")) + : Flux.from(payloads); + } + })) + .interceptors( + ir -> + ir.forResponder( + (Function) + (__) -> testRequestInterceptor)) + .connect(LocalClientTransport.create("test")) + .block(); + + try { + Assertions.assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue(); + + testRequestInterceptor + .expectOnStart(2, FrameType.REQUEST_FNF) + .expectOnComplete(2) + .expectOnStart(4, FrameType.REQUEST_RESPONSE) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 4) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectOnStart(6, FrameType.REQUEST_STREAM) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 6) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectOnStart(8, FrameType.REQUEST_CHANNEL) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 8) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectNothing(); + + } finally { + rSocket.dispose(); + closeable.dispose(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void interceptorShouldBeInstalledProperlyOnTheServerRequesterSide(boolean errorOutcome) { + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final Closeable closeable = + RSocketServer.create( + SocketAcceptor.with( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + return Mono.empty(); + } + + @Override + public Mono requestResponse(Payload payload) { + return errorOutcome + ? Mono.error(new RuntimeException("test")) + : Mono.just(payload); + } + + @Override + public Flux requestStream(Payload payload) { + return errorOutcome + ? Flux.error(new RuntimeException("test")) + : Flux.just(payload); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return errorOutcome + ? Flux.error(new RuntimeException("test")) + : Flux.from(payloads); + } + })) + .interceptors( + ir -> + ir.forResponder( + (Function) + (__) -> testRequestInterceptor)) + .bindNow(LocalServerTransport.create("test")); + final RSocket rSocket = + RSocketConnector.create().connect(LocalClientTransport.create("test")).block(); + + try { + rSocket + .fireAndForget(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .block(); + + rSocket + .requestResponse(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .block(); + + rSocket + .requestStream(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .blockLast(); + + rSocket + .requestChannel(Flux.just(DefaultPayload.create("test"))) + .onErrorResume(__ -> Mono.empty()) + .blockLast(); + + testRequestInterceptor + .expectOnStart(1, FrameType.REQUEST_FNF) + .expectOnComplete(1) + .expectOnStart(3, FrameType.REQUEST_RESPONSE) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 3) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectOnStart(5, FrameType.REQUEST_STREAM) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 5) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectOnStart(7, FrameType.REQUEST_CHANNEL) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 7) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectNothing(); + } finally { + rSocket.dispose(); + closeable.dispose(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void interceptorShouldBeInstalledProperlyOnTheServerResponderSide(boolean errorOutcome) + throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final Closeable closeable = + RSocketServer.create( + (setup, rSocket) -> + Mono.just(new RSocket() {}) + .doAfterTerminate( + () -> { + new Thread( + () -> { + rSocket + .fireAndForget(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .block(); + + rSocket + .requestResponse(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .block(); + + rSocket + .requestStream(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .blockLast(); + + rSocket + .requestChannel( + Flux.just(DefaultPayload.create("test"))) + .onErrorResume(__ -> Mono.empty()) + .blockLast(); + latch.countDown(); + }) + .start(); + })) + .interceptors( + ir -> + ir.forRequester( + (Function) + (__) -> testRequestInterceptor)) + .bindNow(LocalServerTransport.create("test")); + final RSocket rSocket = + RSocketConnector.create() + .acceptor( + SocketAcceptor.with( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + return errorOutcome + ? Mono.error(new RuntimeException("test")) + : Mono.empty(); + } + + @Override + public Mono requestResponse(Payload payload) { + return errorOutcome + ? Mono.error(new RuntimeException("test")) + : Mono.just(payload); + } + + @Override + public Flux requestStream(Payload payload) { + return errorOutcome + ? Flux.error(new RuntimeException("test")) + : Flux.just(payload); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return errorOutcome + ? Flux.error(new RuntimeException("test")) + : Flux.from(payloads); + } + })) + .connect(LocalClientTransport.create("test")) + .block(); + + try { + Assertions.assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue(); + + testRequestInterceptor + .expectOnStart(2, FrameType.REQUEST_FNF) + .expectOnComplete(2) + .expectOnStart(4, FrameType.REQUEST_RESPONSE) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 4) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectOnStart(6, FrameType.REQUEST_STREAM) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 6) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectOnStart(8, FrameType.REQUEST_CHANNEL) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 8) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectNothing(); + + } finally { + rSocket.dispose(); + closeable.dispose(); + } + } + + @Test + void ensuresExceptionInTheInterceptorIsHandledProperly() { + final Closeable closeable = + RSocketServer.create( + SocketAcceptor.with( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + return Mono.empty(); + } + + @Override + public Mono requestResponse(Payload payload) { + return Mono.just(payload); + } + + @Override + public Flux requestStream(Payload payload) { + return Flux.just(payload); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads); + } + })) + .bindNow(LocalServerTransport.create("test")); + + final RequestInterceptor testRequestInterceptor = + new RequestInterceptor() { + @Override + public void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metadata) { + throw new RuntimeException("testOnStart"); + } + + @Override + public void onTerminate(int streamId, @Nullable Throwable terminalSignal) { + throw new RuntimeException("testOnTerminate"); + } + + @Override + public void onCancel(int streamId) { + throw new RuntimeException("testOnCancel"); + } + + @Override + public void onReject( + Throwable rejectionReason, FrameType requestType, @Nullable ByteBuf metadata) { + throw new RuntimeException("testOnReject"); + } + + @Override + public void dispose() {} + }; + final RSocket rSocket = + RSocketConnector.create() + .interceptors( + ir -> + ir.forRequester( + (Function) + (__) -> testRequestInterceptor)) + .connect(LocalClientTransport.create("test")) + .block(); + + try { + StepVerifier.create(rSocket.fireAndForget(DefaultPayload.create("test"))) + .expectSubscription() + .expectComplete() + .verify(); + + StepVerifier.create(rSocket.requestResponse(DefaultPayload.create("test"))) + .expectSubscription() + .expectNextCount(1) + .expectComplete() + .verify(); + + StepVerifier.create(rSocket.requestStream(DefaultPayload.create("test"))) + .expectSubscription() + .expectNextCount(1) + .expectComplete() + .verify(); + + StepVerifier.create(rSocket.requestChannel(Flux.just(DefaultPayload.create("test")))) + .expectSubscription() + .expectNextCount(1) + .expectComplete() + .verify(); + } finally { + rSocket.dispose(); + closeable.dispose(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void shouldSupportMultipleInterceptors(boolean errorOutcome) { + final Closeable closeable = + RSocketServer.create( + SocketAcceptor.with( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + return Mono.empty(); + } + + @Override + public Mono requestResponse(Payload payload) { + return errorOutcome + ? Mono.error(new RuntimeException("test")) + : Mono.just(payload); + } + + @Override + public Flux requestStream(Payload payload) { + return errorOutcome + ? Flux.error(new RuntimeException("test")) + : Flux.just(payload); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return errorOutcome + ? Flux.error(new RuntimeException("test")) + : Flux.from(payloads); + } + })) + .bindNow(LocalServerTransport.create("test")); + + final RequestInterceptor testRequestInterceptor1 = + new RequestInterceptor() { + @Override + public void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metadata) { + throw new RuntimeException("testOnStart"); + } + + @Override + public void onTerminate(int streamId, @Nullable Throwable terminalSignal) { + throw new RuntimeException("testOnTerminate"); + } + + @Override + public void onCancel(int streamId) { + throw new RuntimeException("testOnTerminate"); + } + + @Override + public void onReject( + Throwable rejectionReason, FrameType requestType, @Nullable ByteBuf metadata) { + throw new RuntimeException("testOnReject"); + } + + @Override + public void dispose() {} + }; + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); + final TestRequestInterceptor testRequestInterceptor2 = new TestRequestInterceptor(); + final RSocket rSocket = + RSocketConnector.create() + .interceptors( + ir -> + ir.forRequester( + (Function) + (__) -> testRequestInterceptor) + .forRequester( + (Function) + (__) -> testRequestInterceptor1) + .forRequester( + (Function) + (__) -> testRequestInterceptor2)) + .connect(LocalClientTransport.create("test")) + .block(); + + try { + rSocket + .fireAndForget(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .block(); + + rSocket + .requestResponse(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .block(); + + rSocket + .requestStream(DefaultPayload.create("test")) + .onErrorResume(__ -> Mono.empty()) + .blockLast(); + + rSocket + .requestChannel(Flux.just(DefaultPayload.create("test"))) + .onErrorResume(__ -> Mono.empty()) + .blockLast(); + + testRequestInterceptor + .expectOnStart(1, FrameType.REQUEST_FNF) + .expectOnComplete(1) + .expectOnStart(3, FrameType.REQUEST_RESPONSE) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 3) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectOnStart(5, FrameType.REQUEST_STREAM) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 5) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectOnStart(7, FrameType.REQUEST_CHANNEL) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 7) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectNothing(); + + testRequestInterceptor2 + .expectOnStart(1, FrameType.REQUEST_FNF) + .expectOnComplete(1) + .expectOnStart(3, FrameType.REQUEST_RESPONSE) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 3) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectOnStart(5, FrameType.REQUEST_STREAM) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 5) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectOnStart(7, FrameType.REQUEST_CHANNEL) + .assertNext( + e -> + Assertions.assertThat(e) + .hasFieldOrPropertyWithValue("streamId", 7) + .hasFieldOrPropertyWithValue( + "eventType", + errorOutcome + ? TestRequestInterceptor.EventType.ON_ERROR + : TestRequestInterceptor.EventType.ON_COMPLETE)) + .expectNothing(); + } finally { + rSocket.dispose(); + closeable.dispose(); + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/plugins/TestRequestInterceptor.java b/rsocket-core/src/test/java/io/rsocket/plugins/TestRequestInterceptor.java new file mode 100644 index 000000000..fe9de7ce1 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/plugins/TestRequestInterceptor.java @@ -0,0 +1,141 @@ +package io.rsocket.plugins; + +import io.netty.buffer.ByteBuf; +import io.rsocket.frame.FrameType; +import io.rsocket.internal.jctools.queues.MpscUnboundedArrayQueue; +import java.util.Queue; +import java.util.function.Consumer; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.Condition; +import reactor.util.annotation.Nullable; + +public class TestRequestInterceptor implements RequestInterceptor { + + final Queue events = new MpscUnboundedArrayQueue<>(128); + + @Override + public void dispose() {} + + @Override + public void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metadata) { + events.add(new Event(EventType.ON_START, streamId, requestType, null)); + } + + @Override + public void onTerminate(int streamId, @Nullable Throwable t) { + events.add( + new Event(t == null ? EventType.ON_COMPLETE : EventType.ON_ERROR, streamId, null, t)); + } + + @Override + public void onCancel(int streamId) { + events.add(new Event(EventType.ON_CANCEL, streamId, null, null)); + } + + @Override + public void onReject( + Throwable rejectionReason, FrameType requestType, @Nullable ByteBuf metadata) { + events.add(new Event(EventType.ON_REJECT, -1, requestType, rejectionReason)); + } + + public TestRequestInterceptor expectOnStart(int streamId, FrameType requestType) { + final Event event = events.poll(); + + Assertions.assertThat(event) + .hasFieldOrPropertyWithValue("eventType", EventType.ON_START) + .hasFieldOrPropertyWithValue("streamId", streamId) + .hasFieldOrPropertyWithValue("requestType", requestType); + + return this; + } + + public TestRequestInterceptor expectOnComplete(int streamId) { + final Event event = events.poll(); + + Assertions.assertThat(event) + .hasFieldOrPropertyWithValue("eventType", EventType.ON_COMPLETE) + .hasFieldOrPropertyWithValue("streamId", streamId); + + return this; + } + + public TestRequestInterceptor expectOnError(int streamId) { + final Event event = events.poll(); + + Assertions.assertThat(event) + .hasFieldOrPropertyWithValue("eventType", EventType.ON_ERROR) + .hasFieldOrPropertyWithValue("streamId", streamId); + + return this; + } + + public TestRequestInterceptor expectOnCancel(int streamId) { + final Event event = events.poll(); + + Assertions.assertThat(event) + .hasFieldOrPropertyWithValue("eventType", EventType.ON_CANCEL) + .hasFieldOrPropertyWithValue("streamId", streamId); + + return this; + } + + public TestRequestInterceptor assertNext(Consumer consumer) { + final Event event = events.poll(); + Assertions.assertThat(event).isNotNull(); + + consumer.accept(event); + + return this; + } + + public TestRequestInterceptor expectOnReject(FrameType requestType, Throwable rejectionReason) { + final Event event = events.poll(); + + Assertions.assertThat(event) + .hasFieldOrPropertyWithValue("eventType", EventType.ON_REJECT) + .has( + new Condition<>( + e -> { + Assertions.assertThat(e.error) + .isExactlyInstanceOf(rejectionReason.getClass()) + .hasMessage(rejectionReason.getMessage()) + .hasCause(rejectionReason.getCause()); + return true; + }, + "Has rejection reason which matches to %s", + rejectionReason)) + .hasFieldOrPropertyWithValue("requestType", requestType); + + return this; + } + + public TestRequestInterceptor expectNothing() { + final Event event = events.poll(); + + Assertions.assertThat(event).isNull(); + + return this; + } + + public static final class Event { + public final EventType eventType; + public final int streamId; + public final FrameType requestType; + public final Throwable error; + + Event(EventType eventType, int streamId, FrameType requestType, Throwable error) { + this.eventType = eventType; + this.streamId = streamId; + this.requestType = requestType; + this.error = error; + } + } + + public enum EventType { + ON_START, + ON_COMPLETE, + ON_ERROR, + ON_CANCEL, + ON_REJECT + } +} From e122454a367c5c111906fb8c77ca344a1b6c242b Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 22 Oct 2020 16:41:27 +0300 Subject: [PATCH 042/183] improves RequestInterceptor API to have FrameType in all the calls Signed-off-by: Oleh Dokuka --- .../core/FireAndForgetRequesterMono.java | 10 +++--- .../FireAndForgetResponderSubscriber.java | 11 +++--- .../core/RequestChannelRequesterFlux.java | 20 +++++------ .../RequestChannelResponderSubscriber.java | 36 +++++++++---------- .../core/RequestResponseRequesterMono.java | 12 +++---- .../RequestResponseResponderSubscriber.java | 20 +++++------ .../core/RequestStreamRequesterFlux.java | 16 +++++---- .../RequestStreamResponderSubscriber.java | 18 +++++----- .../plugins/CompositeRequestInterceptor.java | 16 ++++----- .../rsocket/plugins/RequestInterceptor.java | 14 +++++--- .../plugins/RequestInterceptorTest.java | 10 +++--- .../plugins/TestRequestInterceptor.java | 9 ++--- 12 files changed, 102 insertions(+), 90 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java b/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java index dec946bab..eceb0976c 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java +++ b/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java @@ -141,7 +141,7 @@ public void subscribe(CoreSubscriber actual) { p.release(); if (interceptor != null) { - interceptor.onCancel(streamId); + interceptor.onCancel(streamId, FrameType.REQUEST_FNF); } return; @@ -153,7 +153,7 @@ public void subscribe(CoreSubscriber actual) { lazyTerminate(STATE, this); if (interceptor != null) { - interceptor.onTerminate(streamId, e); + interceptor.onTerminate(streamId, FrameType.REQUEST_FNF, e); } actual.onError(e); @@ -163,7 +163,7 @@ public void subscribe(CoreSubscriber actual) { lazyTerminate(STATE, this); if (interceptor != null) { - interceptor.onTerminate(streamId, null); + interceptor.onTerminate(streamId, FrameType.REQUEST_FNF, null); } actual.onComplete(); @@ -262,7 +262,7 @@ public Void block() { lazyTerminate(STATE, this); if (interceptor != null) { - interceptor.onTerminate(streamId, e); + interceptor.onTerminate(streamId, FrameType.REQUEST_FNF, e); } throw Exceptions.propagate(e); @@ -271,7 +271,7 @@ public Void block() { lazyTerminate(STATE, this); if (interceptor != null) { - interceptor.onTerminate(streamId, null); + interceptor.onTerminate(streamId, FrameType.REQUEST_FNF, null); } return null; diff --git a/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetResponderSubscriber.java b/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetResponderSubscriber.java index 889c98fde..e76fdf9ed 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetResponderSubscriber.java @@ -21,6 +21,7 @@ import io.netty.util.ReferenceCountUtil; import io.rsocket.Payload; import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.plugins.RequestInterceptor; import org.reactivestreams.Subscription; @@ -101,7 +102,7 @@ public void onNext(Void voidVal) {} public void onError(Throwable t) { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(this.streamId, t); + requestInterceptor.onTerminate(this.streamId, FrameType.REQUEST_FNF, t); } logger.debug("Dropped Outbound error", t); @@ -111,7 +112,7 @@ public void onError(Throwable t) { public void onComplete() { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(this.streamId, null); + requestInterceptor.onTerminate(this.streamId, FrameType.REQUEST_FNF, null); } } @@ -131,7 +132,7 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, t); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_FNF, t); } logger.debug("Reassembly has failed", t); @@ -151,7 +152,7 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(this.streamId, t); + requestInterceptor.onTerminate(this.streamId, FrameType.REQUEST_FNF, t); } logger.debug("Reassembly has failed", t); @@ -175,7 +176,7 @@ public final void handleCancel() { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onCancel(streamId); + requestInterceptor.onCancel(streamId, FrameType.REQUEST_FNF); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java index 8a57820c5..9b2936444 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java @@ -260,7 +260,7 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { this.inboundDone = true; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, t); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, t); } this.inboundSubscriber.onError(t); @@ -281,7 +281,7 @@ void sendFirstPayload(Payload firstPayload, long initialRequestN) { connection.sendFrame(streamId, cancelFrame); if (requestInterceptor != null) { - requestInterceptor.onCancel(streamId); + requestInterceptor.onCancel(streamId, FrameType.REQUEST_CHANNEL); } return; } @@ -364,7 +364,7 @@ void propagateErrorSafely(Throwable t) { if (!this.inboundDone) { final RequestInterceptor interceptor = requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(this.streamId, t); + interceptor.onTerminate(this.streamId, FrameType.REQUEST_CHANNEL, t); } this.inboundDone = true; @@ -386,7 +386,7 @@ public final void cancel() { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onCancel(this.streamId); + requestInterceptor.onCancel(this.streamId, FrameType.REQUEST_CHANNEL); } } @@ -449,7 +449,7 @@ public void onError(Throwable t) { synchronized (this) { final RequestInterceptor interceptor = requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, t); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, t); } this.inboundDone = true; @@ -492,7 +492,7 @@ public void onComplete() { if (isInboundTerminated) { final RequestInterceptor interceptor = requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, null); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, null); } } } @@ -515,7 +515,7 @@ public final void handleComplete() { final RequestInterceptor interceptor = requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, null); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, null); } } @@ -538,7 +538,7 @@ public final void handleError(Throwable cause) { } else if (isInboundTerminated(previousState)) { final RequestInterceptor interceptor = this.requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(this.streamId, cause); + interceptor.onTerminate(this.streamId, FrameType.REQUEST_CHANNEL, cause); } Operators.onErrorDropped(cause, this.inboundSubscriber.currentContext()); @@ -555,7 +555,7 @@ public final void handleError(Throwable cause) { final RequestInterceptor interceptor = requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, cause); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, cause); } this.inboundSubscriber.onError(cause); @@ -599,7 +599,7 @@ public void handleCancel() { if (inboundTerminated) { final RequestInterceptor interceptor = requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(this.streamId, null); + interceptor.onTerminate(this.streamId, FrameType.REQUEST_CHANNEL, null); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java index 9d4cd5f1e..c52fdca25 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java @@ -310,7 +310,7 @@ public void cancel() { if (isOutboundTerminated) { final RequestInterceptor interceptor = requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, null); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, null); } } } @@ -337,7 +337,7 @@ public final void handleCancel() { final RequestInterceptor interceptor = this.requestInterceptor; if (interceptor != null) { - interceptor.onCancel(this.streamId); + interceptor.onCancel(this.streamId, FrameType.REQUEST_CHANNEL); } return; } @@ -349,7 +349,7 @@ public final void handleCancel() { final RequestInterceptor interceptor = this.requestInterceptor; if (interceptor != null) { - interceptor.onCancel(this.streamId); + interceptor.onCancel(this.streamId, FrameType.REQUEST_CHANNEL); } } @@ -464,7 +464,7 @@ public final void handleError(Throwable t) { final RequestInterceptor interceptor = requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(this.streamId, t); + interceptor.onTerminate(this.streamId, FrameType.REQUEST_CHANNEL, t); } } @@ -490,7 +490,7 @@ public void handleComplete() { if (isOutboundTerminated) { final RequestInterceptor interceptor = this.requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(this.streamId, null); + interceptor.onTerminate(this.streamId, FrameType.REQUEST_CHANNEL, null); } } } @@ -514,7 +514,7 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) } else if (isOutboundTerminated(previousState)) { final RequestInterceptor interceptor = this.requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(this.streamId, t); + interceptor.onTerminate(this.streamId, FrameType.REQUEST_CHANNEL, t); } Operators.onErrorDropped(t, this.inboundSubscriber.currentContext()); @@ -530,7 +530,7 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) final RequestInterceptor interceptor = requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, t); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, t); } return; } @@ -572,7 +572,7 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) } else if (isOutboundTerminated(previousState)) { final RequestInterceptor interceptor = this.requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(this.streamId, e); + interceptor.onTerminate(this.streamId, FrameType.REQUEST_CHANNEL, e); } Operators.onErrorDropped(e, this.inboundSubscriber.currentContext()); @@ -591,7 +591,7 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) final RequestInterceptor interceptor = this.requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, e); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, e); } return; @@ -620,7 +620,7 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) } else if (isOutboundTerminated(previousState)) { final RequestInterceptor interceptor = this.requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(this.streamId, t); + interceptor.onTerminate(this.streamId, FrameType.REQUEST_CHANNEL, t); } Operators.onErrorDropped(t, this.inboundSubscriber.currentContext()); @@ -638,7 +638,7 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) final RequestInterceptor interceptor = requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, t); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, t); } return; @@ -690,7 +690,7 @@ public void onNext(Payload p) { final RequestInterceptor interceptor = this.requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, e); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, e); } Operators.onErrorDropped(e, this.inboundSubscriber.currentContext()); @@ -705,7 +705,7 @@ public void onNext(Payload p) { final RequestInterceptor interceptor = this.requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, e); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, e); } return; } @@ -720,7 +720,7 @@ public void onNext(Payload p) { } else if (isOutboundTerminated(previousState)) { final RequestInterceptor interceptor = this.requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, e); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, e); } Operators.onErrorDropped(e, this.inboundSubscriber.currentContext()); @@ -736,7 +736,7 @@ public void onNext(Payload p) { final RequestInterceptor interceptor = requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, e); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, e); } return; } @@ -749,7 +749,7 @@ public void onNext(Payload p) { long previousState = this.tryTerminate(false); final RequestInterceptor interceptor = requestInterceptor; if (interceptor != null && !isTerminated(previousState)) { - interceptor.onTerminate(streamId, t); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, t); } } } @@ -810,7 +810,7 @@ && isFirstFrameSent(previousState) final RequestInterceptor interceptor = this.requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, t); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, t); } } @@ -840,7 +840,7 @@ public void onComplete() { if (isInboundTerminated) { final RequestInterceptor interceptor = this.requestInterceptor; if (interceptor != null) { - interceptor.onTerminate(streamId, null); + interceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, null); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java index f3c52f648..850298a2a 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseRequesterMono.java @@ -185,7 +185,7 @@ void sendFirstPayload(Payload payload, long initialRequestN) { sm.remove(streamId, this); if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, e); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_RESPONSE, e); } this.actual.onError(e); @@ -204,7 +204,7 @@ void sendFirstPayload(Payload payload, long initialRequestN) { connection.sendFrame(streamId, cancelFrame); if (requestInterceptor != null) { - requestInterceptor.onCancel(streamId); + requestInterceptor.onCancel(streamId, FrameType.REQUEST_RESPONSE); } } } @@ -226,7 +226,7 @@ public final void cancel() { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onCancel(streamId); + requestInterceptor.onCancel(streamId, FrameType.REQUEST_RESPONSE); } } else if (!hasRequested(previousState)) { this.payload.release(); @@ -253,7 +253,7 @@ public final void handlePayload(Payload value) { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, null); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_RESPONSE, null); } final CoreSubscriber a = this.actual; @@ -279,7 +279,7 @@ public final void handleComplete() { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, null); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_RESPONSE, null); } this.actual.onComplete(); @@ -307,7 +307,7 @@ public final void handleError(Throwable cause) { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, cause); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_RESPONSE, cause); } this.actual.onError(cause); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java index 648afff13..3d9d020ff 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestResponseResponderSubscriber.java @@ -145,7 +145,7 @@ public void onNext(@Nullable Payload p) { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, null); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_RESPONSE, null); } return; } @@ -165,7 +165,7 @@ public void onNext(@Nullable Payload p) { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, e); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_RESPONSE, e); } return; } @@ -181,7 +181,7 @@ public void onNext(@Nullable Payload p) { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, e); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_RESPONSE, e); } return; } @@ -191,14 +191,14 @@ public void onNext(@Nullable Payload p) { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, null); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_RESPONSE, null); } } catch (Throwable t) { currentSubscription.cancel(); final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, t); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_RESPONSE, t); } } } @@ -228,7 +228,7 @@ public void onError(Throwable t) { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, t); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_RESPONSE, t); } } @@ -260,7 +260,7 @@ public void handleCancel() { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onCancel(streamId); + requestInterceptor.onCancel(streamId, FrameType.REQUEST_RESPONSE); } return; } @@ -276,7 +276,7 @@ public void handleCancel() { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onCancel(streamId); + requestInterceptor.onCancel(streamId, FrameType.REQUEST_RESPONSE); } } @@ -310,7 +310,7 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, t); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_RESPONSE, t); } return; } @@ -341,7 +341,7 @@ public void handleNext(ByteBuf frame, boolean hasFollows, boolean isLastPayload) final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, t); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_RESPONSE, t); } return; } 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 3608eaf52..47e8c1610 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java @@ -200,7 +200,7 @@ void sendFirstPayload(Payload payload, long initialRequestN) { sm.remove(streamId, this); if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, t); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, t); } this.inboundSubscriber.onError(t); @@ -219,7 +219,7 @@ void sendFirstPayload(Payload payload, long initialRequestN) { connection.sendFrame(streamId, cancelFrame); if (requestInterceptor != null) { - requestInterceptor.onCancel(streamId); + requestInterceptor.onCancel(streamId, FrameType.REQUEST_STREAM); } return; } @@ -259,7 +259,7 @@ public final void cancel() { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onCancel(streamId); + requestInterceptor.onCancel(streamId, FrameType.REQUEST_STREAM); } } else if (!hasRequested(previousState)) { // no need to send anything, since the first request has not happened @@ -290,11 +290,12 @@ public final void handleComplete() { return; } - this.requesterResponderSupport.remove(this.streamId, this); + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, null); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, null); } this.inboundSubscriber.onComplete(); @@ -315,13 +316,14 @@ public final void handleError(Throwable cause) { return; } - this.requesterResponderSupport.remove(this.streamId, this); + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); ReassemblyUtils.synchronizedRelease(this, previousState); final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, cause); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, cause); } this.inboundSubscriber.onError(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 6b06bc119..774fae9e5 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamResponderSubscriber.java @@ -146,7 +146,7 @@ public void onNext(Payload p) { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, e); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, e); } return; } @@ -164,7 +164,7 @@ public void onNext(Payload p) { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, e); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, e); } return; } @@ -178,7 +178,7 @@ public void onNext(Payload p) { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, t); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, t); } } } @@ -229,7 +229,7 @@ public void onError(Throwable t) { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, t); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, t); } } @@ -253,7 +253,7 @@ public void onComplete() { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, null); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, null); } } @@ -285,7 +285,7 @@ public final void handleCancel() { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onCancel(streamId); + requestInterceptor.onCancel(streamId, FrameType.REQUEST_STREAM); } return; } @@ -301,7 +301,7 @@ public final void handleCancel() { final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onCancel(streamId); + requestInterceptor.onCancel(streamId, FrameType.REQUEST_STREAM); } } @@ -336,7 +336,7 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, e); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, e); } logger.debug("Reassembly has failed", e); @@ -368,7 +368,7 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { - requestInterceptor.onTerminate(streamId, t); + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, t); } logger.debug("Reassembly has failed", t); diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/CompositeRequestInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/CompositeRequestInterceptor.java index b4e1a1ba3..d455c79ba 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/CompositeRequestInterceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/CompositeRequestInterceptor.java @@ -40,12 +40,12 @@ public void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metad } @Override - public void onTerminate(int streamId, @Nullable Throwable cause) { + public void onTerminate(int streamId, FrameType requestType, @Nullable Throwable cause) { final RequestInterceptor[] requestInterceptors = this.requestInterceptors; for (int i = 0; i < requestInterceptors.length; i++) { final RequestInterceptor requestInterceptor = requestInterceptors[i]; try { - requestInterceptor.onTerminate(streamId, cause); + requestInterceptor.onTerminate(streamId, requestType, cause); } catch (Throwable t) { Operators.onErrorDropped(t, Context.empty()); } @@ -53,12 +53,12 @@ public void onTerminate(int streamId, @Nullable Throwable cause) { } @Override - public void onCancel(int streamId) { + public void onCancel(int streamId, FrameType requestType) { final RequestInterceptor[] requestInterceptors = this.requestInterceptors; for (int i = 0; i < requestInterceptors.length; i++) { final RequestInterceptor requestInterceptor = requestInterceptors[i]; try { - requestInterceptor.onCancel(streamId); + requestInterceptor.onCancel(streamId, requestType); } catch (Throwable t) { Operators.onErrorDropped(t, Context.empty()); } @@ -121,18 +121,18 @@ public void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metad } @Override - public void onTerminate(int streamId, @Nullable Throwable cause) { + public void onTerminate(int streamId, FrameType requestType, @Nullable Throwable cause) { try { - requestInterceptor.onTerminate(streamId, cause); + requestInterceptor.onTerminate(streamId, requestType, cause); } catch (Throwable t) { Operators.onErrorDropped(t, Context.empty()); } } @Override - public void onCancel(int streamId) { + public void onCancel(int streamId, FrameType requestType) { try { - requestInterceptor.onCancel(streamId); + requestInterceptor.onCancel(streamId, requestType); } catch (Throwable t) { Operators.onErrorDropped(t, Context.empty()); } diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/RequestInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/RequestInterceptor.java index 5da850837..08131b39d 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/RequestInterceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/RequestInterceptor.java @@ -33,21 +33,27 @@ public interface RequestInterceptor extends Disposable { /** * Method which is being invoked once a successfully accepted request is terminated. This method * can be invoked only after the {@link #onStart(int, FrameType, ByteBuf)} method. This method is - * exclusive with {@link #onCancel(int)}. + * exclusive with {@link #onCancel(int, FrameType)}. * * @param streamId used by this request + * @param requestType of the request. Must be one of the following types {@link + * FrameType#REQUEST_FNF}, {@link FrameType#REQUEST_RESPONSE}, {@link + * FrameType#REQUEST_STREAM} or {@link FrameType#REQUEST_CHANNEL} * @param t with which this finished has terminated. Must be one of the following signals */ - void onTerminate(int streamId, @Nullable Throwable t); + void onTerminate(int streamId, FrameType requestType, @Nullable Throwable t); /** * Method which is being invoked once a successfully accepted request is cancelled. This method * can be invoked only after the {@link #onStart(int, FrameType, ByteBuf)} method. This method is - * exclusive with {@link #onTerminate(int, Throwable)}. + * exclusive with {@link #onTerminate(int, FrameType, Throwable)}. * + * @param requestType of the request. Must be one of the following types {@link + * FrameType#REQUEST_FNF}, {@link FrameType#REQUEST_RESPONSE}, {@link + * FrameType#REQUEST_STREAM} or {@link FrameType#REQUEST_CHANNEL} * @param streamId used by this request */ - void onCancel(int streamId); + void onCancel(int streamId, FrameType requestType); /** * Method which is being invoked on the request rejection. This method is being called only if the 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 24a035b78..6f156a380 100644 --- a/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java @@ -520,12 +520,13 @@ public void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metad } @Override - public void onTerminate(int streamId, @Nullable Throwable terminalSignal) { + public void onTerminate( + int streamId, FrameType requestType, @Nullable Throwable terminalSignal) { throw new RuntimeException("testOnTerminate"); } @Override - public void onCancel(int streamId) { + public void onCancel(int streamId, FrameType requestType) { throw new RuntimeException("testOnCancel"); } @@ -620,12 +621,13 @@ public void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metad } @Override - public void onTerminate(int streamId, @Nullable Throwable terminalSignal) { + public void onTerminate( + int streamId, FrameType requestType, @Nullable Throwable terminalSignal) { throw new RuntimeException("testOnTerminate"); } @Override - public void onCancel(int streamId) { + public void onCancel(int streamId, FrameType requestType) { throw new RuntimeException("testOnTerminate"); } diff --git a/rsocket-core/src/test/java/io/rsocket/plugins/TestRequestInterceptor.java b/rsocket-core/src/test/java/io/rsocket/plugins/TestRequestInterceptor.java index fe9de7ce1..8261b3f49 100644 --- a/rsocket-core/src/test/java/io/rsocket/plugins/TestRequestInterceptor.java +++ b/rsocket-core/src/test/java/io/rsocket/plugins/TestRequestInterceptor.java @@ -22,14 +22,15 @@ public void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metad } @Override - public void onTerminate(int streamId, @Nullable Throwable t) { + public void onTerminate(int streamId, FrameType requestType, @Nullable Throwable t) { events.add( - new Event(t == null ? EventType.ON_COMPLETE : EventType.ON_ERROR, streamId, null, t)); + new Event( + t == null ? EventType.ON_COMPLETE : EventType.ON_ERROR, streamId, requestType, t)); } @Override - public void onCancel(int streamId) { - events.add(new Event(EventType.ON_CANCEL, streamId, null, null)); + public void onCancel(int streamId, FrameType requestType) { + events.add(new Event(EventType.ON_CANCEL, streamId, requestType, null)); } @Override From 41eec029b63aa26ad663a392cef7aba10f2df5a2 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 22 Oct 2020 18:09:17 +0300 Subject: [PATCH 043/183] migrates to RequestInterceptor to track Stats Signed-off-by: Oleh Dokuka --- benchmarks/README.md | 2 +- .../loadbalance/BaseWeightedStats.java | 221 ++++ .../ClientLoadbalanceStrategy.java | 14 + .../java/io/rsocket/loadbalance/Ewma.java | 24 +- .../loadbalance/FluxDeferredResolution.java | 8 +- .../rsocket/loadbalance/FrugalQuantile.java | 10 +- .../rsocket/loadbalance/Int2LongHashMap.java | 1005 +++++++++++++++++ .../loadbalance/LoadbalanceRSocketClient.java | 9 +- .../java/io/rsocket/loadbalance/Median.java | 1 + ...eightedRSocket.java => PooledRSocket.java} | 140 +-- .../io/rsocket/loadbalance/RSocketPool.java | 56 +- .../java/io/rsocket/loadbalance/Stats.java | 308 ----- .../WeightedLoadbalanceStrategy.java | 120 +- .../rsocket/loadbalance/WeightedRSocket.java | 23 - .../io/rsocket/loadbalance/WeightedStats.java | 19 + .../WeightedStatsRequestInterceptor.java | 91 ++ .../RoundRobinRSocketLoadbalancerExample.java | 3 +- 17 files changed, 1521 insertions(+), 533 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java create mode 100644 rsocket-core/src/main/java/io/rsocket/loadbalance/ClientLoadbalanceStrategy.java create mode 100644 rsocket-core/src/main/java/io/rsocket/loadbalance/Int2LongHashMap.java rename rsocket-core/src/main/java/io/rsocket/loadbalance/{PooledWeightedRSocket.java => PooledRSocket.java} (59%) delete mode 100644 rsocket-core/src/main/java/io/rsocket/loadbalance/Stats.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedRSocket.java create mode 100644 rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java create mode 100644 rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRequestInterceptor.java diff --git a/benchmarks/README.md b/benchmarks/README.md index 6ba6755a6..656e2de4b 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -17,7 +17,7 @@ Specify extra profilers: Prominent profilers (for full list call `jmhProfilers` task): - comp - JitCompilations, tune your iterations - stack - which methods used most time -- gc - print garbage collection stats +- gc - print garbage collection defaultWeightedStats - hs_thr - thread usage Change report format from JSON to one of [CSV, JSON, NONE, SCSV, TEXT]: diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java new file mode 100644 index 000000000..6514244c3 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java @@ -0,0 +1,221 @@ +package io.rsocket.loadbalance; + +import io.rsocket.util.Clock; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +/** + * The base implementation of the {@link WeightedStats} interface + * + * @since 1.1 + */ +public class BaseWeightedStats implements WeightedStats { + + private static final double DEFAULT_LOWER_QUANTILE = 0.5; + private static final double DEFAULT_HIGHER_QUANTILE = 0.8; + private static final int INACTIVITY_FACTOR = 500; + private static final long DEFAULT_INITIAL_INTER_ARRIVAL_TIME = + Clock.unit().convert(1L, TimeUnit.SECONDS); + + private static final double STARTUP_PENALTY = Long.MAX_VALUE >> 12; + + private final Quantile lowerQuantile; + private final Quantile higherQuantile; + private final Ewma availabilityPercentage; + private final Median median; + private final Ewma interArrivalTime; + + private final long tau; + private final long inactivityFactor; + + private long errorStamp; // last we got an error + private long stamp; // last timestamp we sent a request + private long stamp0; // last timestamp we sent a request or receive a response + private long duration; // instantaneous cumulative duration + + private double availability = 1.0; + + private volatile int pendingRequests; // instantaneous rate + private static final AtomicIntegerFieldUpdater PENDING_REQUESTS = + AtomicIntegerFieldUpdater.newUpdater(BaseWeightedStats.class, "pendingRequests"); + private volatile int pendingStreams; // number of active streams + private static final AtomicIntegerFieldUpdater PENDING_STREAMS = + AtomicIntegerFieldUpdater.newUpdater(BaseWeightedStats.class, "pendingStreams"); + + protected BaseWeightedStats() { + this( + new FrugalQuantile(DEFAULT_LOWER_QUANTILE), + new FrugalQuantile(DEFAULT_HIGHER_QUANTILE), + INACTIVITY_FACTOR); + } + + private BaseWeightedStats( + Quantile lowerQuantile, Quantile higherQuantile, long inactivityFactor) { + this.lowerQuantile = lowerQuantile; + this.higherQuantile = higherQuantile; + this.inactivityFactor = inactivityFactor; + + long now = Clock.now(); + this.stamp = now; + this.errorStamp = now; + this.stamp0 = now; + this.duration = 0L; + this.pendingRequests = 0; + this.median = new Median(); + this.interArrivalTime = new Ewma(1, TimeUnit.MINUTES, DEFAULT_INITIAL_INTER_ARRIVAL_TIME); + this.availabilityPercentage = new Ewma(5, TimeUnit.SECONDS, 1.0); + this.tau = Clock.unit().convert((long) (5 / Math.log(2)), TimeUnit.SECONDS); + } + + @Override + public double lowerQuantileLatency() { + return lowerQuantile.estimation(); + } + + @Override + public double higherQuantileLatency() { + return higherQuantile.estimation(); + } + + @Override + public int pending() { + return pendingRequests + pendingStreams; + } + + @Override + public double availability() { + if (Clock.now() - stamp > tau) { + updateAvailability(1.0); + } + return availability * availabilityPercentage.value(); + } + + @Override + public double predictedLatency() { + final long now = Clock.now(); + final long elapsed; + + synchronized (this) { + elapsed = Math.max(now - stamp, 1L); + } + + final double latency; + final double prediction = median.estimation(); + + final int pending = this.pending(); + if (prediction == 0.0) { + if (pending == 0) { + latency = 0.0; // first request + } else { + // subsequent requests while we don't have any history + latency = STARTUP_PENALTY + pending; + } + } else if (pending == 0 && elapsed > inactivityFactor * interArrivalTime.value()) { + // if we did't see any data for a while, we decay the prediction by inserting + // artificial 0.0 into the median + median.insert(0.0); + latency = median.estimation(); + } else { + final double predicted = prediction * pending; + final double instant = instantaneous(now, pending); + + if (predicted < instant) { // NB: (0.0 < 0.0) == false + latency = instant / pending; // NB: pending never equal 0 here + } else { + // we are under the predictions + latency = prediction; + } + } + + return latency; + } + + long instantaneous(long now, int pending) { + return duration + (now - stamp0) * pending; + } + + void startStream() { + PENDING_STREAMS.incrementAndGet(this); + } + + void stopStream() { + PENDING_STREAMS.decrementAndGet(this); + } + + synchronized long startRequest() { + final long now = Clock.now(); + final int pendingRequests = this.pendingRequests; + + interArrivalTime.insert(now - stamp); + duration += Math.max(0, now - stamp0) * pendingRequests; + PENDING_REQUESTS.lazySet(this, pendingRequests + 1); + stamp = now; + stamp0 = now; + + return now; + } + + synchronized long stopRequest(long timestamp) { + final long now = Clock.now(); + final int pendingRequests = this.pendingRequests; + + duration += Math.max(0, now - stamp0) * pendingRequests - (now - timestamp); + PENDING_REQUESTS.lazySet(this, pendingRequests - 1); + stamp0 = now; + + return now; + } + + synchronized void record(double roundTripTime) { + median.insert(roundTripTime); + lowerQuantile.insert(roundTripTime); + higherQuantile.insert(roundTripTime); + } + + void updateAvailability(double value) { + availabilityPercentage.insert(value); + if (value == 0.0d) { + synchronized (this) { + errorStamp = Clock.now(); + } + } + } + + void setAvailability(double availability) { + this.availability = availability; + } + + @Override + public String toString() { + return "Stats{" + + "lowerQuantile=" + + lowerQuantile.estimation() + + ", higherQuantile=" + + higherQuantile.estimation() + + ", inactivityFactor=" + + inactivityFactor + + ", tau=" + + tau + + ", errorPercentage=" + + availabilityPercentage.value() + + ", pending=" + + pendingRequests + + ", errorStamp=" + + errorStamp + + ", stamp=" + + stamp + + ", stamp0=" + + stamp0 + + ", duration=" + + duration + + ", median=" + + median.estimation() + + ", interArrivalTime=" + + interArrivalTime.value() + + ", pendingStreams=" + + pendingStreams + + ", availability=" + + availability + + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/ClientLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/ClientLoadbalanceStrategy.java new file mode 100644 index 000000000..a35151fa6 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/ClientLoadbalanceStrategy.java @@ -0,0 +1,14 @@ +package io.rsocket.loadbalance; + +import io.rsocket.core.RSocketConnector; + +/** + * Extension for {@link LoadbalanceStrategy} which allows pre-setup {@link RSocketConnector} for + * {@link LoadbalanceStrategy} needs + * + * @since 1.1 + */ +public interface ClientLoadbalanceStrategy extends LoadbalanceStrategy { + + void initialize(RSocketConnector connector); +} diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/Ewma.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/Ewma.java index 4812114dd..0f87f6510 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/Ewma.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/Ewma.java @@ -18,6 +18,7 @@ import io.rsocket.util.Clock; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; /** * Compute the exponential weighted moving average of a series of values. The time at which you @@ -28,20 +29,27 @@ * equal to (200 - 100)/2 = 150 (half of the distance between the new and the old value) */ class Ewma { - private final long tau; - private volatile long stamp; - private volatile double ewma; + + final long tau; + + volatile long stamp; + static final AtomicLongFieldUpdater STAMP = + AtomicLongFieldUpdater.newUpdater(Ewma.class, "stamp"); + volatile double ewma; public Ewma(long halfLife, TimeUnit unit, double initialValue) { this.tau = Clock.unit().convert((long) (halfLife / Math.log(2)), unit); - stamp = 0L; - ewma = initialValue; + + this.ewma = initialValue; + + STAMP.lazySet(this, 0L); } public synchronized void insert(double x) { - long now = Clock.now(); - double elapsed = Math.max(0, now - stamp); - stamp = now; + final long now = Clock.now(); + final double elapsed = Math.max(0, now - stamp); + + STAMP.lazySet(this, now); double w = Math.exp(-elapsed / tau); ewma = w * ewma + (1.0 - w) * x; diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/FluxDeferredResolution.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/FluxDeferredResolution.java index 337edc530..6c2b9c3ea 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/FluxDeferredResolution.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/FluxDeferredResolution.java @@ -83,7 +83,7 @@ public final Context currentContext() { @Nullable @Override - public Object scanUnsafe(Attr key) { + public final Object scanUnsafe(Attr key) { long state = this.requested; if (key == Attr.PARENT) { @@ -145,7 +145,7 @@ public final void onNext(Payload payload) { } @Override - public void onError(Throwable t) { + public final void onError(Throwable t) { if (this.done) { Operators.onErrorDropped(t, this.actual.currentContext()); return; @@ -156,7 +156,7 @@ public void onError(Throwable t) { } @Override - public void onComplete() { + public final void onComplete() { if (this.done) { return; } @@ -206,7 +206,7 @@ public final void request(long n) { } } - public void cancel() { + public final void cancel() { long state = REQUESTED.getAndSet(this, STATE_TERMINATED); if (state == STATE_TERMINATED) { return; diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/FrugalQuantile.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/FrugalQuantile.java index efa32ff83..a15d88529 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/FrugalQuantile.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/FrugalQuantile.java @@ -26,12 +26,14 @@ *

    More info: http://blog.aggregateknowledge.com/2013/09/16/sketch-of-the-day-frugal-streaming/ */ class FrugalQuantile implements Quantile { - private final double increment; - volatile double estimate; + final double increment; + final SplittableRandom rnd; + int step; int sign; - private double quantile; - private SplittableRandom rnd; + double quantile; + + volatile double estimate; public FrugalQuantile(double quantile, double increment) { this.increment = increment; diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/Int2LongHashMap.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/Int2LongHashMap.java new file mode 100644 index 000000000..eebf82fe9 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/Int2LongHashMap.java @@ -0,0 +1,1005 @@ +/* + * Copyright 2014-2020 Real Logic Limited. + * + * Licensed under the Apache License, Version 2.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.loadbalance; + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.IntToLongFunction; +import reactor.util.annotation.Nullable; + +/** A open addressing with linear probing hash map specialised for primitive key and value pairs. */ +class Int2LongHashMap implements Map, Serializable { + static final float DEFAULT_LOAD_FACTOR = 0.55f; + static final int MIN_CAPACITY = 8; + private static final long serialVersionUID = -690554872053575793L; + + private final float loadFactor; + private final long missingValue; + private int resizeThreshold; + private int size = 0; + private final boolean shouldAvoidAllocation; + + private long[] entries; + private KeySet keySet; + private ValueCollection values; + private EntrySet entrySet; + + /** @param missingValue for the map that represents null. */ + public Int2LongHashMap(final long missingValue) { + this(MIN_CAPACITY, DEFAULT_LOAD_FACTOR, missingValue); + } + + /** + * @param initialCapacity for the map to override {@link #MIN_CAPACITY} + * @param loadFactor for the map to override {@link #DEFAULT_LOAD_FACTOR}. + * @param missingValue for the map that represents null. + */ + public Int2LongHashMap( + final int initialCapacity, final float loadFactor, final long missingValue) { + this(initialCapacity, loadFactor, missingValue, true); + } + + /** + * @param initialCapacity for the map to override {@link #MIN_CAPACITY} + * @param loadFactor for the map to override {@link #DEFAULT_LOAD_FACTOR}. + * @param missingValue for the map that represents null. + * @param shouldAvoidAllocation should allocation be avoided by caching iterators and map entries. + */ + public Int2LongHashMap( + final int initialCapacity, + final float loadFactor, + final long missingValue, + final boolean shouldAvoidAllocation) { + validateLoadFactor(loadFactor); + + this.loadFactor = loadFactor; + this.missingValue = missingValue; + this.shouldAvoidAllocation = shouldAvoidAllocation; + + capacity(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, initialCapacity))); + } + + /** + * The value to be used as a null marker in the map. + * + * @return value to be used as a null marker in the map. + */ + public long missingValue() { + return missingValue; + } + + /** + * Get the load factor applied for resize operations. + * + * @return the load factor applied for resize operations. + */ + public float loadFactor() { + return loadFactor; + } + + /** + * Get the total capacity for the map to which the load factor will be a fraction of. + * + * @return the total capacity for the map. + */ + public int capacity() { + return entries.length >> 1; + } + + /** + * Get the actual threshold which when reached the map will resize. This is a function of the + * current capacity and load factor. + * + * @return the threshold when the map will resize. + */ + public int resizeThreshold() { + return resizeThreshold; + } + + /** {@inheritDoc} */ + public int size() { + return size; + } + + /** {@inheritDoc} */ + public boolean isEmpty() { + return size == 0; + } + + /** + * Get a value using provided key avoiding boxing. + * + * @param key lookup key. + * @return value associated with the key or {@link #missingValue()} if key is not found in the + * map. + */ + public long get(final int key) { + final int mask = entries.length - 1; + int index = evenHash(key, mask); + + long value = missingValue; + while (entries[index + 1] != missingValue) { + if (entries[index] == key) { + value = entries[index + 1]; + break; + } + + index = next(index, mask); + } + + return value; + } + + /** + * Put a key value pair in the map. + * + * @param key lookup key + * @param value new value, must not be {@link #missingValue()} + * @return previous value associated with the key, or {@link #missingValue()} if none found + * @throws IllegalArgumentException if value is {@link #missingValue()} + */ + public long put(final int key, final long value) { + if (value == missingValue) { + throw new IllegalArgumentException("cannot accept missingValue"); + } + + final int mask = entries.length - 1; + int index = evenHash(key, mask); + long oldValue = missingValue; + + while (entries[index + 1] != missingValue) { + if (entries[index] == key) { + oldValue = entries[index + 1]; + break; + } + + index = next(index, mask); + } + + if (oldValue == missingValue) { + ++size; + entries[index] = key; + } + + entries[index + 1] = value; + + increaseCapacity(); + + return oldValue; + } + + private void increaseCapacity() { + if (size > resizeThreshold) { + // entries.length = 2 * capacity + final int newCapacity = entries.length; + rehash(newCapacity); + } + } + + private void rehash(final int newCapacity) { + final long[] oldEntries = entries; + final int length = entries.length; + + capacity(newCapacity); + + final long[] newEntries = entries; + final int mask = entries.length - 1; + + for (int keyIndex = 0; keyIndex < length; keyIndex += 2) { + final long value = oldEntries[keyIndex + 1]; + if (value != missingValue) { + final int key = (int) oldEntries[keyIndex]; + int index = evenHash(key, mask); + + while (newEntries[index + 1] != missingValue) { + index = next(index, mask); + } + + newEntries[index] = key; + newEntries[index + 1] = value; + } + } + } + + /** + * Int primitive specialised containsKey. + * + * @param key the key to check. + * @return true if the map contains key as a key, false otherwise. + */ + public boolean containsKey(final int key) { + return get(key) != missingValue; + } + + /** + * Does the map contain the value. + * + * @param value to be tested against contained values. + * @return true if contained otherwise value. + */ + public boolean containsValue(final long value) { + boolean found = false; + if (value != missingValue) { + final int length = entries.length; + int remaining = size; + + for (int valueIndex = 1; remaining > 0 && valueIndex < length; valueIndex += 2) { + if (missingValue != entries[valueIndex]) { + if (value == entries[valueIndex]) { + found = true; + break; + } + --remaining; + } + } + } + + return found; + } + + /** {@inheritDoc} */ + public void clear() { + if (size > 0) { + Arrays.fill(entries, missingValue); + size = 0; + } + } + + /** + * Compact the backing arrays by rehashing with a capacity just larger than current size and + * giving consideration to the load factor. + */ + public void compact() { + final int idealCapacity = (int) Math.round(size() * (1.0d / loadFactor)); + rehash(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, idealCapacity))); + } + + /** + * Primitive specialised version of {@link #computeIfAbsent(Object, Function)} + * + * @param key to search on. + * @param mappingFunction to provide a value if the get returns null. + * @return the value if found otherwise the missing value. + */ + public long computeIfAbsent(final int key, final IntToLongFunction mappingFunction) { + long value = get(key); + if (value == missingValue) { + value = mappingFunction.applyAsLong(key); + if (value != missingValue) { + put(key, value); + } + } + + return value; + } + + // ---------------- Boxed Versions Below ---------------- + + /** {@inheritDoc} */ + @Nullable + public Long get(final Object key) { + return valOrNull(get((int) key)); + } + + /** {@inheritDoc} */ + public Long put(final Integer key, final Long value) { + return valOrNull(put((int) key, (long) value)); + } + + /** {@inheritDoc} */ + public boolean containsKey(final Object key) { + return containsKey((int) key); + } + + /** {@inheritDoc} */ + public boolean containsValue(final Object value) { + return containsValue((long) value); + } + + /** {@inheritDoc} */ + public void putAll(final Map map) { + for (final Map.Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + /** {@inheritDoc} */ + public KeySet keySet() { + if (null == keySet) { + keySet = new KeySet(); + } + + return keySet; + } + + /** {@inheritDoc} */ + public ValueCollection values() { + if (null == values) { + values = new ValueCollection(); + } + + return values; + } + + /** {@inheritDoc} */ + public EntrySet entrySet() { + if (null == entrySet) { + entrySet = new EntrySet(); + } + + return entrySet; + } + + /** {@inheritDoc} */ + @Nullable + public Long remove(final Object key) { + return valOrNull(remove((int) key)); + } + + /** + * Remove value from the map using given key avoiding boxing. + * + * @param key whose mapping is to be removed from the map. + * @return removed value or {@link #missingValue()} if key was not found in the map. + */ + public long remove(final int key) { + final int mask = entries.length - 1; + int keyIndex = evenHash(key, mask); + + long oldValue = missingValue; + while (entries[keyIndex + 1] != missingValue) { + if (entries[keyIndex] == key) { + oldValue = entries[keyIndex + 1]; + entries[keyIndex + 1] = missingValue; + size--; + + compactChain(keyIndex); + + break; + } + + keyIndex = next(keyIndex, mask); + } + + return oldValue; + } + + @SuppressWarnings("FinalParameters") + private void compactChain(int deleteKeyIndex) { + final int mask = entries.length - 1; + int keyIndex = deleteKeyIndex; + + while (true) { + keyIndex = next(keyIndex, mask); + if (entries[keyIndex + 1] == missingValue) { + break; + } + + final int hash = evenHash((int) entries[keyIndex], mask); + + if ((keyIndex < hash && (hash <= deleteKeyIndex || deleteKeyIndex <= keyIndex)) + || (hash <= deleteKeyIndex && deleteKeyIndex <= keyIndex)) { + entries[deleteKeyIndex] = entries[keyIndex]; + entries[deleteKeyIndex + 1] = entries[keyIndex + 1]; + + entries[keyIndex + 1] = missingValue; + deleteKeyIndex = keyIndex; + } + } + } + + /** + * Get the minimum value stored in the map. If the map is empty then it will return {@link + * #missingValue()} + * + * @return the minimum value stored in the map. + */ + public long minValue() { + final long missingValue = this.missingValue; + long min = size == 0 ? missingValue : Long.MAX_VALUE; + final int length = entries.length; + + for (int valueIndex = 1; valueIndex < length; valueIndex += 2) { + final long value = entries[valueIndex]; + if (value != missingValue) { + min = Math.min(min, value); + } + } + + return min; + } + + /** + * Get the maximum value stored in the map. If the map is empty then it will return {@link + * #missingValue()} + * + * @return the maximum value stored in the map. + */ + public long maxValue() { + final long missingValue = this.missingValue; + long max = size == 0 ? missingValue : Long.MIN_VALUE; + final int length = entries.length; + + for (int valueIndex = 1; valueIndex < length; valueIndex += 2) { + final long value = entries[valueIndex]; + if (value != missingValue) { + max = Math.max(max, value); + } + } + + return max; + } + + /** {@inheritDoc} */ + public String toString() { + if (isEmpty()) { + return "{}"; + } + + final EntryIterator entryIterator = new EntryIterator(); + entryIterator.reset(); + + final StringBuilder sb = new StringBuilder().append('{'); + while (true) { + entryIterator.next(); + sb.append(entryIterator.getIntKey()).append('=').append(entryIterator.getLongValue()); + if (!entryIterator.hasNext()) { + return sb.append('}').toString(); + } + sb.append(',').append(' '); + } + } + + /** + * Primitive specialised version of {@link #replace(Object, Object)} + * + * @param key key with which the specified value is associated + * @param value value to be associated with the specified key + * @return the previous value associated with the specified key, or {@link #missingValue()} if + * there was no mapping for the key. + */ + public long replace(final int key, final long value) { + long currentValue = get(key); + if (currentValue != missingValue) { + currentValue = put(key, value); + } + + return currentValue; + } + + /** + * Primitive specialised version of {@link #replace(Object, Object, Object)} + * + * @param key key with which the specified value is associated + * @param oldValue value expected to be associated with the specified key + * @param newValue value to be associated with the specified key + * @return {@code true} if the value was replaced + */ + public boolean replace(final int key, final long oldValue, final long newValue) { + final long curValue = get(key); + if (curValue != oldValue || curValue == missingValue) { + return false; + } + + put(key, newValue); + + return true; + } + + /** {@inheritDoc} */ + public boolean equals(final Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof Map)) { + return false; + } + + final Map that = (Map) o; + + return size == that.size() && entrySet().equals(that.entrySet()); + } + + public int hashCode() { + return entrySet().hashCode(); + } + + private static int next(final int index, final int mask) { + return (index + 2) & mask; + } + + private void capacity(final int newCapacity) { + final int entriesLength = newCapacity * 2; + if (entriesLength < 0) { + throw new IllegalStateException("max capacity reached at size=" + size); + } + + /*@DoNotSub*/ resizeThreshold = (int) (newCapacity * loadFactor); + entries = new long[entriesLength]; + Arrays.fill(entries, missingValue); + } + + @Nullable + private Long valOrNull(final long value) { + return value == missingValue ? null : value; + } + + // ---------------- Utility Classes ---------------- + + /** Base iterator implementation. */ + abstract class AbstractIterator implements Serializable { + private static final long serialVersionUID = 5262459454112462433L; + /** Is current position valid. */ + protected boolean isPositionValid = false; + + private int remaining; + private int positionCounter; + private int stopCounter; + + final void reset() { + isPositionValid = false; + remaining = Int2LongHashMap.this.size; + final long missingValue = Int2LongHashMap.this.missingValue; + final long[] entries = Int2LongHashMap.this.entries; + final int capacity = entries.length; + + int keyIndex = capacity; + if (entries[capacity - 1] != missingValue) { + for (int i = 1; i < capacity; i += 2) { + if (entries[i] == missingValue) { + keyIndex = i - 1; + break; + } + } + } + + stopCounter = keyIndex; + positionCounter = keyIndex + capacity; + } + + /** + * Returns position of the key of the current entry. + * + * @return key position. + */ + protected final int keyPosition() { + return positionCounter & entries.length - 1; + } + + /** + * Number of remaining elements. + * + * @return number of remaining elements. + */ + public int remaining() { + return remaining; + } + + /** + * Check if there are more elements remaining. + * + * @return {@code true} if {@code remaining > 0}. + */ + public boolean hasNext() { + return remaining > 0; + } + + /** + * Advance to the next entry. + * + * @throws NoSuchElementException if no more entries available. + */ + protected final void findNext() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + final long[] entries = Int2LongHashMap.this.entries; + final long missingValue = Int2LongHashMap.this.missingValue; + final int mask = entries.length - 1; + + for (int keyIndex = positionCounter - 2; keyIndex >= stopCounter; keyIndex -= 2) { + final int index = keyIndex & mask; + if (entries[index + 1] != missingValue) { + isPositionValid = true; + positionCounter = keyIndex; + --remaining; + return; + } + } + + isPositionValid = false; + throw new IllegalStateException(); + } + + /** {@inheritDoc} */ + public void remove() { + if (isPositionValid) { + final int position = keyPosition(); + entries[position + 1] = missingValue; + --size; + + compactChain(position); + + isPositionValid = false; + } else { + throw new IllegalStateException(); + } + } + } + + /** Iterator over keys which supports access to unboxed keys via {@link #nextValue()}. */ + public final class KeyIterator extends AbstractIterator + implements Iterator, Serializable { + private static final long serialVersionUID = 9151493609653852972L; + + public Integer next() { + return nextValue(); + } + + /** + * Return next key. + * + * @return next key. + */ + public int nextValue() { + findNext(); + return (int) entries[keyPosition()]; + } + } + + /** Iterator over values which supports access to unboxed values. */ + public final class ValueIterator extends AbstractIterator + implements Iterator, Serializable { + private static final long serialVersionUID = -5670291734793552927L; + + public Long next() { + return nextValue(); + } + + /** + * Return next value. + * + * @return next value. + */ + public long nextValue() { + findNext(); + return entries[keyPosition() + 1]; + } + } + + /** Iterator over entries which supports access to unboxed keys and values. */ + public final class EntryIterator extends AbstractIterator + implements Iterator>, Entry, Serializable { + private static final long serialVersionUID = 1744408438593481051L; + + public Integer getKey() { + return getIntKey(); + } + + /** + * Returns the key of the current entry. + * + * @return the key. + */ + public int getIntKey() { + return (int) entries[keyPosition()]; + } + + public Long getValue() { + return getLongValue(); + } + + /** + * Returns the value of the current entry. + * + * @return the value. + */ + public long getLongValue() { + return entries[keyPosition() + 1]; + } + + public Long setValue(final Long value) { + return setValue(value.longValue()); + } + + /** + * Sets the value of the current entry. + * + * @param value to be set. + * @return previous value of the entry. + */ + public long setValue(final long value) { + if (!isPositionValid) { + throw new IllegalStateException(); + } + + if (missingValue == value) { + throw new IllegalArgumentException(); + } + + final int keyPosition = keyPosition(); + final long prevValue = entries[keyPosition + 1]; + entries[keyPosition + 1] = value; + return prevValue; + } + + public Entry next() { + findNext(); + + if (shouldAvoidAllocation) { + return this; + } + + return allocateDuplicateEntry(); + } + + private Entry allocateDuplicateEntry() { + return new MapEntry(getIntKey(), getLongValue()); + } + + /** {@inheritDoc} */ + public int hashCode() { + return Integer.hashCode(getIntKey()) ^ Long.hashCode(getLongValue()); + } + + /** {@inheritDoc} */ + public boolean equals(final Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof Entry)) { + return false; + } + + final Entry that = (Entry) o; + + return Objects.equals(getKey(), that.getKey()) && Objects.equals(getValue(), that.getValue()); + } + + /** An {@link java.util.Map.Entry} implementation. */ + public final class MapEntry implements Entry { + private final int k; + private final long v; + + /** + * Constructs entry with given key and value. + * + * @param k key. + * @param v value. + */ + public MapEntry(final int k, final long v) { + this.k = k; + this.v = v; + } + + public Integer getKey() { + return k; + } + + public Long getValue() { + return v; + } + + public Long setValue(final Long value) { + return Int2LongHashMap.this.put(k, value.longValue()); + } + + public int hashCode() { + return Integer.hashCode(getIntKey()) ^ Long.hashCode(getLongValue()); + } + + public boolean equals(final Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + + final Entry e = (Entry) o; + + return (e.getKey() != null && e.getValue() != null) + && (e.getKey().equals(k) && e.getValue().equals(v)); + } + + public String toString() { + return k + "=" + v; + } + } + } + + /** Set of keys which supports optional cached iterators to avoid allocation. */ + public final class KeySet extends AbstractSet implements Serializable { + private static final long serialVersionUID = -7645453993079742625L; + private final KeyIterator keyIterator = shouldAvoidAllocation ? new KeyIterator() : null; + + /** {@inheritDoc} */ + public KeyIterator iterator() { + KeyIterator keyIterator = this.keyIterator; + if (null == keyIterator) { + keyIterator = new KeyIterator(); + } + + keyIterator.reset(); + + return keyIterator; + } + + /** {@inheritDoc} */ + public int size() { + return Int2LongHashMap.this.size(); + } + + /** {@inheritDoc} */ + public boolean isEmpty() { + return Int2LongHashMap.this.isEmpty(); + } + + /** {@inheritDoc} */ + public void clear() { + Int2LongHashMap.this.clear(); + } + + /** {@inheritDoc} */ + public boolean contains(final Object o) { + return contains((int) o); + } + + /** + * Checks if key is contained in the map without boxing. + * + * @param key to check. + * @return {@code true} if key is contained in this map. + */ + public boolean contains(final int key) { + return containsKey(key); + } + } + + /** Collection of values which supports optionally cached iterators to avoid allocation. */ + public final class ValueCollection extends AbstractCollection implements Serializable { + private static final long serialVersionUID = -8925598924781601919L; + private final ValueIterator valueIterator = shouldAvoidAllocation ? new ValueIterator() : null; + + /** {@inheritDoc} */ + public ValueIterator iterator() { + ValueIterator valueIterator = this.valueIterator; + if (null == valueIterator) { + valueIterator = new ValueIterator(); + } + + valueIterator.reset(); + + return valueIterator; + } + + /** {@inheritDoc} */ + public int size() { + return Int2LongHashMap.this.size(); + } + + /** {@inheritDoc} */ + public boolean contains(final Object o) { + return contains((long) o); + } + + /** + * Checks if the value is contained in the map. + * + * @param value to be checked. + * @return {@code true} if value is contained in this map. + */ + public boolean contains(final long value) { + return containsValue(value); + } + } + + /** Set of entries which supports optionally cached iterators to avoid allocation. */ + public final class EntrySet extends AbstractSet> + implements Serializable { + private static final long serialVersionUID = 63641283589916174L; + private final EntryIterator entryIterator = shouldAvoidAllocation ? new EntryIterator() : null; + + /** {@inheritDoc} */ + public EntryIterator iterator() { + EntryIterator entryIterator = this.entryIterator; + if (null == entryIterator) { + entryIterator = new EntryIterator(); + } + + entryIterator.reset(); + + return entryIterator; + } + + /** {@inheritDoc} */ + public int size() { + return Int2LongHashMap.this.size(); + } + + /** {@inheritDoc} */ + public boolean isEmpty() { + return Int2LongHashMap.this.isEmpty(); + } + + /** {@inheritDoc} */ + public void clear() { + Int2LongHashMap.this.clear(); + } + + /** {@inheritDoc} */ + public boolean contains(final Object o) { + if (!(o instanceof Entry)) { + return false; + } + final Entry entry = (Entry) o; + final Long value = get(entry.getKey()); + + return value != null && value.equals(entry.getValue()); + } + + /** {@inheritDoc} */ + public Object[] toArray() { + return toArray(new Object[size()]); + } + + /** {@inheritDoc} */ + @SuppressWarnings("unchecked") + public T[] toArray(final T[] a) { + final T[] array = + a.length >= size + ? a + : (T[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size); + final EntryIterator it = iterator(); + + for (int i = 0; i < array.length; i++) { + if (it.hasNext()) { + it.next(); + array[i] = (T) it.allocateDuplicateEntry(); + } else { + array[i] = null; + break; + } + } + + return array; + } + } + + private static int evenHash(final int value, final int mask) { + final int hash = (value << 1) - (value << 8); + + return hash & mask; + } + + private static void validateLoadFactor(final float loadFactor) { + if (loadFactor < 0.1f || loadFactor > 0.9f) { + throw new IllegalArgumentException( + "load factor must be in the range of 0.1 to 0.9: " + loadFactor); + } + } + + private static int findNextPositivePowerOfTwo(final int value) { + return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(value - 1)); + } +} 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 89ae01f18..8822632a0 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java @@ -165,8 +165,15 @@ public Builder loadbalanceStrategy(LoadbalanceStrategy strategy) { /** Build the {@link LoadbalanceRSocketClient} instance. */ public LoadbalanceRSocketClient build() { + final RSocketConnector connector = initConnector(); + final LoadbalanceStrategy strategy = initLoadbalanceStrategy(); + + if (strategy instanceof ClientLoadbalanceStrategy) { + ((ClientLoadbalanceStrategy) strategy).initialize(connector); + } + return new LoadbalanceRSocketClient( - new RSocketPool(initConnector(), this.targetPublisher, initLoadbalanceStrategy())); + new RSocketPool(connector, this.targetPublisher, strategy)); } private RSocketConnector initConnector() { diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/Median.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/Median.java index 833bd5380..42b125b41 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/Median.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/Median.java @@ -18,6 +18,7 @@ /** This implementation gives better results because it considers more data-point. */ class Median extends FrugalQuantile { + public Median() { super(0.5, 1.0); } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledWeightedRSocket.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java similarity index 59% rename from rsocket-core/src/main/java/io/rsocket/loadbalance/PooledWeightedRSocket.java rename to rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java index ad681087e..3d9011bf6 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledWeightedRSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java @@ -28,29 +28,24 @@ import reactor.core.publisher.Operators; import reactor.util.context.Context; -/** Default implementation of {@link WeightedRSocket} stored in {@link RSocketPool} */ -final class PooledWeightedRSocket extends ResolvingOperator - implements CoreSubscriber, WeightedRSocket { +/** Default implementation of {@link RSocket} stored in {@link RSocketPool} */ +final class PooledRSocket extends ResolvingOperator + implements CoreSubscriber, RSocket { final RSocketPool parent; final Mono rSocketSource; final LoadbalanceTarget loadbalanceTarget; - final Stats stats; volatile Subscription s; - static final AtomicReferenceFieldUpdater S = - AtomicReferenceFieldUpdater.newUpdater(PooledWeightedRSocket.class, Subscription.class, "s"); + static final AtomicReferenceFieldUpdater S = + AtomicReferenceFieldUpdater.newUpdater(PooledRSocket.class, Subscription.class, "s"); - PooledWeightedRSocket( - RSocketPool parent, - Mono rSocketSource, - LoadbalanceTarget loadbalanceTarget, - Stats stats) { + PooledRSocket( + RSocketPool parent, Mono rSocketSource, LoadbalanceTarget loadbalanceTarget) { this.parent = parent; this.rSocketSource = rSocketSource; this.loadbalanceTarget = loadbalanceTarget; - this.stats = stats; } @Override @@ -113,13 +108,11 @@ protected void doSubscribe() { @Override protected void doOnValueResolved(RSocket value) { - stats.setAvailability(1.0); value.onClose().subscribe(null, t -> this.invalidate(), this::invalidate); } @Override protected void doOnValueExpired(RSocket value) { - stats.setAvailability(0.0); value.dispose(); this.dispose(); } @@ -133,7 +126,7 @@ public void dispose() { protected void doOnDispose() { final RSocketPool parent = this.parent; for (; ; ) { - final PooledWeightedRSocket[] sockets = parent.activeSockets; + final PooledRSocket[] sockets = parent.activeSockets; final int activeSocketsCount = sockets.length; int index = -1; @@ -149,7 +142,7 @@ protected void doOnDispose() { } final int lastIndex = activeSocketsCount - 1; - final PooledWeightedRSocket[] newSockets = new PooledWeightedRSocket[lastIndex]; + final PooledRSocket[] newSockets = new PooledRSocket[lastIndex]; if (index != 0) { System.arraycopy(sockets, 0, newSockets, 0, index); } @@ -162,43 +155,32 @@ protected void doOnDispose() { break; } } - stats.setAvailability(0.0); Operators.terminate(S, this); } @Override public Mono fireAndForget(Payload payload) { - return new RequestTrackingMonoInner<>(this, payload, FrameType.REQUEST_FNF); + return new MonoInner<>(this, payload, FrameType.REQUEST_FNF); } @Override public Mono requestResponse(Payload payload) { - return new RequestTrackingMonoInner<>(this, payload, FrameType.REQUEST_RESPONSE); + return new MonoInner<>(this, payload, FrameType.REQUEST_RESPONSE); } @Override public Flux requestStream(Payload payload) { - return new RequestTrackingFluxInner<>(this, payload, FrameType.REQUEST_STREAM); + return new FluxInner<>(this, payload, FrameType.REQUEST_STREAM); } @Override public Flux requestChannel(Publisher payloads) { - return new RequestTrackingFluxInner<>(this, payloads, FrameType.REQUEST_CHANNEL); + return new FluxInner<>(this, payloads, FrameType.REQUEST_CHANNEL); } @Override public Mono metadataPush(Payload payload) { - return new RequestTrackingMonoInner<>(this, payload, FrameType.METADATA_PUSH); - } - - /** - * Indicates number of active requests - * - * @return number of requests in progress - */ - @Override - public Stats stats() { - return stats; + return new MonoInner<>(this, payload, FrameType.METADATA_PUSH); } LoadbalanceTarget target() { @@ -207,15 +189,13 @@ LoadbalanceTarget target() { @Override public double availability() { - return stats.availability(); + final RSocket socket = valueIfResolved(); + return socket != null ? socket.availability() : 0.0d; } - static final class RequestTrackingMonoInner - extends MonoDeferredResolution { + static final class MonoInner extends MonoDeferredResolution { - long startTime; - - RequestTrackingMonoInner(PooledWeightedRSocket parent, Payload payload, FrameType requestType) { + MonoInner(PooledRSocket parent, Payload payload, FrameType requestType) { super(parent, payload, requestType); } @@ -249,58 +229,16 @@ public void accept(RSocket rSocket, Throwable t) { return; } - startTime = ((PooledWeightedRSocket) parent).stats.startRequest(); - source.subscribe((CoreSubscriber) this); } else { parent.add(this); } } - - @Override - public void onComplete() { - final long state = this.requested; - if (state != TERMINATED_STATE && REQUESTED.compareAndSet(this, state, TERMINATED_STATE)) { - final Stats stats = ((PooledWeightedRSocket) parent).stats; - final long now = stats.stopRequest(startTime); - stats.record(now - startTime); - super.onComplete(); - } - } - - @Override - public void onError(Throwable t) { - final long state = this.requested; - if (state != TERMINATED_STATE && REQUESTED.compareAndSet(this, state, TERMINATED_STATE)) { - Stats stats = ((PooledWeightedRSocket) parent).stats; - stats.stopRequest(startTime); - stats.recordError(0.0); - super.onError(t); - } - } - - @Override - public void cancel() { - long state = REQUESTED.getAndSet(this, STATE_TERMINATED); - if (state == STATE_TERMINATED) { - return; - } - - if (state == STATE_SUBSCRIBED) { - this.s.cancel(); - ((PooledWeightedRSocket) parent).stats.stopRequest(startTime); - } else { - this.parent.remove(this); - ReferenceCountUtil.safeRelease(this.payload); - } - } } - static final class RequestTrackingFluxInner - extends FluxDeferredResolution { + static final class FluxInner extends FluxDeferredResolution { - RequestTrackingFluxInner( - PooledWeightedRSocket parent, INPUT fluxOrPayload, FrameType requestType) { + FluxInner(PooledRSocket parent, INPUT fluxOrPayload, FrameType requestType) { super(parent, fluxOrPayload, requestType); } @@ -333,48 +271,10 @@ public void accept(RSocket rSocket, Throwable t) { return; } - ((PooledWeightedRSocket) parent).stats.startStream(); - source.subscribe(this); } else { parent.add(this); } } - - @Override - public void onComplete() { - final long state = this.requested; - if (state != TERMINATED_STATE && REQUESTED.compareAndSet(this, state, TERMINATED_STATE)) { - ((PooledWeightedRSocket) parent).stats.stopStream(); - super.onComplete(); - } - } - - @Override - public void onError(Throwable t) { - final long state = this.requested; - if (state != TERMINATED_STATE && REQUESTED.compareAndSet(this, state, TERMINATED_STATE)) { - ((PooledWeightedRSocket) parent).stats.stopStream(); - super.onError(t); - } - } - - @Override - public void cancel() { - long state = REQUESTED.getAndSet(this, STATE_TERMINATED); - if (state == STATE_TERMINATED) { - return; - } - - if (state == STATE_SUBSCRIBED) { - this.s.cancel(); - ((PooledWeightedRSocket) parent).stats.stopStream(); - } else { - this.parent.remove(this); - if (requestType == FrameType.REQUEST_STREAM) { - ReferenceCountUtil.safeRelease(this.fluxOrPayload); - } - } - } } } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java index dbd05abcb..733b06f3c 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java @@ -28,7 +28,6 @@ import java.util.ListIterator; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; -import java.util.function.Supplier; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; @@ -43,16 +42,15 @@ class RSocketPool extends ResolvingOperator final DeferredResolutionRSocket deferredResolutionRSocket = new DeferredResolutionRSocket(this); final RSocketConnector connector; final LoadbalanceStrategy loadbalanceStrategy; - final Supplier statsSupplier; - volatile PooledWeightedRSocket[] activeSockets; + volatile PooledRSocket[] activeSockets; - static final AtomicReferenceFieldUpdater ACTIVE_SOCKETS = + static final AtomicReferenceFieldUpdater ACTIVE_SOCKETS = AtomicReferenceFieldUpdater.newUpdater( - RSocketPool.class, PooledWeightedRSocket[].class, "activeSockets"); + RSocketPool.class, PooledRSocket[].class, "activeSockets"); - static final PooledWeightedRSocket[] EMPTY = new PooledWeightedRSocket[0]; - static final PooledWeightedRSocket[] TERMINATED = new PooledWeightedRSocket[0]; + static final PooledRSocket[] EMPTY = new PooledRSocket[0]; + static final PooledRSocket[] TERMINATED = new PooledRSocket[0]; volatile Subscription s; static final AtomicReferenceFieldUpdater S = @@ -64,11 +62,6 @@ public RSocketPool( LoadbalanceStrategy loadbalanceStrategy) { this.connector = connector; this.loadbalanceStrategy = loadbalanceStrategy; - if (loadbalanceStrategy instanceof WeightedLoadbalanceStrategy) { - this.statsSupplier = Stats::create; - } else { - this.statsSupplier = Stats::noOps; - } ACTIVE_SOCKETS.lazySet(this, EMPTY); @@ -105,8 +98,8 @@ public void onNext(List targets) { return; } - PooledWeightedRSocket[] previouslyActiveSockets; - PooledWeightedRSocket[] activeSockets; + PooledRSocket[] previouslyActiveSockets; + PooledRSocket[] activeSockets; for (; ; ) { HashMap rSocketSuppliersCopy = new HashMap<>(); @@ -117,11 +110,11 @@ public void onNext(List targets) { // checking intersection of active RSocket with the newly received set previouslyActiveSockets = this.activeSockets; - PooledWeightedRSocket[] nextActiveSockets = - new PooledWeightedRSocket[previouslyActiveSockets.length + rSocketSuppliersCopy.size()]; + PooledRSocket[] nextActiveSockets = + new PooledRSocket[previouslyActiveSockets.length + rSocketSuppliersCopy.size()]; int position = 0; for (int i = 0; i < previouslyActiveSockets.length; i++) { - PooledWeightedRSocket rSocket = previouslyActiveSockets[i]; + PooledRSocket rSocket = previouslyActiveSockets[i]; Integer index = rSocketSuppliersCopy.remove(rSocket.target()); if (index == null) { @@ -140,11 +133,7 @@ public void onNext(List targets) { // put newly create RSocket instance LoadbalanceTarget target = targets.get(index); nextActiveSockets[position++] = - new PooledWeightedRSocket( - this, - this.connector.connect(target.getTransport()), - target, - this.statsSupplier.get()); + new PooledRSocket(this, this.connector.connect(target.getTransport()), target); } } } @@ -152,11 +141,7 @@ public void onNext(List targets) { // going though brightly new rsocket for (LoadbalanceTarget target : rSocketSuppliersCopy.keySet()) { nextActiveSockets[position++] = - new PooledWeightedRSocket( - this, - this.connector.connect(target.getTransport()), - target, - this.statsSupplier.get()); + new PooledRSocket(this, this.connector.connect(target.getTransport()), target); } // shrank to actual length @@ -215,7 +200,7 @@ RSocket select() { @Nullable RSocket doSelect() { - WeightedRSocket[] sockets = this.activeSockets; + PooledRSocket[] sockets = this.activeSockets; if (sockets == EMPTY) { return null; } @@ -224,8 +209,15 @@ RSocket doSelect() { } @Override - public WeightedRSocket get(int index) { - return activeSockets[index]; + public RSocket get(int index) { + final PooledRSocket socket = activeSockets[index]; + final RSocket realValue = socket.valueIfResolved(); + + if (realValue != null) { + return realValue; + } + + return socket; } @Override @@ -423,7 +415,7 @@ public void clear() { } @Override - public WeightedRSocket set(int index, RSocket element) { + public RSocket set(int index, RSocket element) { throw new UnsupportedOperationException(); } @@ -433,7 +425,7 @@ public void add(int index, RSocket element) { } @Override - public WeightedRSocket remove(int index) { + public RSocket remove(int index) { throw new UnsupportedOperationException(); } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/Stats.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/Stats.java deleted file mode 100644 index 2e9828938..000000000 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/Stats.java +++ /dev/null @@ -1,308 +0,0 @@ -package io.rsocket.loadbalance; - -import io.rsocket.Availability; -import io.rsocket.util.Clock; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; - -class Stats implements Availability { - - private static final double DEFAULT_LOWER_QUANTILE = 0.5; - private static final double DEFAULT_HIGHER_QUANTILE = 0.8; - private static final int INACTIVITY_FACTOR = 500; - private static final long DEFAULT_INITIAL_INTER_ARRIVAL_TIME = - Clock.unit().convert(1L, TimeUnit.SECONDS); - - private static final double STARTUP_PENALTY = Long.MAX_VALUE >> 12; - - private final Quantile lowerQuantile; - private final Quantile higherQuantile; - private final Ewma errorPercentage; - private final Median median; - private final Ewma interArrivalTime; - - private final long tau; - private final long inactivityFactor; - - private long errorStamp; // last we got an error - private long stamp; // last timestamp we sent a request - private long stamp0; // last timestamp we sent a request or receive a response - private long duration; // instantaneous cumulative duration - - private double availability = 1.0; - - private volatile int pending; // instantaneous rate - private volatile long pendingStreams; // number of active streams - private static final AtomicLongFieldUpdater PENDING_STREAMS = - AtomicLongFieldUpdater.newUpdater(Stats.class, "pendingStreams"); - - private Stats() { - this( - new FrugalQuantile(DEFAULT_LOWER_QUANTILE), - new FrugalQuantile(DEFAULT_HIGHER_QUANTILE), - INACTIVITY_FACTOR); - } - - private Stats(Quantile lowerQuantile, Quantile higherQuantile, long inactivityFactor) { - this.lowerQuantile = lowerQuantile; - this.higherQuantile = higherQuantile; - this.inactivityFactor = inactivityFactor; - - long now = Clock.now(); - this.stamp = now; - this.errorStamp = now; - this.stamp0 = now; - this.duration = 0L; - this.pending = 0; - this.median = new Median(); - this.interArrivalTime = new Ewma(1, TimeUnit.MINUTES, DEFAULT_INITIAL_INTER_ARRIVAL_TIME); - this.errorPercentage = new Ewma(5, TimeUnit.SECONDS, 1.0); - this.tau = Clock.unit().convert((long) (5 / Math.log(2)), TimeUnit.SECONDS); - } - - public double errorPercentage() { - return errorPercentage.value(); - } - - public double medianLatency() { - return median.estimation(); - } - - public double lowerQuantileLatency() { - return lowerQuantile.estimation(); - } - - public double higherQuantileLatency() { - return higherQuantile.estimation(); - } - - public double interArrivalTime() { - return interArrivalTime.value(); - } - - public int pending() { - return pending; - } - - public long lastTimeUsedMillis() { - return stamp0; - } - - @Override - public double availability() { - if (Clock.now() - stamp > tau) { - recordError(1.0); - } - return availability * errorPercentage.value(); - } - - public synchronized double predictedLatency() { - long now = Clock.now(); - long elapsed = Math.max(now - stamp, 1L); - - double weight; - double prediction = median.estimation(); - - if (prediction == 0.0) { - if (pending == 0) { - weight = 0.0; // first request - } else { - // subsequent requests while we don't have any history - weight = STARTUP_PENALTY + pending; - } - } else if (pending == 0 && elapsed > inactivityFactor * interArrivalTime.value()) { - // if we did't see any data for a while, we decay the prediction by inserting - // artificial 0.0 into the median - median.insert(0.0); - weight = median.estimation(); - } else { - double predicted = prediction * pending; - double instant = instantaneous(now); - - if (predicted < instant) { // NB: (0.0 < 0.0) == false - weight = instant / pending; // NB: pending never equal 0 here - } else { - // we are under the predictions - weight = prediction; - } - } - - return weight; - } - - synchronized long instantaneous(long now) { - return duration + (now - stamp0) * pending; - } - - public void startStream() { - PENDING_STREAMS.incrementAndGet(this); - } - - public void stopStream() { - PENDING_STREAMS.decrementAndGet(this); - } - - public synchronized long startRequest() { - long now = Clock.now(); - interArrivalTime.insert(now - stamp); - duration += Math.max(0, now - stamp0) * pending; - pending += 1; - stamp = now; - stamp0 = now; - return now; - } - - public synchronized long stopRequest(long timestamp) { - long now = Clock.now(); - duration += Math.max(0, now - stamp0) * pending - (now - timestamp); - pending -= 1; - stamp0 = now; - return now; - } - - public synchronized void record(double roundTripTime) { - median.insert(roundTripTime); - lowerQuantile.insert(roundTripTime); - higherQuantile.insert(roundTripTime); - } - - public synchronized void recordError(double value) { - errorPercentage.insert(value); - errorStamp = Clock.now(); - } - - public void setAvailability(double availability) { - this.availability = availability; - } - - @Override - public String toString() { - return "Stats{" - + "lowerQuantile=" - + lowerQuantile.estimation() - + ", higherQuantile=" - + higherQuantile.estimation() - + ", inactivityFactor=" - + inactivityFactor - + ", tau=" - + tau - + ", errorPercentage=" - + errorPercentage.value() - + ", pending=" - + pending - + ", errorStamp=" - + errorStamp - + ", stamp=" - + stamp - + ", stamp0=" - + stamp0 - + ", duration=" - + duration - + ", median=" - + median.estimation() - + ", interArrivalTime=" - + interArrivalTime.value() - + ", pendingStreams=" - + pendingStreams - + ", availability=" - + availability - + '}'; - } - - private static final class NoOpsStats extends Stats { - - static final Stats INSTANCE = new NoOpsStats(); - - private NoOpsStats() {} - - @Override - public double errorPercentage() { - return 0.0d; - } - - @Override - public double medianLatency() { - return 0.0d; - } - - @Override - public double lowerQuantileLatency() { - return 0.0d; - } - - @Override - public double higherQuantileLatency() { - return 0.0d; - } - - @Override - public double interArrivalTime() { - return 0; - } - - @Override - public int pending() { - return 0; - } - - @Override - public long lastTimeUsedMillis() { - return 0; - } - - @Override - public double availability() { - return 1.0d; - } - - @Override - public double predictedLatency() { - return 0.0d; - } - - @Override - long instantaneous(long now) { - return 0; - } - - @Override - public void startStream() {} - - @Override - public void stopStream() {} - - @Override - public long startRequest() { - return 0; - } - - @Override - public long stopRequest(long timestamp) { - return 0; - } - - @Override - public void record(double roundTripTime) {} - - @Override - public void recordError(double value) {} - - @Override - public String toString() { - return "NoOpsStats{}"; - } - } - - public static Stats noOps() { - return NoOpsStats.INSTANCE; - } - - public static Stats create() { - return new Stats(); - } - - public static Stats create( - Quantile lowerQuantile, Quantile higherQuantile, long inactivityFactor) { - return new Stats(lowerQuantile, higherQuantile, inactivityFactor); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java index 03bc0530d..cdce957aa 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java @@ -17,9 +17,14 @@ package io.rsocket.loadbalance; import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.plugins.RequestInterceptor; import java.util.List; import java.util.SplittableRandom; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Function; import reactor.util.annotation.Nullable; /** @@ -28,7 +33,7 @@ * * @since 1.1 */ -public class WeightedLoadbalanceStrategy implements LoadbalanceStrategy { +public class WeightedLoadbalanceStrategy implements ClientLoadbalanceStrategy { private static final double EXP_FACTOR = 4.0; @@ -36,18 +41,36 @@ public class WeightedLoadbalanceStrategy implements LoadbalanceStrategy { final int effort; final SplittableRandom splittableRandom; + final Function weightedStatsResolver; public WeightedLoadbalanceStrategy() { - this(EFFORT); + this(new DefaultWeightedStatsResolver()); } - public WeightedLoadbalanceStrategy(int effort) { - this(effort, new SplittableRandom(System.nanoTime())); + public WeightedLoadbalanceStrategy(Function weightedStatsResolver) { + this(EFFORT, weightedStatsResolver); } - public WeightedLoadbalanceStrategy(int effort, SplittableRandom splittableRandom) { + public WeightedLoadbalanceStrategy( + int effort, Function weightedStatsResolver) { + this(effort, new SplittableRandom(System.nanoTime()), weightedStatsResolver); + } + + public WeightedLoadbalanceStrategy( + int effort, + SplittableRandom splittableRandom, + Function weightedStatsResolver) { this.effort = effort; this.splittableRandom = splittableRandom; + this.weightedStatsResolver = weightedStatsResolver; + } + + @Override + public void initialize(RSocketConnector connector) { + final Function resolver = weightedStatsResolver; + if (resolver instanceof DefaultWeightedStatsResolver) { + ((DefaultWeightedStatsResolver) resolver).init(connector); + } } @Override @@ -55,18 +78,19 @@ public RSocket select(List sockets) { final int effort = this.effort; final int size = sockets.size(); - WeightedRSocket weightedRSocket; + RSocket weightedRSocket; + final Function weightedStatsResolver = this.weightedStatsResolver; switch (size) { case 1: - weightedRSocket = (WeightedRSocket) sockets.get(0); + weightedRSocket = sockets.get(0); break; case 2: { - WeightedRSocket rsc1 = (WeightedRSocket) sockets.get(0); - WeightedRSocket rsc2 = (WeightedRSocket) sockets.get(1); + RSocket rsc1 = sockets.get(0); + RSocket rsc2 = sockets.get(1); - double w1 = algorithmicWeight(rsc1); - double w2 = algorithmicWeight(rsc2); + double w1 = algorithmicWeight(rsc1, weightedStatsResolver.apply(rsc1)); + double w2 = algorithmicWeight(rsc2, weightedStatsResolver.apply(rsc2)); if (w1 < w2) { weightedRSocket = rsc2; } else { @@ -76,8 +100,8 @@ public RSocket select(List sockets) { break; default: { - WeightedRSocket rsc1 = null; - WeightedRSocket rsc2 = null; + RSocket rsc1 = null; + RSocket rsc2 = null; for (int i = 0; i < effort; i++) { int i1 = ThreadLocalRandom.current().nextInt(size); @@ -86,19 +110,26 @@ public RSocket select(List sockets) { if (i2 >= i1) { i2++; } - rsc1 = (WeightedRSocket) sockets.get(i1); - rsc2 = (WeightedRSocket) sockets.get(i2); + rsc1 = sockets.get(i1); + rsc2 = sockets.get(i2); if (rsc1.availability() > 0.0 && rsc2.availability() > 0.0) { break; } } - double w1 = algorithmicWeight(rsc1); - double w2 = algorithmicWeight(rsc2); - if (w1 < w2) { - weightedRSocket = rsc2; - } else { + if (rsc1 != null & rsc2 != null) { + double w1 = algorithmicWeight(rsc1, weightedStatsResolver.apply(rsc1)); + double w2 = algorithmicWeight(rsc2, weightedStatsResolver.apply(rsc2)); + + if (w1 < w2) { + weightedRSocket = rsc2; + } else { + weightedRSocket = rsc1; + } + } else if (rsc1 != null) { weightedRSocket = rsc1; + } else { + weightedRSocket = rsc2; } } } @@ -106,20 +137,19 @@ public RSocket select(List sockets) { return weightedRSocket; } - private static double algorithmicWeight(@Nullable final WeightedRSocket weightedRSocket) { - if (weightedRSocket == null - || weightedRSocket.isDisposed() - || weightedRSocket.availability() == 0.0) { + private static double algorithmicWeight( + RSocket rSocket, @Nullable final WeightedStats weightedStats) { + if (weightedStats == null || rSocket.isDisposed() || rSocket.availability() == 0.0) { return 0.0; } - final Stats stats = weightedRSocket.stats(); - final int pending = stats.pending(); - double latency = stats.predictedLatency(); + final int pending = weightedStats.pending(); + + double latency = weightedStats.predictedLatency(); - final double low = stats.lowerQuantileLatency(); + final double low = weightedStats.lowerQuantileLatency(); final double high = Math.max( - stats.higherQuantileLatency(), + weightedStats.higherQuantileLatency(), low * 1.001); // ensure higherQuantile > lowerQuantile + .1% final double bandWidth = Math.max(high - low, 1); @@ -129,11 +159,41 @@ private static double algorithmicWeight(@Nullable final WeightedRSocket weighted latency *= calculateFactor(latency, high, bandWidth); } - return weightedRSocket.availability() * 1.0 / (1.0 + latency * (pending + 1)); + return rSocket.availability() / (1.0d + latency * (pending + 1)); } private static double calculateFactor(final double u, final double l, final double bandWidth) { final double alpha = (u - l) / bandWidth; return Math.pow(1 + alpha, EXP_FACTOR); } + + static class DefaultWeightedStatsResolver implements Function { + + final ConcurrentMap rsocketsInterceptors = + new ConcurrentHashMap<>(); + + @Override + public WeightedStats apply(RSocket rSocket) { + return rsocketsInterceptors.get(rSocket); + } + + void init(RSocketConnector connector) { + connector.interceptors( + ir -> + ir.forRequester( + (Function) + rSocket -> { + final WeightedStatsRequestInterceptor interceptor = + new WeightedStatsRequestInterceptor() { + @Override + public void dispose() { + rsocketsInterceptors.remove(rSocket); + } + }; + rsocketsInterceptors.put(rSocket, interceptor); + + return interceptor; + })); + } + } } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedRSocket.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedRSocket.java deleted file mode 100644 index 488a7134d..000000000 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedRSocket.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.loadbalance; - -import io.rsocket.RSocket; - -interface WeightedRSocket extends RSocket { - - Stats stats(); -} diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java new file mode 100644 index 000000000..b0cf02560 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java @@ -0,0 +1,19 @@ +package io.rsocket.loadbalance; + +import io.rsocket.Availability; + +/** + * Representation of stats used by the {@link WeightedLoadbalanceStrategy} + * + * @since 1.1 + */ +public interface WeightedStats extends Availability { + + double higherQuantileLatency(); + + double lowerQuantileLatency(); + + int pending(); + + double predictedLatency(); +} diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRequestInterceptor.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRequestInterceptor.java new file mode 100644 index 000000000..f1e790309 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStatsRequestInterceptor.java @@ -0,0 +1,91 @@ +package io.rsocket.loadbalance; + +import io.netty.buffer.ByteBuf; +import io.rsocket.frame.FrameType; +import io.rsocket.plugins.RequestInterceptor; +import reactor.util.annotation.Nullable; + +/** + * A {@link RequestInterceptor} implementation + * + * @since 1.1 + */ +public class WeightedStatsRequestInterceptor extends BaseWeightedStats + implements RequestInterceptor { + + final Int2LongHashMap requestsStartTime = new Int2LongHashMap(-1); + + public WeightedStatsRequestInterceptor() { + super(); + } + + @Override + public final void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metadata) { + switch (requestType) { + case REQUEST_FNF: + case REQUEST_RESPONSE: + final long startTime = startRequest(); + final Int2LongHashMap requestsStartTime = this.requestsStartTime; + synchronized (requestsStartTime) { + requestsStartTime.put(streamId, startTime); + } + break; + case REQUEST_STREAM: + case REQUEST_CHANNEL: + this.startStream(); + } + } + + @Override + public final void onTerminate(int streamId, FrameType requestType, @Nullable Throwable t) { + switch (requestType) { + case REQUEST_FNF: + case REQUEST_RESPONSE: + long startTime; + final Int2LongHashMap requestsStartTime = this.requestsStartTime; + synchronized (requestsStartTime) { + startTime = requestsStartTime.remove(streamId); + } + long endTime = stopRequest(startTime); + if (t == null) { + record(endTime - startTime); + } + break; + case REQUEST_STREAM: + case REQUEST_CHANNEL: + stopStream(); + break; + } + + if (t != null) { + updateAvailability(0.0d); + } else { + updateAvailability(1.0d); + } + } + + @Override + public final void onCancel(int streamId, FrameType requestType) { + switch (requestType) { + case REQUEST_FNF: + case REQUEST_RESPONSE: + long startTime; + final Int2LongHashMap requestsStartTime = this.requestsStartTime; + synchronized (requestsStartTime) { + startTime = requestsStartTime.remove(streamId); + } + stopRequest(startTime); + break; + case REQUEST_STREAM: + case REQUEST_CHANNEL: + stopStream(); + break; + } + } + + @Override + public final void onReject(Throwable rejectionReason, FrameType requestType, ByteBuf metadata) {} + + @Override + public void dispose() {} +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java index 27d10b472..feafdb7a6 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java @@ -97,11 +97,10 @@ public static void main(String[] args) { }); RSocketClient rSocketClient = - LoadbalanceRSocketClient.builder(producer).roundRobinLoadbalanceStrategy().build(); + LoadbalanceRSocketClient.builder(producer).weightedLoadbalanceStrategy().build(); for (int i = 0; i < 10000; i++) { try { - rSocketClient.requestResponse(Mono.just(DefaultPayload.create("test" + i))).block(); } catch (Throwable t) { // no ops From dc85fa7013f15491ba74ca8d9ad2820924e7dfbc Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 22 Oct 2020 21:05:12 +0300 Subject: [PATCH 044/183] improves LeaksTrackingByteBufAllocator to wait for all buffer to be released Signed-off-by: Oleh Dokuka --- .../buffer/LeaksTrackingByteBufAllocator.java | 143 +++++++++++++++++- .../io/rsocket/core/AbstractSocketRule.java | 5 +- 2 files changed, 140 insertions(+), 8 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 800e5d678..96d2720d1 100644 --- a/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java +++ b/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java @@ -1,8 +1,16 @@ package io.rsocket.buffer; +import static java.util.concurrent.locks.LockSupport.parkNanos; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; +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.Set; import java.util.concurrent.ConcurrentLinkedQueue; import org.assertj.core.api.Assertions; @@ -19,24 +27,89 @@ public class LeaksTrackingByteBufAllocator implements ByteBufAllocator { * @return */ public static LeaksTrackingByteBufAllocator instrument(ByteBufAllocator allocator) { - return new LeaksTrackingByteBufAllocator(allocator); + return new LeaksTrackingByteBufAllocator(allocator, Duration.ZERO, ""); + } + + /** + * Allows to instrument any given the instance of ByteBufAllocator + * + * @param allocator + * @return + */ + public static LeaksTrackingByteBufAllocator instrument( + ByteBufAllocator allocator, Duration awaitZeroRefCntDuration, String tag) { + return new LeaksTrackingByteBufAllocator(allocator, awaitZeroRefCntDuration, tag); } final ConcurrentLinkedQueue tracker = new ConcurrentLinkedQueue<>(); final ByteBufAllocator delegate; - private LeaksTrackingByteBufAllocator(ByteBufAllocator delegate) { + final Duration awaitZeroRefCntDuration; + + final String tag; + + private LeaksTrackingByteBufAllocator( + ByteBufAllocator delegate, Duration awaitZeroRefCntDuration, String tag) { this.delegate = delegate; + this.awaitZeroRefCntDuration = awaitZeroRefCntDuration; + this.tag = tag; } public LeaksTrackingByteBufAllocator assertHasNoLeaks() { try { - Assertions.assertThat(tracker) - .allSatisfy( - buf -> - Assertions.assertThat(buf) - .matches(bb -> bb.refCnt() == 0, "buffer should be released")); + ArrayList unreleased = new ArrayList<>(); + for (ByteBuf bb : tracker) { + if (bb.refCnt() != 0) { + unreleased.add(bb); + } + } + + final Duration awaitZeroRefCntDuration = this.awaitZeroRefCntDuration; + if (!unreleased.isEmpty() && !awaitZeroRefCntDuration.isZero()) { + final long startTime = System.currentTimeMillis(); + final long endTimeInMillis = startTime + awaitZeroRefCntDuration.toMillis(); + boolean hasUnreleased; + while (System.currentTimeMillis() <= endTimeInMillis) { + hasUnreleased = false; + for (ByteBuf bb : unreleased) { + if (bb.refCnt() != 0) { + hasUnreleased = true; + break; + } + } + + if (!hasUnreleased) { + System.out.println(tag + " all the buffers are released..."); + return this; + } + + System.out.println(tag + " await buffers to be released"); + for (int i = 0; i < 100; i++) { + System.gc(); + parkNanos(1000); + System.gc(); + } + } + } + + 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); + System.out.println(tag + " all the buffers are released..."); } finally { tracker.clear(); } @@ -150,4 +223,60 @@ T track(T buffer) { return buffer; } + + static final Class simpleLeakAwareCompositeByteBufClass; + static final Field leakFieldForComposite; + static final Class simpleLeakAwareByteBufClass; + static final Field leakFieldForNormal; + static final Field allLeaksField; + + static { + try { + { + final Class aClass = Class.forName("io.netty.buffer.SimpleLeakAwareCompositeByteBuf"); + final Field leakField = aClass.getDeclaredField("leak"); + + leakField.setAccessible(true); + + simpleLeakAwareCompositeByteBufClass = aClass; + leakFieldForComposite = leakField; + } + + { + final Class aClass = Class.forName("io.netty.buffer.SimpleLeakAwareByteBuf"); + final Field leakField = aClass.getDeclaredField("leak"); + + leakField.setAccessible(true); + + simpleLeakAwareByteBufClass = aClass; + leakFieldForNormal = leakField; + } + + { + final Class aClass = + Class.forName("io.netty.util.ResourceLeakDetector$DefaultResourceLeak"); + final Field field = aClass.getDeclaredField("allLeaks"); + + field.setAccessible(true); + + allLeaksField = field; + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + static Set resolveTrackingInfo(ByteBuf byteBuf) throws Exception { + if (ResourceLeakDetector.getLevel().ordinal() + >= ResourceLeakDetector.Level.ADVANCED.ordinal()) { + if (simpleLeakAwareCompositeByteBufClass.isInstance(byteBuf)) { + return (Set) allLeaksField.get(leakFieldForComposite.get(byteBuf)); + } else if (simpleLeakAwareByteBufClass.isInstance(byteBuf)) { + return (Set) allLeaksField.get(leakFieldForNormal.get(byteBuf)); + } + } + + return Collections.emptySet(); + } } 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 9f431d0d4..53323a26e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java +++ b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java @@ -23,6 +23,7 @@ import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestSubscriber; +import java.time.Duration; import org.junit.rules.ExternalResource; import org.junit.runner.Description; import org.junit.runners.model.Statement; @@ -42,7 +43,9 @@ public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { - allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + allocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(5), ""); connectSub = TestSubscriber.create(); init(); base.evaluate(); From 76955f065c772a738f86c213578f9c401802accc Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 24 Oct 2020 15:47:54 +0300 Subject: [PATCH 045/183] minor fixes in weighted algorithm formula Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/loadbalance/BaseWeightedStats.java | 10 ++-------- .../loadbalance/WeightedLoadbalanceStrategy.java | 3 ++- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java index 6514244c3..bdd63af3f 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java @@ -33,8 +33,6 @@ public class BaseWeightedStats implements WeightedStats { private long stamp0; // last timestamp we sent a request or receive a response private long duration; // instantaneous cumulative duration - private double availability = 1.0; - private volatile int pendingRequests; // instantaneous rate private static final AtomicIntegerFieldUpdater PENDING_REQUESTS = AtomicIntegerFieldUpdater.newUpdater(BaseWeightedStats.class, "pendingRequests"); @@ -87,7 +85,7 @@ public double availability() { if (Clock.now() - stamp > tau) { updateAvailability(1.0); } - return availability * availabilityPercentage.value(); + return availabilityPercentage.value(); } @Override @@ -181,10 +179,6 @@ void updateAvailability(double value) { } } - void setAvailability(double availability) { - this.availability = availability; - } - @Override public String toString() { return "Stats{" @@ -215,7 +209,7 @@ public String toString() { + ", pendingStreams=" + pendingStreams + ", availability=" - + availability + + availabilityPercentage.value() + '}'; } } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java index cdce957aa..0b6f3b785 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java @@ -159,7 +159,8 @@ private static double algorithmicWeight( latency *= calculateFactor(latency, high, bandWidth); } - return rSocket.availability() / (1.0d + latency * (pending + 1)); + return (rSocket.availability() * weightedStats.availability()) + / (1.0d + latency * (pending + 1)); } private static double calculateFactor(final double u, final double l, final double bandWidth) { From 8a41b419db55afa2f41f13bd3a271aa2de5573ec Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sun, 25 Oct 2020 12:31:32 +0200 Subject: [PATCH 046/183] fixes sink result check condition Signed-off-by: Oleh Dokuka --- .../main/java/io/rsocket/resume/ResumableDuplexConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0b064eb84..dbf77b902 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -115,7 +115,7 @@ void initConnection(DuplexConnection nextConnection) { frameReceivingSubscriber.dispose(); disposable.dispose(); Sinks.EmitResult result = onConnectionClosedSink.tryEmitNext(currentConnectionIndex); - if (result.equals(Sinks.EmitResult.OK)) { + if (!result.equals(Sinks.EmitResult.OK)) { logger.error("Failed to notify session of closed connection: {}", result); } }) From e57621c5e316f8e899295ba40102fa20fee400a3 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 26 Oct 2020 11:35:53 +0200 Subject: [PATCH 047/183] improves WeightedStats API Signed-off-by: Oleh Dokuka --- .../main/java/io/rsocket/loadbalance/BaseWeightedStats.java | 2 +- .../io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java | 2 +- .../src/main/java/io/rsocket/loadbalance/WeightedStats.java | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java index bdd63af3f..bd427da8a 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/BaseWeightedStats.java @@ -81,7 +81,7 @@ public int pending() { } @Override - public double availability() { + public double weightedAvailability() { if (Clock.now() - stamp > tau) { updateAvailability(1.0); } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java index 0b6f3b785..261a9562d 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java @@ -159,7 +159,7 @@ private static double algorithmicWeight( latency *= calculateFactor(latency, high, bandWidth); } - return (rSocket.availability() * weightedStats.availability()) + return (rSocket.availability() * weightedStats.weightedAvailability()) / (1.0d + latency * (pending + 1)); } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java index b0cf02560..7f2891bba 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedStats.java @@ -1,13 +1,11 @@ package io.rsocket.loadbalance; -import io.rsocket.Availability; - /** * Representation of stats used by the {@link WeightedLoadbalanceStrategy} * * @since 1.1 */ -public interface WeightedStats extends Availability { +public interface WeightedStats { double higherQuantileLatency(); @@ -16,4 +14,6 @@ public interface WeightedStats extends Availability { int pending(); double predictedLatency(); + + double weightedAvailability(); } From be1207b61688fdb3012f55016e5bd04f74cd3b9e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 26 Oct 2020 11:18:31 +0000 Subject: [PATCH 048/183] Refactor WeightedLoadbalanceStrategy to use a Builder Signed-off-by: Rossen Stoyanchev --- .../loadbalance/LoadbalanceRSocketClient.java | 2 +- .../WeightedLoadbalanceStrategy.java | 105 ++++++++++++------ 2 files changed, 70 insertions(+), 37 deletions(-) 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 8822632a0..4a1625e8a 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java @@ -149,7 +149,7 @@ public Builder roundRobinLoadbalanceStrategy() { *

    By default, {@link RoundRobinLoadbalanceStrategy} is used. */ public Builder weightedLoadbalanceStrategy() { - this.loadbalanceStrategy = new WeightedLoadbalanceStrategy(); + this.loadbalanceStrategy = WeightedLoadbalanceStrategy.create(); return this; } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java index 261a9562d..e24953cba 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java @@ -20,9 +20,8 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.plugins.RequestInterceptor; import java.util.List; -import java.util.SplittableRandom; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ThreadLocalRandom; import java.util.function.Function; import reactor.util.annotation.Nullable; @@ -37,32 +36,13 @@ public class WeightedLoadbalanceStrategy implements ClientLoadbalanceStrategy { private static final double EXP_FACTOR = 4.0; - private static final int EFFORT = 5; - - final int effort; - final SplittableRandom splittableRandom; + final int maxPairSelectionAttempts; final Function weightedStatsResolver; - public WeightedLoadbalanceStrategy() { - this(new DefaultWeightedStatsResolver()); - } - - public WeightedLoadbalanceStrategy(Function weightedStatsResolver) { - this(EFFORT, weightedStatsResolver); - } - - public WeightedLoadbalanceStrategy( - int effort, Function weightedStatsResolver) { - this(effort, new SplittableRandom(System.nanoTime()), weightedStatsResolver); - } - - public WeightedLoadbalanceStrategy( - int effort, - SplittableRandom splittableRandom, - Function weightedStatsResolver) { - this.effort = effort; - this.splittableRandom = splittableRandom; - this.weightedStatsResolver = weightedStatsResolver; + private WeightedLoadbalanceStrategy( + int numberOfAttempts, @Nullable Function resolver) { + this.maxPairSelectionAttempts = numberOfAttempts; + this.weightedStatsResolver = (resolver != null ? resolver : new DefaultWeightedStatsResolver()); } @Override @@ -75,7 +55,7 @@ public void initialize(RSocketConnector connector) { @Override public RSocket select(List sockets) { - final int effort = this.effort; + final int numberOfAttepmts = this.maxPairSelectionAttempts; final int size = sockets.size(); RSocket weightedRSocket; @@ -103,7 +83,7 @@ public RSocket select(List sockets) { RSocket rsc1 = null; RSocket rsc2 = null; - for (int i = 0; i < effort; i++) { + for (int i = 0; i < numberOfAttepmts; i++) { int i1 = ThreadLocalRandom.current().nextInt(size); int i2 = ThreadLocalRandom.current().nextInt(size - 1); @@ -168,30 +148,83 @@ private static double calculateFactor(final double u, final double l, final doub return Math.pow(1 + alpha, EXP_FACTOR); } - static class DefaultWeightedStatsResolver implements Function { + /** Create an instance of {@link WeightedLoadbalanceStrategy} with default settings. */ + public static WeightedLoadbalanceStrategy create() { + return new Builder().build(); + } + + /** Return a builder to create a {@link WeightedLoadbalanceStrategy} with. */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for {@link WeightedLoadbalanceStrategy}. */ + public static class Builder { + + private int maxPairSelectionAttempts = 5; + + @Nullable private Function weightedStatsResolver; + + private Builder() {} + + /** + * How many times to try to randomly select a pair of RSocket connections with non-zero + * availability. This is applicable when there are more than two connections in the pool. If the + * number of attempts is exceeded, the last selected pair is used. + * + *

    By default this is set to 5. + * + * @param numberOfAttempts the iteration count + */ + public Builder maxPairSelectionAttempts(int numberOfAttempts) { + this.maxPairSelectionAttempts = numberOfAttempts; + return this; + } + + /** + * Configure how the created {@link WeightedLoadbalanceStrategy} should find the stats for a + * given RSocket. + * + *

    By default {@code WeightedLoadbalanceStrategy} installs a {@code RequestInterceptor} when + * {@link ClientLoadbalanceStrategy#initialize(RSocketConnector)} is called in order to keep + * track of stats. + * + * @param resolver the function to find the stats for an RSocket + */ + public Builder weightedStatsResolver(Function resolver) { + this.weightedStatsResolver = resolver; + return this; + } + + public WeightedLoadbalanceStrategy build() { + return new WeightedLoadbalanceStrategy( + this.maxPairSelectionAttempts, this.weightedStatsResolver); + } + } + + private static class DefaultWeightedStatsResolver implements Function { - final ConcurrentMap rsocketsInterceptors = - new ConcurrentHashMap<>(); + final Map statsMap = new ConcurrentHashMap<>(); @Override public WeightedStats apply(RSocket rSocket) { - return rsocketsInterceptors.get(rSocket); + return statsMap.get(rSocket); } void init(RSocketConnector connector) { connector.interceptors( - ir -> - ir.forRequester( + registry -> + registry.forRequester( (Function) rSocket -> { final WeightedStatsRequestInterceptor interceptor = new WeightedStatsRequestInterceptor() { @Override public void dispose() { - rsocketsInterceptors.remove(rSocket); + statsMap.remove(rSocket); } }; - rsocketsInterceptors.put(rSocket, interceptor); + statsMap.put(rSocket, interceptor); return interceptor; })); From e08ff71711ff9a5e31ab768968c29bb4c9862502 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 26 Oct 2020 11:47:19 +0000 Subject: [PATCH 049/183] Rename RequestInterceptor registration methods Signed-off-by: Rossen Stoyanchev --- .../loadbalance/WeightedLoadbalanceStrategy.java | 2 +- .../plugins/InitializingInterceptorRegistry.java | 6 ++++-- .../io/rsocket/plugins/InterceptorRegistry.java | 8 ++++---- .../rsocket/plugins/RequestInterceptorTest.java | 16 ++++++++-------- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java index e24953cba..682a808bf 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategy.java @@ -214,7 +214,7 @@ public WeightedStats apply(RSocket rSocket) { void init(RSocketConnector connector) { connector.interceptors( registry -> - registry.forRequester( + registry.forRequestsInRequester( (Function) rSocket -> { final WeightedStatsRequestInterceptor interceptor = diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java b/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java index be0d8278f..2c53fb6b2 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java @@ -28,12 +28,14 @@ public class InitializingInterceptorRegistry extends InterceptorRegistry { @Nullable public RequestInterceptor initRequesterRequestInterceptor(RSocket rSocketRequester) { - return CompositeRequestInterceptor.create(rSocketRequester, getRequesterRequestInterceptors()); + return CompositeRequestInterceptor.create( + rSocketRequester, getRequestInterceptorsForRequester()); } @Nullable public RequestInterceptor initResponderRequestInterceptor(RSocket rSocketResponder) { - return CompositeRequestInterceptor.create(rSocketResponder, getResponderRequestInterceptors()); + return CompositeRequestInterceptor.create( + rSocketResponder, getRequestInterceptorsForResponder()); } public DuplexConnection initConnection( diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java b/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java index 0ccc4cb92..680fb514f 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java @@ -48,7 +48,7 @@ public class InterceptorRegistry { * RequestInterceptor} * @since 1.1 */ - public InterceptorRegistry forRequester( + public InterceptorRegistry forRequestsInRequester( Function interceptor) { requesterRequestInterceptors.add(interceptor); return this; @@ -61,7 +61,7 @@ public InterceptorRegistry forRequester( * RequestInterceptor} * @since 1.1 */ - public InterceptorRegistry forResponder( + public InterceptorRegistry forRequestsInResponder( Function interceptor) { responderRequestInterceptors.add(interceptor); return this; @@ -134,11 +134,11 @@ public InterceptorRegistry forConnection(Consumer> getRequesterRequestInterceptors() { + List> getRequestInterceptorsForRequester() { return requesterRequestInterceptors; } - List> getResponderRequestInterceptors() { + List> getRequestInterceptorsForResponder() { return responderRequestInterceptors; } 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 6f156a380..2bb718ef7 100644 --- a/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java @@ -66,7 +66,7 @@ public Flux requestChannel(Publisher payloads) { RSocketConnector.create() .interceptors( ir -> - ir.forRequester( + ir.forRequestsInRequester( (Function) (__) -> testRequestInterceptor)) .connect(LocalClientTransport.create("test")) @@ -206,7 +206,7 @@ public Flux requestChannel(Publisher payloads) { })) .interceptors( ir -> - ir.forResponder( + ir.forRequestsInResponder( (Function) (__) -> testRequestInterceptor)) .connect(LocalClientTransport.create("test")) @@ -292,7 +292,7 @@ public Flux requestChannel(Publisher payloads) { })) .interceptors( ir -> - ir.forResponder( + ir.forRequestsInResponder( (Function) (__) -> testRequestInterceptor)) .bindNow(LocalServerTransport.create("test")); @@ -400,7 +400,7 @@ void interceptorShouldBeInstalledProperlyOnTheServerResponderSide(boolean errorO })) .interceptors( ir -> - ir.forRequester( + ir.forRequestsInRequester( (Function) (__) -> testRequestInterceptor)) .bindNow(LocalServerTransport.create("test")); @@ -543,7 +543,7 @@ public void dispose() {} RSocketConnector.create() .interceptors( ir -> - ir.forRequester( + ir.forRequestsInRequester( (Function) (__) -> testRequestInterceptor)) .connect(LocalClientTransport.create("test")) @@ -646,13 +646,13 @@ public void dispose() {} RSocketConnector.create() .interceptors( ir -> - ir.forRequester( + ir.forRequestsInRequester( (Function) (__) -> testRequestInterceptor) - .forRequester( + .forRequestsInRequester( (Function) (__) -> testRequestInterceptor1) - .forRequester( + .forRequestsInRequester( (Function) (__) -> testRequestInterceptor2)) .connect(LocalClientTransport.create("test")) From d047c59809dd65db86ebd3d76924e692b402c6e5 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 26 Oct 2020 16:14:49 +0000 Subject: [PATCH 050/183] Upgrade to Dysprosium-SR13 Closes gh-951 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b5910fb86..9b4dcd2f4 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = 'Dysprosium-SR12' + ext['reactor-bom.version'] = 'Dysprosium-SR13' ext['logback.version'] = '1.2.3' ext['netty-bom.version'] = '4.1.52.Final' ext['netty-boringssl.version'] = '2.0.34.Final' From 51eb48e976437da213b45d76bc1d5ae8932dc537 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 27 Oct 2020 12:15:14 +0000 Subject: [PATCH 051/183] Upgrade to Reactor 2020.0.0 Closes gh-952 Signed-off-by: Rossen Stoyanchev --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 15850ffe1..50fe129e2 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = '2020.0.0-RC2' + ext['reactor-bom.version'] = '2020.0.0' ext['logback.version'] = '1.2.3' ext['netty-bom.version'] = '4.1.52.Final' ext['netty-boringssl.version'] = '2.0.34.Final' From 7950c275be68fd511e3444ee07a1b00202e490e9 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 27 Oct 2020 13:29:14 +0200 Subject: [PATCH 052/183] fixes LoadBalancedRSocketMono to propagate context Signed-off-by: Oleh Dokuka --- .../client/LoadBalancedRSocketMono.java | 78 ++++++++++++------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java index c7f64674c..6329da826 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java @@ -34,7 +34,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,6 +41,8 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Operators; +import reactor.util.context.Context; import reactor.util.retry.Retry; /** @@ -667,26 +668,28 @@ private class WeightedSocket implements LoadBalancerSocketMetrics, RSocket { @Override public Mono requestResponse(Payload payload) { return rSocketMono.flatMap( - source -> { - return Mono.from( - subscriber -> - source - .requestResponse(payload) - .subscribe(new LatencySubscriber<>(subscriber, this))); - }); + source -> + Mono.from( + subscriber -> + source + .requestResponse(payload) + .subscribe( + new LatencySubscriber<>( + Operators.toCoreSubscriber(subscriber), this)))); } @Override public Flux requestStream(Payload payload) { return rSocketMono.flatMapMany( - source -> { - return Flux.from( - subscriber -> - source - .requestStream(payload) - .subscribe(new CountingSubscriber<>(subscriber, this))); - }); + source -> + Flux.from( + subscriber -> + source + .requestStream(payload) + .subscribe( + new CountingSubscriber<>( + Operators.toCoreSubscriber(subscriber), this)))); } @Override @@ -698,7 +701,9 @@ public Mono fireAndForget(Payload payload) { subscriber -> source .fireAndForget(payload) - .subscribe(new CountingSubscriber<>(subscriber, this))); + .subscribe( + new CountingSubscriber<>( + Operators.toCoreSubscriber(subscriber), this))); }); } @@ -710,7 +715,9 @@ public Mono metadataPush(Payload payload) { subscriber -> source .metadataPush(payload) - .subscribe(new CountingSubscriber<>(subscriber, this))); + .subscribe( + new CountingSubscriber<>( + Operators.toCoreSubscriber(subscriber), this))); }); } @@ -718,13 +725,14 @@ public Mono metadataPush(Payload payload) { public Flux requestChannel(Publisher payloads) { return rSocketMono.flatMapMany( - source -> { - return Flux.from( - subscriber -> - source - .requestChannel(payloads) - .subscribe(new CountingSubscriber<>(subscriber, this))); - }); + source -> + Flux.from( + subscriber -> + source + .requestChannel(payloads) + .subscribe( + new CountingSubscriber<>( + Operators.toCoreSubscriber(subscriber), this)))); } synchronized double getPredictedLatency() { @@ -867,18 +875,23 @@ public long lastTimeUsedMillis() { * Subscriber wrapper used for request/response interaction model, measure and collect latency * information. */ - private class LatencySubscriber implements Subscriber { - private final Subscriber child; + private class LatencySubscriber implements CoreSubscriber { + private final CoreSubscriber child; private final WeightedSocket socket; private final AtomicBoolean done; private long start; - LatencySubscriber(Subscriber child, WeightedSocket socket) { + LatencySubscriber(CoreSubscriber child, WeightedSocket socket) { this.child = child; this.socket = socket; this.done = new AtomicBoolean(false); } + @Override + public Context currentContext() { + return child.currentContext(); + } + @Override public void onSubscribe(Subscription s) { start = incr(); @@ -931,15 +944,20 @@ public void onComplete() { * Subscriber wrapper used for stream like interaction model, it only counts the number of * active streams */ - private class CountingSubscriber implements Subscriber { - private final Subscriber child; + private class CountingSubscriber implements CoreSubscriber { + private final CoreSubscriber child; private final WeightedSocket socket; - CountingSubscriber(Subscriber child, WeightedSocket socket) { + CountingSubscriber(CoreSubscriber child, WeightedSocket socket) { this.child = child; this.socket = socket; } + @Override + public Context currentContext() { + return child.currentContext(); + } + @Override public void onSubscribe(Subscription s) { socket.pendingStreams.incrementAndGet(); From fe95709d72139b5ce49eaea5da0d10a3e909e0d5 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 27 Oct 2020 13:30:20 +0200 Subject: [PATCH 053/183] improves loadbalance test coverage and provides fixes Signed-off-by: Oleh Dokuka --- .../loadbalance/MonoDeferredResolution.java | 8 +- .../io/rsocket/loadbalance/PooledRSocket.java | 51 ++- .../io/rsocket/loadbalance/RSocketPool.java | 376 ++++++++++-------- .../rsocket/loadbalance/LoadbalanceTest.java | 313 +++++++++++++++ .../RoundRobinLoadbalanceStrategyTest.java | 154 +++++++ 5 files changed, 721 insertions(+), 181 deletions(-) create mode 100644 rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategyTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/MonoDeferredResolution.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/MonoDeferredResolution.java index b37ec4b47..69838f1b6 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/MonoDeferredResolution.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/MonoDeferredResolution.java @@ -59,7 +59,7 @@ abstract class MonoDeferredResolution extends Mono } @Override - public void subscribe(CoreSubscriber actual) { + public final void subscribe(CoreSubscriber actual) { if (this.requested == STATE_UNSUBSCRIBED && REQUESTED.compareAndSet(this, STATE_UNSUBSCRIBED, STATE_SUBSCRIBER_SET)) { @@ -145,7 +145,7 @@ public final void onNext(RESULT payload) { } @Override - public void onError(Throwable t) { + public final void onError(Throwable t) { if (this.done) { Operators.onErrorDropped(t, this.actual.currentContext()); return; @@ -156,7 +156,7 @@ public void onError(Throwable t) { } @Override - public void onComplete() { + public final void onComplete() { if (this.done) { return; } @@ -206,7 +206,7 @@ public final void request(long n) { } } - public void cancel() { + public final void cancel() { long state = REQUESTED.getAndSet(this, STATE_TERMINATED); if (state == STATE_TERMINATED) { return; diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java index 3d9011bf6..44a9334d3 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java @@ -108,22 +108,16 @@ protected void doSubscribe() { @Override protected void doOnValueResolved(RSocket value) { - value.onClose().subscribe(null, t -> this.invalidate(), this::invalidate); + value.onClose().subscribe(null, t -> this.doCleanup(), this::doCleanup); } - @Override - protected void doOnValueExpired(RSocket value) { - value.dispose(); - this.dispose(); - } + void doCleanup() { + if (isDisposed()) { + return; + } - @Override - public void dispose() { - super.dispose(); - } + this.dispose(); - @Override - protected void doOnDispose() { final RSocketPool parent = this.parent; for (; ; ) { final PooledRSocket[] sockets = parent.activeSockets; @@ -141,20 +135,35 @@ protected void doOnDispose() { break; } - final int lastIndex = activeSocketsCount - 1; - final PooledRSocket[] newSockets = new PooledRSocket[lastIndex]; - if (index != 0) { - System.arraycopy(sockets, 0, newSockets, 0, index); - } + final PooledRSocket[] newSockets; + if (activeSocketsCount == 1) { + newSockets = RSocketPool.EMPTY; + } else { + final int lastIndex = activeSocketsCount - 1; + + newSockets = new PooledRSocket[lastIndex]; + if (index != 0) { + System.arraycopy(sockets, 0, newSockets, 0, index); + } - if (index != lastIndex) { - System.arraycopy(sockets, index + 1, newSockets, index, lastIndex - index); + if (index != lastIndex) { + System.arraycopy(sockets, index + 1, newSockets, index, lastIndex - index); + } } if (RSocketPool.ACTIVE_SOCKETS.compareAndSet(parent, sockets, newSockets)) { break; } } + } + + @Override + protected void doOnValueExpired(RSocket value) { + value.dispose(); + } + + @Override + protected void doOnDispose() { Operators.terminate(S, this); } @@ -231,7 +240,7 @@ public void accept(RSocket rSocket, Throwable t) { source.subscribe((CoreSubscriber) this); } else { - parent.add(this); + parent.observe(this); } } } @@ -273,7 +282,7 @@ public void accept(RSocket rSocket, Throwable t) { source.subscribe(this); } else { - parent.add(this); + parent.observe(this); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java index 733b06f3c..514a5d3f4 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java @@ -36,8 +36,8 @@ import reactor.core.publisher.Operators; import reactor.util.annotation.Nullable; -class RSocketPool extends ResolvingOperator - implements CoreSubscriber>, List { +class RSocketPool extends ResolvingOperator + implements CoreSubscriber> { final DeferredResolutionRSocket deferredResolutionRSocket = new DeferredResolutionRSocket(this); final RSocketConnector connector; @@ -100,6 +100,7 @@ public void onNext(List targets) { PooledRSocket[] previouslyActiveSockets; PooledRSocket[] activeSockets; + PooledRSocket[] inactiveSockets; for (; ; ) { HashMap rSocketSuppliersCopy = new HashMap<>(); @@ -110,9 +111,11 @@ public void onNext(List targets) { // checking intersection of active RSocket with the newly received set previouslyActiveSockets = this.activeSockets; + inactiveSockets = new PooledRSocket[previouslyActiveSockets.length]; PooledRSocket[] nextActiveSockets = new PooledRSocket[previouslyActiveSockets.length + rSocketSuppliersCopy.size()]; - int position = 0; + int activeSocketsPosition = 0; + int inactiveSocketsPosition = 0; for (int i = 0; i < previouslyActiveSockets.length; i++) { PooledRSocket rSocket = previouslyActiveSockets[i]; @@ -121,18 +124,18 @@ public void onNext(List targets) { // if one of the active rSockets is not included, we remove it and put in the // pending removal if (!rSocket.isDisposed()) { - rSocket.dispose(); + inactiveSockets[inactiveSocketsPosition++] = rSocket; // TODO: provide a meaningful algo for keeping removed rsocket in the list // nextActiveSockets[position++] = rSocket; } } else { if (!rSocket.isDisposed()) { // keep old RSocket instance - nextActiveSockets[position++] = rSocket; + nextActiveSockets[activeSocketsPosition++] = rSocket; } else { // put newly create RSocket instance LoadbalanceTarget target = targets.get(index); - nextActiveSockets[position++] = + nextActiveSockets[activeSocketsPosition++] = new PooledRSocket(this, this.connector.connect(target.getTransport()), target); } } @@ -140,15 +143,15 @@ public void onNext(List targets) { // going though brightly new rsocket for (LoadbalanceTarget target : rSocketSuppliersCopy.keySet()) { - nextActiveSockets[position++] = + nextActiveSockets[activeSocketsPosition++] = new PooledRSocket(this, this.connector.connect(target.getTransport()), target); } // shrank to actual length - if (position == 0) { + if (activeSocketsPosition == 0) { activeSockets = EMPTY; } else { - activeSockets = Arrays.copyOf(nextActiveSockets, position); + activeSockets = Arrays.copyOf(nextActiveSockets, activeSocketsPosition); } if (ACTIVE_SOCKETS.compareAndSet(this, previouslyActiveSockets, activeSockets)) { @@ -156,11 +159,19 @@ public void onNext(List targets) { } } + for (PooledRSocket inactiveSocket : inactiveSockets) { + if (inactiveSocket == null) { + break; + } + + inactiveSocket.dispose(); + } + if (isPending()) { // notifies that upstream is resolved if (activeSockets != EMPTY) { //noinspection ConstantConditions - complete(null); + complete(this); } } } @@ -191,6 +202,13 @@ RSocket select() { terminate(new CancellationException("Pool is exhausted")); } else { invalidate(); + + // check since it is possible that between doSelect() and invalidate() we might + // have received new sockets + selected = doSelect(); + if (selected != null) { + return selected; + } } return this.deferredResolutionRSocket; } @@ -201,44 +219,12 @@ RSocket select() { @Nullable RSocket doSelect() { PooledRSocket[] sockets = this.activeSockets; - if (sockets == EMPTY) { - return null; - } - return this.loadbalanceStrategy.select(this); - } - - @Override - public RSocket get(int index) { - final PooledRSocket socket = activeSockets[index]; - final RSocket realValue = socket.valueIfResolved(); - - if (realValue != null) { - return realValue; + if (sockets == EMPTY || sockets == TERMINATED) { + return null; } - return socket; - } - - @Override - public int size() { - return activeSockets.length; - } - - @Override - public boolean isEmpty() { - return activeSockets.length == 0; - } - - @Override - public Object[] toArray() { - return activeSockets; - } - - @Override - @SuppressWarnings("unchecked") - public T[] toArray(T[] a) { - return (T[]) activeSockets; + return this.loadbalanceStrategy.select(WrappingList.wrap(sockets)); } static class DeferredResolutionRSocket implements RSocket { @@ -266,7 +252,7 @@ public Flux requestStream(Payload payload) { @Override public Flux requestChannel(Publisher payloads) { - return new FluxInner<>(this.parent, payloads, FrameType.REQUEST_STREAM); + return new FluxInner<>(this.parent, payloads, FrameType.REQUEST_CHANNEL); } @Override @@ -275,7 +261,7 @@ public Mono metadataPush(Payload payload) { } } - static final class MonoInner extends MonoDeferredResolution { + static final class MonoInner extends MonoDeferredResolution { MonoInner(RSocketPool parent, Payload payload, FrameType requestType) { super(parent, payload, requestType); @@ -283,7 +269,7 @@ static final class MonoInner extends MonoDeferredResolution { @Override @SuppressWarnings({"unchecked", "rawtypes"}) - public void accept(Void aVoid, Throwable t) { + public void accept(Object aVoid, Throwable t) { if (isTerminated()) { return; } @@ -295,32 +281,47 @@ public void accept(Void aVoid, Throwable t) { } RSocketPool parent = (RSocketPool) this.parent; - RSocket rSocket = parent.doSelect(); - if (rSocket != null) { - Mono source; - switch (this.requestType) { - case REQUEST_FNF: - source = rSocket.fireAndForget(this.payload); - break; - case REQUEST_RESPONSE: - source = rSocket.requestResponse(this.payload); - break; - case METADATA_PUSH: - source = rSocket.metadataPush(this.payload); - break; - default: - Operators.error(this.actual, new IllegalStateException("Should never happen")); - return; + for (; ; ) { + RSocket rSocket = parent.doSelect(); + if (rSocket != null) { + Mono source; + switch (this.requestType) { + case REQUEST_FNF: + source = rSocket.fireAndForget(this.payload); + break; + case REQUEST_RESPONSE: + source = rSocket.requestResponse(this.payload); + break; + case METADATA_PUSH: + source = rSocket.metadataPush(this.payload); + break; + default: + Operators.error(this.actual, new IllegalStateException("Should never happen")); + return; + } + + source.subscribe((CoreSubscriber) this); + + return; } - source.subscribe((CoreSubscriber) this); - } else { - parent.add(this); + final int state = parent.add(this); + + if (state == ADDED_STATE) { + return; + } + + if (state == TERMINATED_STATE) { + final Throwable error = parent.t; + ReferenceCountUtil.safeRelease(this.payload); + onError(error); + return; + } } } } - static final class FluxInner extends FluxDeferredResolution { + static final class FluxInner extends FluxDeferredResolution { FluxInner(RSocketPool parent, INPUT fluxOrPayload, FrameType requestType) { super(parent, fluxOrPayload, requestType); @@ -328,7 +329,7 @@ static final class FluxInner extends FluxDeferredResolution @Override @SuppressWarnings("unchecked") - public void accept(Void aVoid, Throwable t) { + public void accept(Object aVoid, Throwable t) { if (isTerminated()) { return; } @@ -342,115 +343,178 @@ public void accept(Void aVoid, Throwable t) { } RSocketPool parent = (RSocketPool) this.parent; - RSocket rSocket = parent.doSelect(); - if (rSocket != null) { - Flux source; - switch (this.requestType) { - case REQUEST_STREAM: - source = rSocket.requestStream((Payload) this.fluxOrPayload); - break; - case REQUEST_CHANNEL: - source = rSocket.requestChannel((Flux) this.fluxOrPayload); - break; - default: - Operators.error(this.actual, new IllegalStateException("Should never happen")); - return; + for (; ; ) { + RSocket rSocket = parent.doSelect(); + if (rSocket != null) { + Flux source; + switch (this.requestType) { + case REQUEST_STREAM: + source = rSocket.requestStream((Payload) this.fluxOrPayload); + break; + case REQUEST_CHANNEL: + source = rSocket.requestChannel((Flux) this.fluxOrPayload); + break; + default: + Operators.error(this.actual, new IllegalStateException("Should never happen")); + return; + } + + source.subscribe(this); + + return; } - source.subscribe(this); - } else { - parent.add(this); + final int state = parent.add(this); + + if (state == ADDED_STATE) { + return; + } + + if (state == TERMINATED_STATE) { + final Throwable error = parent.t; + if (this.requestType == FrameType.REQUEST_STREAM) { + ReferenceCountUtil.safeRelease(this.fluxOrPayload); + } + onError(error); + return; + } } } } - @Override - public boolean contains(Object o) { - throw new UnsupportedOperationException(); - } + static final class WrappingList implements List { - @Override - public Iterator iterator() { - throw new UnsupportedOperationException(); - } + static final ThreadLocal INSTANCE = ThreadLocal.withInitial(WrappingList::new); - @Override - public boolean add(RSocket weightedRSocket) { - throw new UnsupportedOperationException(); - } + private PooledRSocket[] activeSockets; - @Override - public boolean remove(Object o) { - throw new UnsupportedOperationException(); - } + static List wrap(PooledRSocket[] activeSockets) { + final WrappingList sockets = INSTANCE.get(); + sockets.activeSockets = activeSockets; + return sockets; + } - @Override - public boolean containsAll(Collection c) { - throw new UnsupportedOperationException(); - } + @Override + public RSocket get(int index) { + final PooledRSocket socket = activeSockets[index]; + final RSocket realValue = socket.valueIfResolved(); - @Override - public boolean addAll(Collection c) { - throw new UnsupportedOperationException(); - } + if (realValue != null) { + return realValue; + } - @Override - public boolean addAll(int index, Collection c) { - throw new UnsupportedOperationException(); - } + return socket; + } - @Override - public boolean removeAll(Collection c) { - throw new UnsupportedOperationException(); - } + @Override + public int size() { + return activeSockets.length; + } - @Override - public boolean retainAll(Collection c) { - throw new UnsupportedOperationException(); - } + @Override + public boolean isEmpty() { + return activeSockets.length == 0; + } - @Override - public void clear() { - throw new UnsupportedOperationException(); - } + @Override + public Object[] toArray() { + return activeSockets; + } - @Override - public RSocket set(int index, RSocket element) { - throw new UnsupportedOperationException(); - } + @Override + @SuppressWarnings("unchecked") + public T[] toArray(T[] a) { + return (T[]) activeSockets; + } - @Override - public void add(int index, RSocket element) { - throw new UnsupportedOperationException(); - } + @Override + public boolean contains(Object o) { + throw new UnsupportedOperationException(); + } - @Override - public RSocket remove(int index) { - throw new UnsupportedOperationException(); - } + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(); + } - @Override - public int indexOf(Object o) { - throw new UnsupportedOperationException(); - } + @Override + public boolean add(RSocket weightedRSocket) { + throw new UnsupportedOperationException(); + } - @Override - public int lastIndexOf(Object o) { - throw new UnsupportedOperationException(); - } + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } - @Override - public ListIterator listIterator() { - throw new UnsupportedOperationException(); - } + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException(); + } - @Override - public ListIterator listIterator(int index) { - throw new UnsupportedOperationException(); - } + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } - @Override - public List subList(int fromIndex, int toIndex) { - throw new UnsupportedOperationException(); + @Override + public boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public RSocket set(int index, RSocket element) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(int index, RSocket element) { + throw new UnsupportedOperationException(); + } + + @Override + public RSocket remove(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOf(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public int lastIndexOf(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public ListIterator listIterator() { + throw new UnsupportedOperationException(); + } + + @Override + public ListIterator listIterator(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public List subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } } } diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java new file mode 100644 index 000000000..966242ed3 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java @@ -0,0 +1,313 @@ +package io.rsocket.loadbalance; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.transport.ClientTransport; +import io.rsocket.util.EmptyPayload; +import io.rsocket.util.RSocketProxy; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; +import reactor.test.util.RaceTestUtils; +import reactor.util.context.Context; + +public class LoadbalanceTest { + + @Test + public void shouldDeliverAllTheRequestsWithRoundRobinStrategy() { + Hooks.onErrorDropped((__) -> {}); + + final AtomicInteger counter = new AtomicInteger(); + final ClientTransport mockTransport = Mockito.mock(ClientTransport.class); + final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + + Mockito.when(rSocketConnectorMock.connect(Mockito.any(ClientTransport.class))) + .then( + im -> + Mono.just( + new TestRSocket( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + counter.incrementAndGet(); + return Mono.empty(); + } + }))); + + for (int i = 0; i < 1000; i++) { + final TestPublisher> source = TestPublisher.create(); + final RSocketPool rSocketPool = + new RSocketPool(rSocketConnectorMock, source, new RoundRobinLoadbalanceStrategy()); + + RaceTestUtils.race( + () -> { + for (int j = 0; j < 1000; j++) { + Mono.defer(() -> rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE)) + .retry() + .subscribe(); + } + }, + () -> { + for (int j = 0; j < 100; j++) { + source.next(Collections.emptyList()); + source.next(Collections.singletonList(LoadbalanceTarget.from("1", mockTransport))); + source.next( + Arrays.asList( + LoadbalanceTarget.from("1", mockTransport), + LoadbalanceTarget.from("2", mockTransport))); + source.next(Collections.singletonList(LoadbalanceTarget.from("1", mockTransport))); + source.next(Collections.singletonList(LoadbalanceTarget.from("2", mockTransport))); + source.next(Collections.emptyList()); + source.next(Collections.singletonList(LoadbalanceTarget.from("2", mockTransport))); + } + }); + + Assertions.assertThat(counter.get()).isEqualTo(1000); + + counter.set(0); + } + } + + @Test + public void shouldDeliverAllTheRequestsWithWightedStrategy() { + Hooks.onErrorDropped((__) -> {}); + + final AtomicInteger counter = new AtomicInteger(); + final ClientTransport mockTransport = Mockito.mock(ClientTransport.class); + final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + + Mockito.when(rSocketConnectorMock.connect(Mockito.any(ClientTransport.class))) + .then(im -> Mono.just(new TestRSocket(new WeightedRSocket(counter)))); + + for (int i = 0; i < 1000; i++) { + final TestPublisher> source = TestPublisher.create(); + final RSocketPool rSocketPool = + new RSocketPool( + rSocketConnectorMock, + source, + WeightedLoadbalanceStrategy.builder() + .weightedStatsResolver(r -> (WeightedStats) r) + .build()); + + RaceTestUtils.race( + () -> { + for (int j = 0; j < 1000; j++) { + Mono.defer(() -> rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE)) + .retry() + .subscribe(); + } + }, + () -> { + for (int j = 0; j < 100; j++) { + source.next(Collections.emptyList()); + source.next(Collections.singletonList(LoadbalanceTarget.from("1", mockTransport))); + source.next( + Arrays.asList( + LoadbalanceTarget.from("1", mockTransport), + LoadbalanceTarget.from("2", mockTransport))); + source.next(Collections.singletonList(LoadbalanceTarget.from("1", mockTransport))); + source.next(Collections.singletonList(LoadbalanceTarget.from("2", mockTransport))); + source.next(Collections.emptyList()); + source.next(Collections.singletonList(LoadbalanceTarget.from("2", mockTransport))); + } + }); + + Assertions.assertThat(counter.get()).isEqualTo(1000); + + counter.set(0); + } + } + + @Test + public void ensureRSocketIsCleanedFromThePoolIfSourceRSocketIsDisposed() { + Hooks.onErrorDropped((__) -> {}); + + final AtomicInteger counter = new AtomicInteger(); + final ClientTransport mockTransport = Mockito.mock(ClientTransport.class); + final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + + final TestRSocket testRSocket = + new TestRSocket( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + counter.incrementAndGet(); + return Mono.empty(); + } + }); + + Mockito.when(rSocketConnectorMock.connect(Mockito.any(ClientTransport.class))) + .then(im -> Mono.delay(Duration.ofMillis(200)).map(__ -> testRSocket)); + + final TestPublisher> source = TestPublisher.create(); + final RSocketPool rSocketPool = + new RSocketPool(rSocketConnectorMock, source, new RoundRobinLoadbalanceStrategy()); + + source.next(Collections.singletonList(LoadbalanceTarget.from("1", mockTransport))); + + StepVerifier.create(rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE)) + .expectSubscription() + .expectComplete() + .verify(Duration.ofSeconds(2)); + + testRSocket.dispose(); + + Assertions.assertThatThrownBy( + () -> + rSocketPool + .select() + .fireAndForget(EmptyPayload.INSTANCE) + .block(Duration.ofSeconds(2))) + .isExactlyInstanceOf(IllegalStateException.class) + .hasMessage("Timeout on blocking read for 2000000000 NANOSECONDS"); + + Assertions.assertThat(counter.get()).isOne(); + } + + @Test + public void ensureContextIsPropagatedCorrectlyForRequestChannel() { + Hooks.onErrorDropped((__) -> {}); + + final AtomicInteger counter = new AtomicInteger(); + final ClientTransport mockTransport = Mockito.mock(ClientTransport.class); + final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + + Mockito.when(rSocketConnectorMock.connect(Mockito.any(ClientTransport.class))) + .then( + im -> + Mono.delay(Duration.ofMillis(200)) + .map( + __ -> + new TestRSocket( + new RSocket() { + @Override + public Flux requestChannel(Publisher source) { + counter.incrementAndGet(); + return Flux.from(source); + } + }))); + + final TestPublisher> source = TestPublisher.create(); + final RSocketPool rSocketPool = + new RSocketPool(rSocketConnectorMock, source, new RoundRobinLoadbalanceStrategy()); + + // check that context is propagated when there is no rsocket + StepVerifier.create( + rSocketPool + .select() + .requestChannel( + Flux.deferContextual( + cv -> { + if (cv.hasKey("test") && cv.get("test").equals("test")) { + return Flux.just(EmptyPayload.INSTANCE); + } else { + return Flux.error( + new IllegalStateException("Expected context to be propagated")); + } + })) + .contextWrite(Context.of("test", "test"))) + .expectSubscription() + .then( + () -> + source.next(Collections.singletonList(LoadbalanceTarget.from("1", mockTransport)))) + .expectNextCount(1) + .expectComplete() + .verify(Duration.ofSeconds(2)); + + source.next(Collections.singletonList(LoadbalanceTarget.from("2", mockTransport))); + // check that context is propagated when there is an RSocket but it is unresolved + StepVerifier.create( + rSocketPool + .select() + .requestChannel( + Flux.deferContextual( + cv -> { + if (cv.hasKey("test") && cv.get("test").equals("test")) { + return Flux.just(EmptyPayload.INSTANCE); + } else { + return Flux.error( + new IllegalStateException("Expected context to be propagated")); + } + })) + .contextWrite(Context.of("test", "test"))) + .expectSubscription() + .expectNextCount(1) + .expectComplete() + .verify(Duration.ofSeconds(2)); + + // check that context is propagated when there is an RSocket and it is resolved + StepVerifier.create( + rSocketPool + .select() + .requestChannel( + Flux.deferContextual( + cv -> { + if (cv.hasKey("test") && cv.get("test").equals("test")) { + return Flux.just(EmptyPayload.INSTANCE); + } else { + return Flux.error( + new IllegalStateException("Expected context to be propagated")); + } + })) + .contextWrite(Context.of("test", "test"))) + .expectSubscription() + .expectNextCount(1) + .expectComplete() + .verify(Duration.ofSeconds(2)); + + Assertions.assertThat(counter.get()).isEqualTo(3); + } + + static class TestRSocket extends RSocketProxy { + + final MonoProcessor processor = MonoProcessor.create(); + + public TestRSocket(RSocket rSocket) { + super(rSocket); + } + + @Override + public Mono onClose() { + return processor; + } + + @Override + public void dispose() { + processor.onComplete(); + } + } + + private static class WeightedRSocket extends BaseWeightedStats implements RSocket { + + private final AtomicInteger counter; + + public WeightedRSocket(AtomicInteger counter) { + this.counter = counter; + } + + @Override + public Mono fireAndForget(Payload payload) { + final long startTime = startRequest(); + counter.incrementAndGet(); + return Mono.empty() + .doFinally( + (__) -> { + final long stopTime = stopRequest(startTime); + record(stopTime - startTime); + }); + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategyTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategyTest.java new file mode 100644 index 000000000..c4fdcbab9 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategyTest.java @@ -0,0 +1,154 @@ +package io.rsocket.loadbalance; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.transport.ClientTransport; +import io.rsocket.util.EmptyPayload; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.assertj.core.api.Assertions; +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.test.publisher.TestPublisher; + +public class RoundRobinLoadbalanceStrategyTest { + + @Test + public void shouldDeliverValuesProportionally() { + Hooks.onErrorDropped((__) -> {}); + + final AtomicInteger counter1 = new AtomicInteger(); + final AtomicInteger counter2 = new AtomicInteger(); + final ClientTransport mockTransport = Mockito.mock(ClientTransport.class); + final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + + Mockito.when(rSocketConnectorMock.connect(Mockito.any(ClientTransport.class))) + .then( + im -> + Mono.just( + new LoadbalanceTest.TestRSocket( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + counter1.incrementAndGet(); + return Mono.empty(); + } + }))) + .then( + im -> + Mono.just( + new LoadbalanceTest.TestRSocket( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + counter2.incrementAndGet(); + return Mono.empty(); + } + }))); + + final TestPublisher> source = TestPublisher.create(); + final RSocketPool rSocketPool = + new RSocketPool(rSocketConnectorMock, source, new RoundRobinLoadbalanceStrategy()); + + for (int j = 0; j < 1000; j++) { + rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); + } + + source.next( + Arrays.asList( + LoadbalanceTarget.from("1", mockTransport), + LoadbalanceTarget.from("2", mockTransport))); + + Assertions.assertThat(counter1.get()).isCloseTo(500, Offset.offset(1)); + Assertions.assertThat(counter2.get()).isCloseTo(500, Offset.offset(1)); + } + + @Test + public void shouldDeliverValuesToNewlyConnectedSockets() { + Hooks.onErrorDropped((__) -> {}); + + final AtomicInteger counter1 = new AtomicInteger(); + final AtomicInteger counter2 = new AtomicInteger(); + final ClientTransport mockTransport1 = Mockito.mock(ClientTransport.class); + final ClientTransport mockTransport2 = Mockito.mock(ClientTransport.class); + final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + + Mockito.when(rSocketConnectorMock.connect(Mockito.any(ClientTransport.class))) + .then( + im -> + Mono.just( + new LoadbalanceTest.TestRSocket( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + if (im.getArgument(0) == mockTransport1) { + counter1.incrementAndGet(); + } else { + counter2.incrementAndGet(); + } + return Mono.empty(); + } + }))); + + final TestPublisher> source = TestPublisher.create(); + final RSocketPool rSocketPool = + new RSocketPool(rSocketConnectorMock, source, new RoundRobinLoadbalanceStrategy()); + + for (int j = 0; j < 1000; j++) { + rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); + } + + source.next(Arrays.asList(LoadbalanceTarget.from("1", mockTransport1))); + + Assertions.assertThat(counter1.get()).isCloseTo(1000, Offset.offset(1)); + + source.next(Collections.emptyList()); + + for (int j = 0; j < 1000; j++) { + rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); + } + + source.next(Arrays.asList(LoadbalanceTarget.from("1", mockTransport1))); + + Assertions.assertThat(counter1.get()).isCloseTo(2000, Offset.offset(1)); + + source.next( + Arrays.asList( + LoadbalanceTarget.from("1", mockTransport1), + LoadbalanceTarget.from("2", mockTransport2))); + + for (int j = 0; j < 1000; j++) { + rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); + } + + Assertions.assertThat(counter1.get()).isCloseTo(2500, Offset.offset(1)); + Assertions.assertThat(counter2.get()).isCloseTo(500, Offset.offset(1)); + + source.next(Arrays.asList(LoadbalanceTarget.from("2", mockTransport1))); + + for (int j = 0; j < 1000; j++) { + rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); + } + + Assertions.assertThat(counter1.get()).isCloseTo(2500, Offset.offset(1)); + Assertions.assertThat(counter2.get()).isCloseTo(1500, Offset.offset(1)); + + source.next( + Arrays.asList( + LoadbalanceTarget.from("1", mockTransport1), + LoadbalanceTarget.from("2", mockTransport2))); + + for (int j = 0; j < 1000; j++) { + rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); + } + + Assertions.assertThat(counter1.get()).isCloseTo(3000, Offset.offset(1)); + Assertions.assertThat(counter2.get()).isCloseTo(2000, Offset.offset(1)); + } +} From 0a947d31ce12ba8139f2a2078d9405cbb153e624 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 27 Oct 2020 14:53:18 +0200 Subject: [PATCH 054/183] updates list of current developers Signed-off-by: Oleh Dokuka --- gradle/publications.gradle | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/gradle/publications.gradle b/gradle/publications.gradle index b12d9e9c2..51c1d57fa 100644 --- a/gradle/publications.gradle +++ b/gradle/publications.gradle @@ -21,11 +21,6 @@ subprojects { } } developers { - developer { - id = 'robertroeser' - name = 'Robert Roeser' - email = 'robert@netifi.com' - } developer { id = 'rdegnan' name = 'Ryland Degnan' @@ -39,12 +34,12 @@ subprojects { developer { id = 'OlegDokuka' name = 'Oleh Dokuka' - email = 'oleh@netifi.com' + email = 'oleh.dokuka@icloud.com' } developer { - id = 'mostroverkhov' - name = 'Maksym Ostroverkhov' - email = 'm.ostroverkhov@gmail.com' + id = 'rstoyanchev' + name = 'Rossen Stoyanchev' + email = 'rstoyanchev@vmware.com' } } scm { From f404f3eb286550e0ea41a1efdec26206c108cc97 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 27 Oct 2020 15:09:24 +0200 Subject: [PATCH 055/183] fixes to use round-robin strategy Signed-off-by: Oleh Dokuka --- .../tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java index feafdb7a6..abed4a52d 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java @@ -97,7 +97,7 @@ public static void main(String[] args) { }); RSocketClient rSocketClient = - LoadbalanceRSocketClient.builder(producer).weightedLoadbalanceStrategy().build(); + LoadbalanceRSocketClient.builder(producer).roundRobinLoadbalanceStrategy().build(); for (int i = 0; i < 10000; i++) { try { From 9f35dab9073f8ffd9fdfbe79ff9b630441c4f247 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 27 Oct 2020 16:02:02 +0200 Subject: [PATCH 056/183] bumps versions & updates readme Signed-off-by: Oleh Dokuka --- README.md | 10 +++++----- gradle.properties | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index bd12b294c..4b7ced77e 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ repositories { maven { url 'https://repo.spring.io/milestone' } // Reactor milestones (if needed) } dependencies { - implementation 'io.rsocket:rsocket-core:1.1.0-M1' - implementation 'io.rsocket:rsocket-transport-netty:1.1.0-M1' + implementation 'io.rsocket:rsocket-core:1.1.0' + implementation 'io.rsocket:rsocket-transport-netty:1.1.0' } ``` @@ -44,8 +44,8 @@ repositories { maven { url 'https://repo.spring.io/snapshot' } // Reactor snapshots (if needed) } dependencies { - implementation 'io.rsocket:rsocket-core:1.1.0-SNAPSHOT' - implementation 'io.rsocket:rsocket-transport-netty:1.1.0-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:1.1.1-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-netty:1.1.1-SNAPSHOT' } ``` @@ -133,7 +133,7 @@ For bugs, questions and discussions please use the [Github Issues](https://githu ## LICENSE -Copyright 2015-2018 the original author or authors. +Copyright 2015-2020 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/gradle.properties b/gradle.properties index da57c02b4..7a719d4fc 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.0 -perfBaselineVersion=1.0.2 +version=1.1.1-SNAPSHOT +perfBaselineVersion=1.1.0 From 82241f35a6628430a9ce1ea965ecac58e4a4aca8 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 27 Oct 2020 16:28:43 +0200 Subject: [PATCH 057/183] removes outdated perf-test Signed-off-by: Oleh Dokuka --- .../io/rsocket/core/StreamIdSupplierPerf.java | 43 ------------------- 1 file changed, 43 deletions(-) delete mode 100644 benchmarks/src/main/java/io/rsocket/core/StreamIdSupplierPerf.java diff --git a/benchmarks/src/main/java/io/rsocket/core/StreamIdSupplierPerf.java b/benchmarks/src/main/java/io/rsocket/core/StreamIdSupplierPerf.java deleted file mode 100644 index 6b4f3f624..000000000 --- a/benchmarks/src/main/java/io/rsocket/core/StreamIdSupplierPerf.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.rsocket.core; - -import io.netty.util.collection.IntObjectMap; -import io.rsocket.internal.SynchronizedIntObjectHashMap; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Warmup; -import org.openjdk.jmh.infra.Blackhole; - -@BenchmarkMode(Mode.Throughput) -@Fork( - value = 1 // , jvmArgsAppend = {"-Dio.netty.leakDetection.level=advanced"} - ) -@Warmup(iterations = 10) -@Measurement(iterations = 10) -@State(Scope.Thread) -public class StreamIdSupplierPerf { - @Benchmark - public void benchmarkStreamId(Input input) { - int i = input.supplier.nextStreamId(input.map); - input.bh.consume(i); - } - - @State(Scope.Benchmark) - public static class Input { - Blackhole bh; - IntObjectMap map; - StreamIdSupplier supplier; - - @Setup - public void setup(Blackhole bh) { - this.supplier = StreamIdSupplier.clientSupplier(); - this.bh = bh; - this.map = new SynchronizedIntObjectHashMap(); - } - } -} From 2e84405090e585e320e53873cac2b3d80c0c17fc Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 4 Nov 2020 16:13:33 +0200 Subject: [PATCH 058/183] improves error propagation in sync pipe cancellation Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/core/RequestChannelResponderSubscriber.java | 1 + 1 file changed, 1 insertion(+) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java index c52fdca25..8dac9858d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java @@ -288,6 +288,7 @@ public void request(long n) { public void cancel() { long previousState = markInboundTerminated(STATE, this); if (isTerminated(previousState) || isInboundTerminated(previousState)) { + INBOUND_ERROR.lazySet(this, TERMINATED); return; } From bc4a623ff940d1284d143eb0d45ae6f8025b1d3b Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 19 Nov 2020 10:55:40 +0200 Subject: [PATCH 059/183] fixes version name --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7a719d4fc..e6cf7ec55 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.1-SNAPSHOT +version=1.1.1 perfBaselineVersion=1.1.0 From ae94de01c4b52d3e6a55beeee485c97eafd7fc73 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 23 Nov 2020 11:27:19 +0000 Subject: [PATCH 060/183] Ensure Subscriber is removed from sendingSubscriptions Closes gh-961 Signed-off-by: Rossen Stoyanchev --- .../java/io/rsocket/core/RSocketResponder.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) 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 7b67009e8..54f339c12 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -406,22 +406,20 @@ private void handleRequestResponse(int streamId, Mono response) { @Override protected void hookOnNext(Payload payload) { - if (isEmpty) { - isEmpty = false; - } + isEmpty = false; if (!PayloadValidationUtils.isValid(mtu, payload, maxFrameLength)) { payload.release(); cancel(); - final IllegalArgumentException t = - new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); - handleError(streamId, t); + sendingSubscriptions.remove(streamId, this); + handleError(streamId, new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE)); return; } ByteBuf byteBuf = PayloadFrameCodec.encodeNextCompleteReleasingPayload(allocator, streamId, payload); sendProcessor.onNext(byteBuf); + sendingSubscriptions.remove(streamId, this); } @Override @@ -433,10 +431,8 @@ protected void hookOnError(Throwable throwable) { @Override protected void hookOnComplete() { - if (isEmpty) { - if (sendingSubscriptions.remove(streamId, this)) { - sendProcessor.onNext(PayloadFrameCodec.encodeComplete(allocator, streamId)); - } + if (isEmpty && sendingSubscriptions.remove(streamId, this)) { + sendProcessor.onNext(PayloadFrameCodec.encodeComplete(allocator, streamId)); } } }; From a7fb5510bcf2cd2e6d8b2a0a15df0d79360cfafb Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 4 Dec 2020 19:44:59 +0200 Subject: [PATCH 061/183] updates version Signed-off-by: Oleh Dokuka --- README.md | 8 ++++---- gradle.properties | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4f36f6a11..f28185a44 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ repositories { maven { url 'https://repo.spring.io/milestone' } // Reactor milestones (if needed) } dependencies { - implementation 'io.rsocket:rsocket-core:1.0.2' - implementation 'io.rsocket:rsocket-transport-netty:1.0.2' + implementation 'io.rsocket:rsocket-core:1.0.3' + implementation 'io.rsocket:rsocket-transport-netty:1.0.3' } ``` @@ -42,8 +42,8 @@ repositories { maven { url 'https://repo.spring.io/snapshot' } // Reactor snapshots (if needed) } dependencies { - implementation 'io.rsocket:rsocket-core:1.0.3-SNAPSHOT' - implementation 'io.rsocket:rsocket-transport-netty:1.0.3-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:1.0.4-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-netty:1.0.4-SNAPSHOT' } ``` diff --git a/gradle.properties b/gradle.properties index 09eb2d90f..71389f0c1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.3 -perfBaselineVersion=1.0.2 +version=1.0.4 +perfBaselineVersion=1.0.3 From 43d4d0fe20c2d6f1d2ec4225580778c98850cd51 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 7 Dec 2020 20:28:38 +0200 Subject: [PATCH 062/183] fixes RequestOperator to subscribe to the source at later phase (#963) Signed-off-by: Oleh Dokuka --- .../src/main/java/io/rsocket/core/RequestOperator.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java b/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java index f95a5f66c..38c392408 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java @@ -23,6 +23,7 @@ abstract class RequestOperator Fuseable.QueueSubscription, Fuseable { + final CorePublisher source; final String errorMessageOnSecondSubscription; CoreSubscriber actual; @@ -38,8 +39,8 @@ abstract class RequestOperator AtomicIntegerFieldUpdater.newUpdater(RequestOperator.class, "wip"); RequestOperator(CorePublisher source, String errorMessageOnSecondSubscription) { + this.source = source; this.errorMessageOnSecondSubscription = errorMessageOnSecondSubscription; - source.subscribe(this); WIP.lazySet(this, -1); } @@ -52,6 +53,7 @@ public void subscribe(Subscriber actual) { public void subscribe(CoreSubscriber actual) { if (this.wip == -1 && WIP.compareAndSet(this, -1, 0)) { this.actual = actual; + source.subscribe(this); actual.onSubscribe(this); } else { Operators.error(actual, new IllegalStateException(this.errorMessageOnSecondSubscription)); From 0726525c8a0cc8f7006bbbc86b636e682d3ce10d Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 21 Dec 2020 23:11:32 +0200 Subject: [PATCH 063/183] polishes code (#967) Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/core/RSocketServer.java | 3 ++- .../java/io/rsocket/core/ServerSetup.java | 2 +- .../tcp/stream/ClientStreamingToServer.java | 27 ++++++++++--------- .../test/LeaksTrackingByteBufAllocator.java | 2 -- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index b1c93f206..5799d0773 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -385,13 +385,14 @@ private Mono acceptSetup( clientServerConnection, new InvalidSetupException( "Unsupported version: " + SetupFrameCodec.humanReadableVersion(setupFrame))); + return clientServerConnection.onClose(); } boolean leaseEnabled = leasesSupplier != null; if (SetupFrameCodec.honorLease(setupFrame) && !leaseEnabled) { serverSetup.sendError( clientServerConnection, new InvalidSetupException("lease is not supported")); - return Mono.empty(); + return clientServerConnection.onClose(); } return serverSetup.acceptRSocketSetup( 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 1c2b2c7dc..318c54816 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java @@ -65,7 +65,7 @@ public Mono acceptRSocketSetup( if (SetupFrameCodec.resumeEnabled(frame)) { sendError(duplexConnection, new UnsupportedSetupException("resume not supported")); - return Mono.empty(); + return duplexConnection.onClose(); } else { return then.apply(new DefaultKeepAliveHandler(duplexConnection), duplexConnection); } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java index 8116ad4ae..feacdbcfc 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java @@ -16,13 +16,12 @@ package io.rsocket.examples.transport.tcp.stream; -import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.transport.netty.client.TcpClientTransport; -import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.transport.netty.server.WebsocketServerTransport; import io.rsocket.util.DefaultPayload; import java.time.Duration; import org.slf4j.Logger; @@ -33,26 +32,28 @@ public final class ClientStreamingToServer { private static final Logger logger = LoggerFactory.getLogger(ClientStreamingToServer.class); - public static void main(String[] args) { + public static void main(String[] args) throws InterruptedException { RSocketServer.create( SocketAcceptor.forRequestStream( payload -> Flux.interval(Duration.ofMillis(100)) .map(aLong -> DefaultPayload.create("Interval: " + aLong)))) - .bind(TcpServerTransport.create("localhost", 7000)) + .bind(WebsocketServerTransport.create("localhost", 7000)) .subscribe(); RSocket socket = RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000)).block(); - socket - .requestStream(DefaultPayload.create("Hello")) - .map(Payload::getDataUtf8) - .doOnNext(logger::debug) - .take(10) - .then() - .doFinally(signalType -> socket.dispose()) - .then() - .block(); + // socket + // .requestStream(DefaultPayload.create("Hello")) + // .map(Payload::getDataUtf8) + // .doOnNext(logger::debug) + // .take(10) + // .then() + // .doFinally(signalType -> socket.dispose()) + // .then() + // .block(); + + Thread.sleep(1000000); } } 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 0345f2c48..f98a5570e 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java +++ b/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java @@ -80,7 +80,6 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { } if (!hasUnreleased) { - System.out.println(tag + " all the buffers are released..."); return this; } @@ -109,7 +108,6 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { return checkResult; }, tag); - System.out.println(tag + " all the buffers are released..."); } finally { tracker.clear(); } From c2e023a03129a022cc54fa4f36171a90c5a902be Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 26 Dec 2020 15:12:24 +0200 Subject: [PATCH 064/183] adds MpscUnboundedArrayQueue changes from JCTools (#968) Signed-off-by: Oleh Dokuka --- .../jctools/queues/BaseLinkedQueue.java | 80 +++-- .../queues/BaseMpscLinkedArrayQueue.java | 268 ++++++++++------- .../queues/CircularArrayOffsetCalculator.java | 36 --- .../jctools/queues/IndexedQueueSizeUtil.java | 6 +- .../jctools/queues/LinkedArrayQueueUtil.java | 8 +- .../jctools/queues/LinkedQueueNode.java | 8 +- .../jctools/queues/MessagePassingQueue.java | 280 +++++++++++++++++- .../queues/MessagePassingQueueUtil.java | 100 +++++++ .../queues/MpscUnboundedArrayQueue.java | 37 +-- .../{util => queues}/PortableJvmInfo.java | 5 +- .../jctools/{util => queues}/Pow2.java | 5 +- .../jctools/{util => queues}/RangeUtil.java | 5 +- .../{util => queues}/UnsafeAccess.java | 41 ++- .../UnsafeRefArrayAccess.java | 67 ++--- .../internal/jctools/util/InternalAPI.java | 28 -- 15 files changed, 690 insertions(+), 284 deletions(-) delete mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/CircularArrayOffsetCalculator.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueueUtil.java rename rsocket-core/src/main/java/io/rsocket/internal/jctools/{util => queues}/PortableJvmInfo.java (90%) rename rsocket-core/src/main/java/io/rsocket/internal/jctools/{util => queues}/Pow2.java (96%) rename rsocket-core/src/main/java/io/rsocket/internal/jctools/{util => queues}/RangeUtil.java (94%) rename rsocket-core/src/main/java/io/rsocket/internal/jctools/{util => queues}/UnsafeAccess.java (71%) mode change 100755 => 100644 rename rsocket-core/src/main/java/io/rsocket/internal/jctools/{util => queues}/UnsafeRefArrayAccess.java (57%) delete mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/util/InternalAPI.java diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseLinkedQueue.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseLinkedQueue.java index 6939b0f7a..a99ef8a49 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseLinkedQueue.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseLinkedQueue.java @@ -13,15 +13,30 @@ */ package io.rsocket.internal.jctools.queues; -import static io.rsocket.internal.jctools.util.UnsafeAccess.UNSAFE; -import static io.rsocket.internal.jctools.util.UnsafeAccess.fieldOffset; +import static io.rsocket.internal.jctools.queues.UnsafeAccess.UNSAFE; +import static io.rsocket.internal.jctools.queues.UnsafeAccess.fieldOffset; import java.util.AbstractQueue; import java.util.Iterator; abstract class BaseLinkedQueuePad0 extends AbstractQueue implements MessagePassingQueue { - long p00, p01, p02, p03, p04, p05, p06, p07; - long p10, p11, p12, p13, p14, p15, p16; + byte b000, b001, b002, b003, b004, b005, b006, b007; // 8b + byte b010, b011, b012, b013, b014, b015, b016, b017; // 16b + byte b020, b021, b022, b023, b024, b025, b026, b027; // 24b + byte b030, b031, b032, b033, b034, b035, b036, b037; // 32b + byte b040, b041, b042, b043, b044, b045, b046, b047; // 40b + byte b050, b051, b052, b053, b054, b055, b056, b057; // 48b + byte b060, b061, b062, b063, b064, b065, b066, b067; // 56b + byte b070, b071, b072, b073, b074, b075, b076, b077; // 64b + byte b100, b101, b102, b103, b104, b105, b106, b107; // 72b + byte b110, b111, b112, b113, b114, b115, b116, b117; // 80b + byte b120, b121, b122, b123, b124, b125, b126, b127; // 88b + byte b130, b131, b132, b133, b134, b135, b136, b137; // 96b + byte b140, b141, b142, b143, b144, b145, b146, b147; // 104b + byte b150, b151, b152, b153, b154, b155, b156, b157; // 112b + byte b160, b161, b162, b163, b164, b165, b166, b167; // 120b + // byte b170,b171,b172,b173,b174,b175,b176,b177;//128b + // * drop 8b as object header acts as padding and is >= 8b * } // $gen:ordered-fields @@ -29,18 +44,20 @@ abstract class BaseLinkedQueueProducerNodeRef extends BaseLinkedQueuePad0 static final long P_NODE_OFFSET = fieldOffset(BaseLinkedQueueProducerNodeRef.class, "producerNode"); - private LinkedQueueNode producerNode; + private volatile LinkedQueueNode producerNode; final void spProducerNode(LinkedQueueNode newValue) { - producerNode = newValue; + UNSAFE.putObject(this, P_NODE_OFFSET, newValue); + } + + final void soProducerNode(LinkedQueueNode newValue) { + UNSAFE.putOrderedObject(this, P_NODE_OFFSET, newValue); } - @SuppressWarnings("unchecked") final LinkedQueueNode lvProducerNode() { - return (LinkedQueueNode) UNSAFE.getObjectVolatile(this, P_NODE_OFFSET); + return producerNode; } - @SuppressWarnings("unchecked") final boolean casProducerNode(LinkedQueueNode expect, LinkedQueueNode newValue) { return UNSAFE.compareAndSwapObject(this, P_NODE_OFFSET, expect, newValue); } @@ -51,8 +68,22 @@ final LinkedQueueNode lpProducerNode() { } abstract class BaseLinkedQueuePad1 extends BaseLinkedQueueProducerNodeRef { - long p01, p02, p03, p04, p05, p06, p07; - long p10, p11, p12, p13, p14, p15, p16, p17; + byte b000, b001, b002, b003, b004, b005, b006, b007; // 8b + byte b010, b011, b012, b013, b014, b015, b016, b017; // 16b + byte b020, b021, b022, b023, b024, b025, b026, b027; // 24b + byte b030, b031, b032, b033, b034, b035, b036, b037; // 32b + byte b040, b041, b042, b043, b044, b045, b046, b047; // 40b + byte b050, b051, b052, b053, b054, b055, b056, b057; // 48b + byte b060, b061, b062, b063, b064, b065, b066, b067; // 56b + byte b070, b071, b072, b073, b074, b075, b076, b077; // 64b + byte b100, b101, b102, b103, b104, b105, b106, b107; // 72b + byte b110, b111, b112, b113, b114, b115, b116, b117; // 80b + byte b120, b121, b122, b123, b124, b125, b126, b127; // 88b + byte b130, b131, b132, b133, b134, b135, b136, b137; // 96b + byte b140, b141, b142, b143, b144, b145, b146, b147; // 104b + byte b150, b151, b152, b153, b154, b155, b156, b157; // 112b + byte b160, b161, b162, b163, b164, b165, b166, b167; // 120b + byte b170, b171, b172, b173, b174, b175, b176, b177; // 128b } // $gen:ordered-fields @@ -77,16 +108,27 @@ final LinkedQueueNode lpConsumerNode() { } abstract class BaseLinkedQueuePad2 extends BaseLinkedQueueConsumerNodeRef { - long p01, p02, p03, p04, p05, p06, p07; - long p10, p11, p12, p13, p14, p15, p16, p17; + byte b000, b001, b002, b003, b004, b005, b006, b007; // 8b + byte b010, b011, b012, b013, b014, b015, b016, b017; // 16b + byte b020, b021, b022, b023, b024, b025, b026, b027; // 24b + byte b030, b031, b032, b033, b034, b035, b036, b037; // 32b + byte b040, b041, b042, b043, b044, b045, b046, b047; // 40b + byte b050, b051, b052, b053, b054, b055, b056, b057; // 48b + byte b060, b061, b062, b063, b064, b065, b066, b067; // 56b + byte b070, b071, b072, b073, b074, b075, b076, b077; // 64b + byte b100, b101, b102, b103, b104, b105, b106, b107; // 72b + byte b110, b111, b112, b113, b114, b115, b116, b117; // 80b + byte b120, b121, b122, b123, b124, b125, b126, b127; // 88b + byte b130, b131, b132, b133, b134, b135, b136, b137; // 96b + byte b140, b141, b142, b143, b144, b145, b146, b147; // 104b + byte b150, b151, b152, b153, b154, b155, b156, b157; // 112b + byte b160, b161, b162, b163, b164, b165, b166, b167; // 120b + byte b170, b171, b172, b173, b174, b175, b176, b177; // 128b } /** * A base data structure for concurrent linked queues. For convenience also pulled in common single * consumer methods since at this time there's no plan to implement MC. - * - * @param - * @author nitsanw */ abstract class BaseLinkedQueue extends BaseLinkedQueuePad2 { @@ -158,8 +200,10 @@ public final int size() { * @see MessagePassingQueue#isEmpty() */ @Override - public final boolean isEmpty() { - return lvConsumerNode() == lvProducerNode(); + public boolean isEmpty() { + LinkedQueueNode consumerNode = lvConsumerNode(); + LinkedQueueNode producerNode = lvProducerNode(); + return consumerNode == producerNode; } protected E getSingleConsumerNodeValue( diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseMpscLinkedArrayQueue.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseMpscLinkedArrayQueue.java index 635779df3..cfad5ef71 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseMpscLinkedArrayQueue.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseMpscLinkedArrayQueue.java @@ -13,25 +13,38 @@ */ package io.rsocket.internal.jctools.queues; -import static io.rsocket.internal.jctools.queues.CircularArrayOffsetCalculator.allocate; import static io.rsocket.internal.jctools.queues.LinkedArrayQueueUtil.length; -import static io.rsocket.internal.jctools.queues.LinkedArrayQueueUtil.modifiedCalcElementOffset; -import static io.rsocket.internal.jctools.util.UnsafeAccess.UNSAFE; -import static io.rsocket.internal.jctools.util.UnsafeAccess.fieldOffset; -import static io.rsocket.internal.jctools.util.UnsafeRefArrayAccess.calcElementOffset; -import static io.rsocket.internal.jctools.util.UnsafeRefArrayAccess.lvElement; -import static io.rsocket.internal.jctools.util.UnsafeRefArrayAccess.soElement; +import static io.rsocket.internal.jctools.queues.LinkedArrayQueueUtil.modifiedCalcCircularRefElementOffset; +import static io.rsocket.internal.jctools.queues.UnsafeAccess.UNSAFE; +import static io.rsocket.internal.jctools.queues.UnsafeAccess.fieldOffset; +import static io.rsocket.internal.jctools.queues.UnsafeRefArrayAccess.allocateRefArray; +import static io.rsocket.internal.jctools.queues.UnsafeRefArrayAccess.calcCircularRefElementOffset; +import static io.rsocket.internal.jctools.queues.UnsafeRefArrayAccess.calcRefElementOffset; +import static io.rsocket.internal.jctools.queues.UnsafeRefArrayAccess.lvRefElement; +import static io.rsocket.internal.jctools.queues.UnsafeRefArrayAccess.soRefElement; import io.rsocket.internal.jctools.queues.IndexedQueueSizeUtil.IndexedQueue; -import io.rsocket.internal.jctools.util.PortableJvmInfo; -import io.rsocket.internal.jctools.util.Pow2; -import io.rsocket.internal.jctools.util.RangeUtil; import java.util.AbstractQueue; import java.util.Iterator; +import java.util.NoSuchElementException; abstract class BaseMpscLinkedArrayQueuePad1 extends AbstractQueue implements IndexedQueue { - long p01, p02, p03, p04, p05, p06, p07; - long p10, p11, p12, p13, p14, p15, p16, p17; + byte b000, b001, b002, b003, b004, b005, b006, b007; // 8b + byte b010, b011, b012, b013, b014, b015, b016, b017; // 16b + byte b020, b021, b022, b023, b024, b025, b026, b027; // 24b + byte b030, b031, b032, b033, b034, b035, b036, b037; // 32b + byte b040, b041, b042, b043, b044, b045, b046, b047; // 40b + byte b050, b051, b052, b053, b054, b055, b056, b057; // 48b + byte b060, b061, b062, b063, b064, b065, b066, b067; // 56b + byte b070, b071, b072, b073, b074, b075, b076, b077; // 64b + byte b100, b101, b102, b103, b104, b105, b106, b107; // 72b + byte b110, b111, b112, b113, b114, b115, b116, b117; // 80b + byte b120, b121, b122, b123, b124, b125, b126, b127; // 88b + byte b130, b131, b132, b133, b134, b135, b136, b137; // 96b + byte b140, b141, b142, b143, b144, b145, b146, b147; // 104b + byte b150, b151, b152, b153, b154, b155, b156, b157; // 112b + byte b160, b161, b162, b163, b164, b165, b166, b167; // 120b + byte b170, b171, b172, b173, b174, b175, b176, b177; // 128b } // $gen:ordered-fields @@ -56,8 +69,22 @@ final boolean casProducerIndex(long expect, long newValue) { } abstract class BaseMpscLinkedArrayQueuePad2 extends BaseMpscLinkedArrayQueueProducerFields { - long p01, p02, p03, p04, p05, p06, p07; - long p10, p11, p12, p13, p14, p15, p16, p17; + byte b000, b001, b002, b003, b004, b005, b006, b007; // 8b + byte b010, b011, b012, b013, b014, b015, b016, b017; // 16b + byte b020, b021, b022, b023, b024, b025, b026, b027; // 24b + byte b030, b031, b032, b033, b034, b035, b036, b037; // 32b + byte b040, b041, b042, b043, b044, b045, b046, b047; // 40b + byte b050, b051, b052, b053, b054, b055, b056, b057; // 48b + byte b060, b061, b062, b063, b064, b065, b066, b067; // 56b + byte b070, b071, b072, b073, b074, b075, b076, b077; // 64b + byte b100, b101, b102, b103, b104, b105, b106, b107; // 72b + byte b110, b111, b112, b113, b114, b115, b116, b117; // 80b + byte b120, b121, b122, b123, b124, b125, b126, b127; // 88b + byte b130, b131, b132, b133, b134, b135, b136, b137; // 96b + byte b140, b141, b142, b143, b144, b145, b146, b147; // 104b + byte b150, b151, b152, b153, b154, b155, b156, b157; // 112b + byte b160, b161, b162, b163, b164, b165, b166, b167; // 120b + byte b170, b171, b172, b173, b174, b175, b176, b177; // 128b } // $gen:ordered-fields @@ -84,8 +111,22 @@ final void soConsumerIndex(long newValue) { } abstract class BaseMpscLinkedArrayQueuePad3 extends BaseMpscLinkedArrayQueueConsumerFields { - long p0, p1, p2, p3, p4, p5, p6, p7; - long p10, p11, p12, p13, p14, p15, p16, p17; + byte b000, b001, b002, b003, b004, b005, b006, b007; // 8b + byte b010, b011, b012, b013, b014, b015, b016, b017; // 16b + byte b020, b021, b022, b023, b024, b025, b026, b027; // 24b + byte b030, b031, b032, b033, b034, b035, b036, b037; // 32b + byte b040, b041, b042, b043, b044, b045, b046, b047; // 40b + byte b050, b051, b052, b053, b054, b055, b056, b057; // 48b + byte b060, b061, b062, b063, b064, b065, b066, b067; // 56b + byte b070, b071, b072, b073, b074, b075, b076, b077; // 64b + byte b100, b101, b102, b103, b104, b105, b106, b107; // 72b + byte b110, b111, b112, b113, b114, b115, b116, b117; // 80b + byte b120, b121, b122, b123, b124, b125, b126, b127; // 88b + byte b130, b131, b132, b133, b134, b135, b136, b137; // 96b + byte b140, b141, b142, b143, b144, b145, b146, b147; // 104b + byte b150, b151, b152, b153, b154, b155, b156, b157; // 112b + byte b160, b161, b162, b163, b164, b165, b166, b167; // 120b + byte b170, b171, b172, b173, b174, b175, b176, b177; // 128b } // $gen:ordered-fields @@ -115,12 +156,9 @@ final void soProducerLimit(long newValue) { * An MPSC array queue which starts at initialCapacity and grows to maxCapacity in * linked chunks of the initial size. The queue grows only when the current buffer is full and * elements are not copied on resize, instead a link to the new buffer is stored in the old buffer - * for the consumer to follow.
    - * - * @param + * for the consumer to follow. */ -public abstract class BaseMpscLinkedArrayQueue - extends BaseMpscLinkedArrayQueueColdProducerFields +abstract class BaseMpscLinkedArrayQueue extends BaseMpscLinkedArrayQueueColdProducerFields implements MessagePassingQueue, QueueProgressIndicators { // No post padding here, subclasses must add private static final Object JUMP = new Object(); @@ -141,7 +179,7 @@ public BaseMpscLinkedArrayQueue(final int initialCapacity) { // leave lower bit of mask clear long mask = (p2capacity - 1) << 1; // need extra element to point at next array - E[] buffer = allocate(p2capacity + 1); + E[] buffer = allocateRefArray(p2capacity + 1); producerBuffer = buffer; producerMask = mask; consumerBuffer = buffer; @@ -150,7 +188,7 @@ public BaseMpscLinkedArrayQueue(final int initialCapacity) { } @Override - public final int size() { + public int size() { // NOTE: because indices are on even numbers we cannot use the size util. /* @@ -181,7 +219,7 @@ public final int size() { } @Override - public final boolean isEmpty() { + public boolean isEmpty() { // Order matters! // Loading consumer before producer allows for producer increments after consumer index is read. // This ensures this method is conservative in it's estimate. Note that as this is an MPMC there @@ -240,8 +278,8 @@ public boolean offer(final E e) { } } // INDEX visible before ELEMENT - final long offset = modifiedCalcElementOffset(pIndex, mask); - soElement(buffer, offset, e); // release element e + final long offset = modifiedCalcCircularRefElementOffset(pIndex, mask); + soRefElement(buffer, offset, e); // release element e return true; } @@ -257,8 +295,8 @@ public E poll() { final long index = lpConsumerIndex(); final long mask = consumerMask; - final long offset = modifiedCalcElementOffset(index, mask); - Object e = lvElement(buffer, offset); // LoadLoad + final long offset = modifiedCalcCircularRefElementOffset(index, mask); + Object e = lvRefElement(buffer, offset); if (e == null) { if (index != lvProducerIndex()) { // poll() == null iff queue is empty, null element is not strong enough indicator, so we @@ -266,7 +304,7 @@ public E poll() { // check the producer index. If the queue is indeed not empty we spin until element is // visible. do { - e = lvElement(buffer, offset); + e = lvRefElement(buffer, offset); } while (e == null); } else { return null; @@ -278,7 +316,7 @@ public E poll() { return newBufferPoll(nextBuffer, index); } - soElement(buffer, offset, null); // release element null + soRefElement(buffer, offset, null); // release element null soConsumerIndex(index + 2); // release cIndex return (E) e; } @@ -295,14 +333,14 @@ public E peek() { final long index = lpConsumerIndex(); final long mask = consumerMask; - final long offset = modifiedCalcElementOffset(index, mask); - Object e = lvElement(buffer, offset); // LoadLoad + final long offset = modifiedCalcCircularRefElementOffset(index, mask); + Object e = lvRefElement(buffer, offset); if (e == null && index != lvProducerIndex()) { // peek() == null iff queue is empty, null element is not strong enough indicator, so we must // check the producer index. If the queue is indeed not empty we spin until element is // visible. do { - e = lvElement(buffer, offset); + e = lvRefElement(buffer, offset); } while (e == null); } if (e == JUMP) { @@ -346,31 +384,31 @@ else if (casProducerIndex(pIndex, pIndex + 1)) { @SuppressWarnings("unchecked") private E[] nextBuffer(final E[] buffer, final long mask) { final long offset = nextArrayOffset(mask); - final E[] nextBuffer = (E[]) lvElement(buffer, offset); + final E[] nextBuffer = (E[]) lvRefElement(buffer, offset); consumerBuffer = nextBuffer; consumerMask = (length(nextBuffer) - 2) << 1; - soElement(buffer, offset, BUFFER_CONSUMED); + soRefElement(buffer, offset, BUFFER_CONSUMED); return nextBuffer; } - private long nextArrayOffset(long mask) { - return modifiedCalcElementOffset(mask + 2, Long.MAX_VALUE); + private static long nextArrayOffset(long mask) { + return modifiedCalcCircularRefElementOffset(mask + 2, Long.MAX_VALUE); } private E newBufferPoll(E[] nextBuffer, long index) { - final long offset = modifiedCalcElementOffset(index, consumerMask); - final E n = lvElement(nextBuffer, offset); // LoadLoad + final long offset = modifiedCalcCircularRefElementOffset(index, consumerMask); + final E n = lvRefElement(nextBuffer, offset); if (n == null) { throw new IllegalStateException("new buffer must have at least one element"); } - soElement(nextBuffer, offset, null); // StoreStore + soRefElement(nextBuffer, offset, null); soConsumerIndex(index + 2); return n; } private E newBufferPeek(E[] nextBuffer, long index) { - final long offset = modifiedCalcElementOffset(index, consumerMask); - final E n = lvElement(nextBuffer, offset); // LoadLoad + final long offset = modifiedCalcCircularRefElementOffset(index, consumerMask); + final E n = lvRefElement(nextBuffer, offset); if (null == n) { throw new IllegalStateException("new buffer must have at least one element"); } @@ -402,8 +440,8 @@ public E relaxedPoll() { final long index = lpConsumerIndex(); final long mask = consumerMask; - final long offset = modifiedCalcElementOffset(index, mask); - Object e = lvElement(buffer, offset); // LoadLoad + final long offset = modifiedCalcCircularRefElementOffset(index, mask); + Object e = lvRefElement(buffer, offset); if (e == null) { return null; } @@ -411,7 +449,7 @@ public E relaxedPoll() { final E[] nextBuffer = nextBuffer(buffer, mask); return newBufferPoll(nextBuffer, index); } - soElement(buffer, offset, null); + soRefElement(buffer, offset, null); soConsumerIndex(index + 2); return (E) e; } @@ -423,8 +461,8 @@ public E relaxedPeek() { final long index = lpConsumerIndex(); final long mask = consumerMask; - final long offset = modifiedCalcElementOffset(index, mask); - Object e = lvElement(buffer, offset); // LoadLoad + final long offset = modifiedCalcCircularRefElementOffset(index, mask); + Object e = lvRefElement(buffer, offset); if (e == JUMP) { return newBufferPeek(nextBuffer(buffer, mask), index); } @@ -447,7 +485,11 @@ public int fill(Supplier s) { } @Override - public int fill(Supplier s, int batchSize) { + public int fill(Supplier s, int limit) { + if (null == s) throw new IllegalArgumentException("supplier is null"); + if (limit < 0) throw new IllegalArgumentException("limit is negative:" + limit); + if (limit == 0) return 0; + long mask; E[] buffer; long pIndex; @@ -471,9 +513,10 @@ public int fill(Supplier s, int batchSize) { // a successful CAS ties the ordering, lv(pIndex) -> [mask/buffer] -> cas(pIndex) // we want 'limit' slots, but will settle for whatever is visible to 'producerLimit' - long batchIndex = Math.min(producerLimit, pIndex + 2 * batchSize); + long batchIndex = + Math.min(producerLimit, pIndex + 2l * limit); // -> producerLimit >= batchIndex - if (pIndex >= producerLimit || producerLimit < batchIndex) { + if (pIndex >= producerLimit) { int result = offerSlowPath(mask, pIndex, producerLimit); switch (result) { case CONTINUE_TO_P_INDEX_CAS: @@ -496,23 +539,15 @@ public int fill(Supplier s, int batchSize) { } for (int i = 0; i < claimedSlots; i++) { - final long offset = modifiedCalcElementOffset(pIndex + 2 * i, mask); - soElement(buffer, offset, s.get()); + final long offset = modifiedCalcCircularRefElementOffset(pIndex + 2l * i, mask); + soRefElement(buffer, offset, s.get()); } return claimedSlots; } @Override - public void fill(Supplier s, WaitStrategy w, ExitCondition exit) { - - while (exit.keepRunning()) { - if (fill(s, PortableJvmInfo.RECOMENDED_OFFER_BATCH) == 0) { - int idleCounter = 0; - while (exit.keepRunning() && fill(s, PortableJvmInfo.RECOMENDED_OFFER_BATCH) == 0) { - idleCounter = w.idle(idleCounter); - } - } - } + public void fill(Supplier s, WaitStrategy wait, ExitCondition exit) { + MessagePassingQueueUtil.fill(this, s, wait, exit); } @Override @@ -521,30 +556,13 @@ public int drain(Consumer c) { } @Override - public int drain(final Consumer c, final int limit) { - // Impl note: there are potentially some small gains to be had by manually inlining - // relaxedPoll() and hoisting - // reused fields out to reduce redundant reads. - int i = 0; - E m; - for (; i < limit && (m = relaxedPoll()) != null; i++) { - c.accept(m); - } - return i; + public int drain(Consumer c, int limit) { + return MessagePassingQueueUtil.drain(this, c, limit); } @Override - public void drain(Consumer c, WaitStrategy w, ExitCondition exit) { - int idleCounter = 0; - while (exit.keepRunning()) { - E e = relaxedPoll(); - if (e == null) { - idleCounter = w.idle(idleCounter); - continue; - } - idleCounter = 0; - c.accept(e); - } + public void drain(Consumer c, WaitStrategy wait, ExitCondition exit) { + MessagePassingQueueUtil.drain(this, c, wait, exit); } /** @@ -559,21 +577,28 @@ public void drain(Consumer c, WaitStrategy w, ExitCondition exit) { */ @Override public Iterator iterator() { - return new WeakIterator(); + return new WeakIterator(consumerBuffer, lvConsumerIndex(), lvProducerIndex()); } - private final class WeakIterator implements Iterator { - + private static class WeakIterator implements Iterator { + private final long pIndex; private long nextIndex; private E nextElement; private E[] currentBuffer; - private int currentBufferLength; + private int mask; - WeakIterator() { - setBuffer(consumerBuffer); + WeakIterator(E[] currentBuffer, long cIndex, long pIndex) { + this.pIndex = pIndex >> 1; + this.nextIndex = cIndex >> 1; + setBuffer(currentBuffer); nextElement = getNext(); } + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + @Override public boolean hasNext() { return nextElement != null; @@ -581,37 +606,54 @@ public boolean hasNext() { @Override public E next() { - E e = nextElement; + final E e = nextElement; + if (e == null) { + throw new NoSuchElementException(); + } nextElement = getNext(); return e; } private void setBuffer(E[] buffer) { this.currentBuffer = buffer; - this.currentBufferLength = length(buffer); - this.nextIndex = 0; + this.mask = length(buffer) - 2; } private E getNext() { - while (true) { - while (nextIndex < currentBufferLength - 1) { - long offset = calcElementOffset(nextIndex++); - E e = lvElement(currentBuffer, offset); - if (e != null && e != JUMP) { - return e; - } + while (nextIndex < pIndex) { + long index = nextIndex++; + E e = lvRefElement(currentBuffer, calcCircularRefElementOffset(index, mask)); + // skip removed/not yet visible elements + if (e == null) { + continue; } - long offset = calcElementOffset(currentBufferLength - 1); - Object nextArray = lvElement(currentBuffer, offset); - if (nextArray == BUFFER_CONSUMED) { - // Consumer may have passed us, just jump to the current consumer buffer - setBuffer(consumerBuffer); - } else if (nextArray != null) { - setBuffer((E[]) nextArray); - } else { + + // not null && not JUMP -> found next element + if (e != JUMP) { + return e; + } + + // need to jump to the next buffer + int nextBufferIndex = mask + 1; + Object nextBuffer = lvRefElement(currentBuffer, calcRefElementOffset(nextBufferIndex)); + + if (nextBuffer == BUFFER_CONSUMED || nextBuffer == null) { + // Consumer may have passed us, or the next buffer is not visible yet: drop out early return null; } + + setBuffer((E[]) nextBuffer); + // now with the new array retry the load, it can't be a JUMP, but we need to repeat same + // index + e = lvRefElement(currentBuffer, calcCircularRefElementOffset(index, mask)); + // skip removed/not yet visible elements + if (e == null) { + continue; + } else { + return e; + } } + return null; } } @@ -620,7 +662,7 @@ private void resize(long oldMask, E[] oldBuffer, long pIndex, E e, Supplier s int newBufferLength = getNextBufferSize(oldBuffer); final E[] newBuffer; try { - newBuffer = allocate(newBufferLength); + newBuffer = allocateRefArray(newBufferLength); } catch (OutOfMemoryError oom) { assert lvProducerIndex() == pIndex + 1; soProducerIndex(pIndex); @@ -631,11 +673,11 @@ private void resize(long oldMask, E[] oldBuffer, long pIndex, E e, Supplier s final int newMask = (newBufferLength - 2) << 1; producerMask = newMask; - final long offsetInOld = modifiedCalcElementOffset(pIndex, oldMask); - final long offsetInNew = modifiedCalcElementOffset(pIndex, newMask); + final long offsetInOld = modifiedCalcCircularRefElementOffset(pIndex, oldMask); + final long offsetInNew = modifiedCalcCircularRefElementOffset(pIndex, newMask); - soElement(newBuffer, offsetInNew, e == null ? s.get() : e); // element in new array - soElement(oldBuffer, nextArrayOffset(oldMask), newBuffer); // buffer linked + soRefElement(newBuffer, offsetInNew, e == null ? s.get() : e); // element in new array + soRefElement(oldBuffer, nextArrayOffset(oldMask), newBuffer); // buffer linked // ASSERT code final long cIndex = lvConsumerIndex(); @@ -652,7 +694,7 @@ private void resize(long oldMask, E[] oldBuffer, long pIndex, E e, Supplier s // INDEX visible before ELEMENT, consistent with consumer expectation // make resize visible to consumer - soElement(oldBuffer, offsetInOld, JUMP); + soRefElement(oldBuffer, offsetInOld, JUMP); } /** @return next buffer size(inclusive of next array pointer) */ diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/CircularArrayOffsetCalculator.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/CircularArrayOffsetCalculator.java deleted file mode 100644 index d746fccbb..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/CircularArrayOffsetCalculator.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.internal.jctools.queues; - -import static io.rsocket.internal.jctools.util.UnsafeRefArrayAccess.REF_ARRAY_BASE; -import static io.rsocket.internal.jctools.util.UnsafeRefArrayAccess.REF_ELEMENT_SHIFT; - -import io.rsocket.internal.jctools.util.InternalAPI; - -@InternalAPI -public final class CircularArrayOffsetCalculator { - @SuppressWarnings("unchecked") - public static E[] allocate(int capacity) { - return (E[]) new Object[capacity]; - } - - /** - * @param index desirable element index - * @param mask (length - 1) - * @return the offset in bytes within the array for a given index. - */ - public static long calcElementOffset(long index, long mask) { - return REF_ARRAY_BASE + ((index & mask) << REF_ELEMENT_SHIFT); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/IndexedQueueSizeUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/IndexedQueueSizeUtil.java index 1b7d43166..40116bbe1 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/IndexedQueueSizeUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/IndexedQueueSizeUtil.java @@ -13,10 +13,7 @@ */ package io.rsocket.internal.jctools.queues; -import io.rsocket.internal.jctools.util.InternalAPI; - -@InternalAPI -public final class IndexedQueueSizeUtil { +final class IndexedQueueSizeUtil { public static int size(IndexedQueue iq) { /* * It is possible for a thread to be interrupted or reschedule between the read of the producer and @@ -54,7 +51,6 @@ public static boolean isEmpty(IndexedQueue iq) { return (iq.lvConsumerIndex() == iq.lvProducerIndex()); } - @InternalAPI public interface IndexedQueue { long lvConsumerIndex(); diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedArrayQueueUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedArrayQueueUtil.java index 5e7831128..37651f351 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedArrayQueueUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedArrayQueueUtil.java @@ -13,13 +13,11 @@ */ package io.rsocket.internal.jctools.queues; -import static io.rsocket.internal.jctools.util.UnsafeRefArrayAccess.REF_ARRAY_BASE; -import static io.rsocket.internal.jctools.util.UnsafeRefArrayAccess.REF_ELEMENT_SHIFT; +import static io.rsocket.internal.jctools.queues.UnsafeRefArrayAccess.REF_ARRAY_BASE; +import static io.rsocket.internal.jctools.queues.UnsafeRefArrayAccess.REF_ELEMENT_SHIFT; /** This is used for method substitution in the LinkedArray classes code generation. */ final class LinkedArrayQueueUtil { - private LinkedArrayQueueUtil() {} - static int length(Object[] buf) { return buf.length; } @@ -29,7 +27,7 @@ static int length(Object[] buf) { * is compensated for by reducing the element shift. The computation is constant folded, so * there's no cost. */ - static long modifiedCalcElementOffset(long index, long mask) { + static long modifiedCalcCircularRefElementOffset(long index, long mask) { return REF_ARRAY_BASE + ((index & mask) << (REF_ELEMENT_SHIFT - 1)); } diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedQueueNode.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedQueueNode.java index 6ea69e330..72e78bb92 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedQueueNode.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedQueueNode.java @@ -13,8 +13,8 @@ */ package io.rsocket.internal.jctools.queues; -import static io.rsocket.internal.jctools.util.UnsafeAccess.UNSAFE; -import static io.rsocket.internal.jctools.util.UnsafeAccess.fieldOffset; +import static io.rsocket.internal.jctools.queues.UnsafeAccess.UNSAFE; +import static io.rsocket.internal.jctools.queues.UnsafeAccess.fieldOffset; final class LinkedQueueNode { private static final long NEXT_OFFSET = fieldOffset(LinkedQueueNode.class, "next"); @@ -53,6 +53,10 @@ public void soNext(LinkedQueueNode n) { UNSAFE.putOrderedObject(this, NEXT_OFFSET, n); } + public void spNext(LinkedQueueNode n) { + UNSAFE.putObject(this, NEXT_OFFSET, n); + } + public LinkedQueueNode lvNext() { return next; } diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueue.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueue.java index e0c3d0ee1..7a0fa901f 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueue.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueue.java @@ -13,55 +13,327 @@ */ package io.rsocket.internal.jctools.queues; +import java.util.Queue; + +/** + * Message passing queues are intended for concurrent method passing. A subset of {@link Queue} + * methods are provided with the same semantics, while further functionality which accomodates the + * concurrent usecase is also on offer. + * + *

    Message passing queues provide happens before semantics to messages passed through, namely + * that writes made by the producer before offering the message are visible to the consuming thread + * after the message has been polled out of the queue. + * + * @param the event/message type + */ public interface MessagePassingQueue { int UNBOUNDED_CAPACITY = -1; interface Supplier { + /** + * This method will return the next value to be written to the queue. As such the queue + * implementations are commited to insert the value once the call is made. + * + *

    Users should be aware that underlying queue implementations may upfront claim parts of the + * queue for batch operations and this will effect the view on the queue from the supplier + * method. In particular size and any offer methods may take the view that the full batch has + * already happened. + * + *

    WARNING: this method is assumed to never throw. Breaking this assumption can lead + * to a broken queue. + * + *

    WARNING: this method is assumed to never return {@code null}. Breaking this + * assumption can lead to a broken queue. + * + * @return new element, NEVER {@code null} + */ T get(); } interface Consumer { + /** + * This method will process an element already removed from the queue. This method is expected + * to never throw an exception. + * + *

    Users should be aware that underlying queue implementations may upfront claim parts of the + * queue for batch operations and this will effect the view on the queue from the accept method. + * In particular size and any poll/peek methods may take the view that the full batch has + * already happened. + * + *

    WARNING: this method is assumed to never throw. Breaking this assumption can lead + * to a broken queue. + * + * @param e not {@code null} + */ void accept(T e); } interface WaitStrategy { + /** + * This method can implement static or dynamic backoff. Dynamic backoff will rely on the counter + * for estimating how long the caller has been idling. The expected usage is: + * + *

    + * + *

    +     * 
    +     * int ic = 0;
    +     * while(true) {
    +     *   if(!isGodotArrived()) {
    +     *     ic = w.idle(ic);
    +     *     continue;
    +     *   }
    +     *   ic = 0;
    +     *   // party with Godot until he goes again
    +     * }
    +     * 
    +     * 
    + * + * @param idleCounter idle calls counter, managed by the idle method until reset + * @return new counter value to be used on subsequent idle cycle + */ int idle(int idleCounter); } interface ExitCondition { + /** + * This method should be implemented such that the flag read or determination cannot be hoisted + * out of a loop which notmally means a volatile load, but with JDK9 VarHandles may mean + * getOpaque. + * + * @return true as long as we should keep running + */ boolean keepRunning(); } + /** + * Called from a producer thread subject to the restrictions appropriate to the implementation and + * according to the {@link Queue#offer(Object)} interface. + * + * @param e not {@code null}, will throw NPE if it is + * @return true if element was inserted into the queue, false iff full + */ boolean offer(T e); + /** + * Called from the consumer thread subject to the restrictions appropriate to the implementation + * and according to the {@link Queue#poll()} interface. + * + * @return a message from the queue if one is available, {@code null} iff empty + */ T poll(); + /** + * Called from the consumer thread subject to the restrictions appropriate to the implementation + * and according to the {@link Queue#peek()} interface. + * + * @return a message from the queue if one is available, {@code null} iff empty + */ T peek(); + /** + * This method's accuracy is subject to concurrent modifications happening as the size is + * estimated and as such is a best effort rather than absolute value. For some implementations + * this method may be O(n) rather than O(1). + * + * @return number of messages in the queue, between 0 and {@link Integer#MAX_VALUE} but less or + * equals to capacity (if bounded). + */ int size(); + /** + * Removes all items from the queue. Called from the consumer thread subject to the restrictions + * appropriate to the implementation and according to the {@link Queue#clear()} interface. + */ void clear(); + /** + * This method's accuracy is subject to concurrent modifications happening as the observation is + * carried out. + * + * @return true if empty, false otherwise + */ boolean isEmpty(); + /** + * @return the capacity of this queue or {@link MessagePassingQueue#UNBOUNDED_CAPACITY} if not + * bounded + */ int capacity(); + /** + * Called from a producer thread subject to the restrictions appropriate to the implementation. As + * opposed to {@link Queue#offer(Object)} this method may return false without the queue being + * full. + * + * @param e not {@code null}, will throw NPE if it is + * @return true if element was inserted into the queue, false if unable to offer + */ boolean relaxedOffer(T e); + /** + * Called from the consumer thread subject to the restrictions appropriate to the implementation. + * As opposed to {@link Queue#poll()} this method may return {@code null} without the queue being + * empty. + * + * @return a message from the queue if one is available, {@code null} if unable to poll + */ T relaxedPoll(); + /** + * Called from the consumer thread subject to the restrictions appropriate to the implementation. + * As opposed to {@link Queue#peek()} this method may return {@code null} without the queue being + * empty. + * + * @return a message from the queue if one is available, {@code null} if unable to peek + */ T relaxedPeek(); - int drain(Consumer c); - - int fill(Supplier s); - + /** + * Remove up to limit elements from the queue and hand to consume. This should be + * semantically similar to: + * + *

    + * + *

    {@code
    +   * M m;
    +   * int i = 0;
    +   * for(;i < limit && (m = relaxedPoll()) != null; i++){
    +   *   c.accept(m);
    +   * }
    +   * return i;
    +   * }
    + * + *

    There's no strong commitment to the queue being empty at the end of a drain. Called from a + * consumer thread subject to the restrictions appropriate to the implementation. + * + *

    WARNING: Explicit assumptions are made with regards to {@link Consumer#accept} make + * sure you have read and understood these before using this method. + * + * @return the number of polled elements + * @throws IllegalArgumentException c is {@code null} + * @throws IllegalArgumentException if limit is negative + */ int drain(Consumer c, int limit); + /** + * Stuff the queue with up to limit elements from the supplier. Semantically similar to: + * + *

    + * + *

    {@code
    +   * for(int i=0; i < limit && relaxedOffer(s.get()); i++);
    +   * }
    + * + *

    There's no strong commitment to the queue being full at the end of a fill. Called from a + * producer thread subject to the restrictions appropriate to the implementation. + * + *

    WARNING: Explicit assumptions are made with regards to {@link Supplier#get} make sure + * you have read and understood these before using this method. + * + * @return the number of offered elements + * @throws IllegalArgumentException s is {@code null} + * @throws IllegalArgumentException if limit is negative + */ int fill(Supplier s, int limit); + /** + * Remove all available item from the queue and hand to consume. This should be semantically + * similar to: + * + *

    +   * M m;
    +   * while((m = relaxedPoll()) != null){
    +   * c.accept(m);
    +   * }
    +   * 
    + * + * There's no strong commitment to the queue being empty at the end of a drain. Called from a + * consumer thread subject to the restrictions appropriate to the implementation. + * + *

    WARNING: Explicit assumptions are made with regards to {@link Consumer#accept} make + * sure you have read and understood these before using this method. + * + * @return the number of polled elements + * @throws IllegalArgumentException c is {@code null} + */ + int drain(Consumer c); + + /** + * Stuff the queue with elements from the supplier. Semantically similar to: + * + *

    +   * while(relaxedOffer(s.get());
    +   * 
    + * + * There's no strong commitment to the queue being full at the end of a fill. Called from a + * producer thread subject to the restrictions appropriate to the implementation. + * + *

    Unbounded queues will fill up the queue with a fixed amount rather than fill up to oblivion. + * + *

    WARNING: Explicit assumptions are made with regards to {@link Supplier#get} make sure + * you have read and understood these before using this method. + * + * @return the number of offered elements + * @throws IllegalArgumentException s is {@code null} + */ + int fill(Supplier s); + + /** + * Remove elements from the queue and hand to consume forever. Semantically similar to: + * + *

    + * + *

    +   *  int idleCounter = 0;
    +   *  while (exit.keepRunning()) {
    +   *      E e = relaxedPoll();
    +   *      if(e==null){
    +   *          idleCounter = wait.idle(idleCounter);
    +   *          continue;
    +   *      }
    +   *      idleCounter = 0;
    +   *      c.accept(e);
    +   *  }
    +   * 
    + * + *

    Called from a consumer thread subject to the restrictions appropriate to the implementation. + * + *

    WARNING: Explicit assumptions are made with regards to {@link Consumer#accept} make + * sure you have read and understood these before using this method. + * + * @throws IllegalArgumentException c OR wait OR exit are {@code null} + */ void drain(Consumer c, WaitStrategy wait, ExitCondition exit); + /** + * Stuff the queue with elements from the supplier forever. Semantically similar to: + * + *

    + * + *

    +   * 
    +   *  int idleCounter = 0;
    +   *  while (exit.keepRunning()) {
    +   *      E e = s.get();
    +   *      while (!relaxedOffer(e)) {
    +   *          idleCounter = wait.idle(idleCounter);
    +   *          continue;
    +   *      }
    +   *      idleCounter = 0;
    +   *  }
    +   * 
    +   * 
    + * + *

    Called from a producer thread subject to the restrictions appropriate to the implementation. + * The main difference being that implementors MUST assure room in the queue is available BEFORE + * calling {@link Supplier#get}. + * + *

    WARNING: Explicit assumptions are made with regards to {@link Supplier#get} make sure + * you have read and understood these before using this method. + * + * @throws IllegalArgumentException s OR wait OR exit are {@code null} + */ void fill(Supplier s, WaitStrategy wait, ExitCondition exit); } diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueueUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueueUtil.java new file mode 100644 index 000000000..cb03364d8 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueueUtil.java @@ -0,0 +1,100 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.internal.jctools.queues; + +import io.rsocket.internal.jctools.queues.MessagePassingQueue.Consumer; +import io.rsocket.internal.jctools.queues.MessagePassingQueue.ExitCondition; +import io.rsocket.internal.jctools.queues.MessagePassingQueue.Supplier; +import io.rsocket.internal.jctools.queues.MessagePassingQueue.WaitStrategy; + +final class MessagePassingQueueUtil { + public static int drain(MessagePassingQueue queue, Consumer c, int limit) { + if (null == c) throw new IllegalArgumentException("c is null"); + if (limit < 0) throw new IllegalArgumentException("limit is negative: " + limit); + if (limit == 0) return 0; + E e; + int i = 0; + for (; i < limit && (e = queue.relaxedPoll()) != null; i++) { + c.accept(e); + } + return i; + } + + public static int drain(MessagePassingQueue queue, Consumer c) { + if (null == c) throw new IllegalArgumentException("c is null"); + E e; + int i = 0; + while ((e = queue.relaxedPoll()) != null) { + i++; + c.accept(e); + } + return i; + } + + public static void drain( + MessagePassingQueue queue, Consumer c, WaitStrategy wait, ExitCondition exit) { + if (null == c) throw new IllegalArgumentException("c is null"); + if (null == wait) throw new IllegalArgumentException("wait is null"); + if (null == exit) throw new IllegalArgumentException("exit condition is null"); + + int idleCounter = 0; + while (exit.keepRunning()) { + final E e = queue.relaxedPoll(); + if (e == null) { + idleCounter = wait.idle(idleCounter); + continue; + } + idleCounter = 0; + c.accept(e); + } + } + + public static void fill( + MessagePassingQueue q, Supplier s, WaitStrategy wait, ExitCondition exit) { + if (null == wait) throw new IllegalArgumentException("waiter is null"); + if (null == exit) throw new IllegalArgumentException("exit condition is null"); + + int idleCounter = 0; + while (exit.keepRunning()) { + if (q.fill(s, PortableJvmInfo.RECOMENDED_OFFER_BATCH) == 0) { + idleCounter = wait.idle(idleCounter); + continue; + } + idleCounter = 0; + } + } + + public static int fillBounded(MessagePassingQueue q, Supplier s) { + return fillInBatchesToLimit(q, s, PortableJvmInfo.RECOMENDED_OFFER_BATCH, q.capacity()); + } + + public static int fillInBatchesToLimit( + MessagePassingQueue q, Supplier s, int batch, int limit) { + long result = + 0; // result is a long because we want to have a safepoint check at regular intervals + do { + final int filled = q.fill(s, batch); + if (filled == 0) { + return (int) result; + } + result += filled; + } while (result <= limit); + return (int) result; + } + + public static int fillUnbounded(MessagePassingQueue q, Supplier s) { + return fillInBatchesToLimit(q, s, PortableJvmInfo.RECOMENDED_OFFER_BATCH, 4096); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MpscUnboundedArrayQueue.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MpscUnboundedArrayQueue.java index 59eab33a1..179070be4 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MpscUnboundedArrayQueue.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MpscUnboundedArrayQueue.java @@ -14,20 +14,31 @@ package io.rsocket.internal.jctools.queues; import static io.rsocket.internal.jctools.queues.LinkedArrayQueueUtil.length; - -import io.rsocket.internal.jctools.util.PortableJvmInfo; +import static io.rsocket.internal.jctools.queues.MessagePassingQueueUtil.fillUnbounded; /** * An MPSC array queue which starts at initialCapacity and grows indefinitely in linked * chunks of the initial size. The queue grows only when the current chunk is full and elements are * not copied on resize, instead a link to the new chunk is stored in the old chunk for the consumer - * to follow.
    - * - * @param + * to follow. */ public class MpscUnboundedArrayQueue extends BaseMpscLinkedArrayQueue { - long p0, p1, p2, p3, p4, p5, p6, p7; - long p10, p11, p12, p13, p14, p15, p16, p17; + byte b000, b001, b002, b003, b004, b005, b006, b007; // 8b + byte b010, b011, b012, b013, b014, b015, b016, b017; // 16b + byte b020, b021, b022, b023, b024, b025, b026, b027; // 24b + byte b030, b031, b032, b033, b034, b035, b036, b037; // 32b + byte b040, b041, b042, b043, b044, b045, b046, b047; // 40b + byte b050, b051, b052, b053, b054, b055, b056, b057; // 48b + byte b060, b061, b062, b063, b064, b065, b066, b067; // 56b + byte b070, b071, b072, b073, b074, b075, b076, b077; // 64b + byte b100, b101, b102, b103, b104, b105, b106, b107; // 72b + byte b110, b111, b112, b113, b114, b115, b116, b117; // 80b + byte b120, b121, b122, b123, b124, b125, b126, b127; // 88b + byte b130, b131, b132, b133, b134, b135, b136, b137; // 96b + byte b140, b141, b142, b143, b144, b145, b146, b147; // 104b + byte b150, b151, b152, b153, b154, b155, b156, b157; // 112b + byte b160, b161, b162, b163, b164, b165, b166, b167; // 120b + byte b170, b171, b172, b173, b174, b175, b176, b177; // 128b public MpscUnboundedArrayQueue(int chunkSize) { super(chunkSize); @@ -50,17 +61,7 @@ public int drain(Consumer c) { @Override public int fill(Supplier s) { - long result = - 0; // result is a long because we want to have a safepoint check at regular intervals - final int capacity = 4096; - do { - final int filled = fill(s, PortableJvmInfo.RECOMENDED_OFFER_BATCH); - if (filled == 0) { - return (int) result; - } - result += filled; - } while (result <= capacity); - return (int) result; + return fillUnbounded(this, s); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/PortableJvmInfo.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/PortableJvmInfo.java similarity index 90% rename from rsocket-core/src/main/java/io/rsocket/internal/jctools/util/PortableJvmInfo.java rename to rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/PortableJvmInfo.java index 2d567d60d..f037857e8 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/PortableJvmInfo.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/PortableJvmInfo.java @@ -11,11 +11,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.internal.jctools.util; +package io.rsocket.internal.jctools.queues; /** JVM Information that is standard and available on all JVMs (i.e. does not use unsafe) */ -@InternalAPI -public interface PortableJvmInfo { +interface PortableJvmInfo { int CACHE_LINE_SIZE = Integer.getInteger("jctools.cacheLineSize", 64); int CPUs = Runtime.getRuntime().availableProcessors(); int RECOMENDED_OFFER_BATCH = CPUs * 4; diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/Pow2.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/Pow2.java similarity index 96% rename from rsocket-core/src/main/java/io/rsocket/internal/jctools/util/Pow2.java rename to rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/Pow2.java index d8c66d89e..282a22f02 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/Pow2.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/Pow2.java @@ -11,11 +11,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.internal.jctools.util; +package io.rsocket.internal.jctools.queues; /** Power of 2 utility functions. */ -@InternalAPI -public final class Pow2 { +final class Pow2 { public static final int MAX_POW2 = 1 << 30; /** diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/RangeUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/RangeUtil.java similarity index 94% rename from rsocket-core/src/main/java/io/rsocket/internal/jctools/util/RangeUtil.java rename to rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/RangeUtil.java index 77a0582ca..3adcb2f3c 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/RangeUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/RangeUtil.java @@ -11,10 +11,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.internal.jctools.util; +package io.rsocket.internal.jctools.queues; -@InternalAPI -public final class RangeUtil { +final class RangeUtil { public static long checkPositive(long n, String name) { if (n <= 0) { throw new IllegalArgumentException(name + ": " + n + " (expected: > 0)"); diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/UnsafeAccess.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/UnsafeAccess.java old mode 100755 new mode 100644 similarity index 71% rename from rsocket-core/src/main/java/io/rsocket/internal/jctools/util/UnsafeAccess.java rename to rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/UnsafeAccess.java index 793e64505..c99aeb689 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/UnsafeAccess.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/UnsafeAccess.java @@ -11,7 +11,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.internal.jctools.util; +package io.rsocket.internal.jctools.queues; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -33,41 +33,56 @@ * * @author nitsanw */ -@InternalAPI -public class UnsafeAccess { - public static final boolean SUPPORTS_GET_AND_SET; +class UnsafeAccess { + public static final boolean SUPPORTS_GET_AND_SET_REF; + public static final boolean SUPPORTS_GET_AND_ADD_LONG; public static final Unsafe UNSAFE; static { + UNSAFE = getUnsafe(); + SUPPORTS_GET_AND_SET_REF = hasGetAndSetSupport(); + SUPPORTS_GET_AND_ADD_LONG = hasGetAndAddLongSupport(); + } + + private static Unsafe getUnsafe() { Unsafe instance; try { final Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); instance = (Unsafe) field.get(null); } catch (Exception ignored) { - // Some platforms, notably Android, might not have a sun.misc.Unsafe - // implementation with a private `theUnsafe` static instance. In this - // case we can try and call the default constructor, which proves - // sufficient for Android usage. + // Some platforms, notably Android, might not have a sun.misc.Unsafe implementation with a + // private + // `theUnsafe` static instance. In this case we can try to call the default constructor, which + // is sufficient + // for Android usage. try { Constructor c = Unsafe.class.getDeclaredConstructor(); c.setAccessible(true); instance = c.newInstance(); } catch (Exception e) { - SUPPORTS_GET_AND_SET = false; throw new RuntimeException(e); } } + return instance; + } - boolean getAndSetSupport = false; + private static boolean hasGetAndSetSupport() { try { Unsafe.class.getMethod("getAndSetObject", Object.class, Long.TYPE, Object.class); - getAndSetSupport = true; + return true; } catch (Exception ignored) { } + return false; + } - UNSAFE = instance; - SUPPORTS_GET_AND_SET = getAndSetSupport; + private static boolean hasGetAndAddLongSupport() { + try { + Unsafe.class.getMethod("getAndAddLong", Object.class, Long.TYPE, Long.TYPE); + return true; + } catch (Exception ignored) { + } + return false; } public static long fieldOffset(Class clz, String fieldName) throws RuntimeException { diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/UnsafeRefArrayAccess.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/UnsafeRefArrayAccess.java similarity index 57% rename from rsocket-core/src/main/java/io/rsocket/internal/jctools/util/UnsafeRefArrayAccess.java rename to rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/UnsafeRefArrayAccess.java index d8309c5c5..c734a9914 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/UnsafeRefArrayAccess.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/UnsafeRefArrayAccess.java @@ -11,32 +11,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.internal.jctools.util; +package io.rsocket.internal.jctools.queues; -import static io.rsocket.internal.jctools.util.UnsafeAccess.UNSAFE; +import static io.rsocket.internal.jctools.queues.UnsafeAccess.UNSAFE; -/** - * A concurrent access enabling class used by circular array based queues this class exposes an - * offset computation method along with differently memory fenced load/store methods into the - * underlying array. The class is pre-padded and the array is padded on either side to help with - * False sharing prvention. It is expected theat subclasses handle post padding. - * - *

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

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

    - * - * @author nitsanw - */ -@InternalAPI -public final class UnsafeRefArrayAccess { +final class UnsafeRefArrayAccess { public static final long REF_ARRAY_BASE; public static final int REF_ELEMENT_SHIFT; static { - final int scale = UnsafeAccess.UNSAFE.arrayIndexScale(Object[].class); + final int scale = UNSAFE.arrayIndexScale(Object[].class); if (4 == scale) { REF_ELEMENT_SHIFT = 2; } else if (8 == scale) { @@ -44,28 +28,28 @@ public final class UnsafeRefArrayAccess { } else { throw new IllegalStateException("Unknown pointer size: " + scale); } - REF_ARRAY_BASE = UnsafeAccess.UNSAFE.arrayBaseOffset(Object[].class); + REF_ARRAY_BASE = UNSAFE.arrayBaseOffset(Object[].class); } /** * A plain store (no ordering/fences) of an element to a given offset * * @param buffer this.buffer - * @param offset computed via {@link UnsafeRefArrayAccess#calcElementOffset(long)} + * @param offset computed via {@link UnsafeRefArrayAccess#calcRefElementOffset(long)} * @param e an orderly kitty */ - public static void spElement(E[] buffer, long offset, E e) { + public static void spRefElement(E[] buffer, long offset, E e) { UNSAFE.putObject(buffer, offset, e); } /** - * An ordered store(store + StoreStore barrier) of an element to a given offset + * An ordered store of an element to a given offset * * @param buffer this.buffer - * @param offset computed via {@link UnsafeRefArrayAccess#calcElementOffset} + * @param offset computed via {@link UnsafeRefArrayAccess#calcCircularRefElementOffset} * @param e an orderly kitty */ - public static void soElement(E[] buffer, long offset, E e) { + public static void soRefElement(E[] buffer, long offset, E e) { UNSAFE.putOrderedObject(buffer, offset, e); } @@ -73,31 +57,48 @@ public static void soElement(E[] buffer, long offset, E e) { * A plain load (no ordering/fences) of an element from a given offset. * * @param buffer this.buffer - * @param offset computed via {@link UnsafeRefArrayAccess#calcElementOffset(long)} + * @param offset computed via {@link UnsafeRefArrayAccess#calcRefElementOffset(long)} * @return the element at the offset */ @SuppressWarnings("unchecked") - public static E lpElement(E[] buffer, long offset) { + public static E lpRefElement(E[] buffer, long offset) { return (E) UNSAFE.getObject(buffer, offset); } /** - * A volatile load (load + LoadLoad barrier) of an element from a given offset. + * A volatile load of an element from a given offset. * * @param buffer this.buffer - * @param offset computed via {@link UnsafeRefArrayAccess#calcElementOffset(long)} + * @param offset computed via {@link UnsafeRefArrayAccess#calcRefElementOffset(long)} * @return the element at the offset */ @SuppressWarnings("unchecked") - public static E lvElement(E[] buffer, long offset) { + public static E lvRefElement(E[] buffer, long offset) { return (E) UNSAFE.getObjectVolatile(buffer, offset); } /** * @param index desirable element index - * @return the offset in bytes within the array for a given index. + * @return the offset in bytes within the array for a given index */ - public static long calcElementOffset(long index) { + public static long calcRefElementOffset(long index) { return REF_ARRAY_BASE + (index << REF_ELEMENT_SHIFT); } + + /** + * Note: circular arrays are assumed a power of 2 in length and the `mask` is (length - 1). + * + * @param index desirable element index + * @param mask (length - 1) + * @return the offset in bytes within the circular array for a given index + */ + public static long calcCircularRefElementOffset(long index, long mask) { + return REF_ARRAY_BASE + ((index & mask) << REF_ELEMENT_SHIFT); + } + + /** This makes for an easier time generating the atomic queues, and removes some warnings. */ + @SuppressWarnings("unchecked") + public static E[] allocateRefArray(int capacity) { + return (E[]) new Object[capacity]; + } } diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/InternalAPI.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/InternalAPI.java deleted file mode 100644 index f233e9597..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/InternalAPI.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.internal.jctools.util; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * This annotation marks classes and methods which may be public for any reason (to support better - * testing or reduce code duplication) but are not intended as public API and may change between - * releases without the change being considered a breaking API change (a major release). - */ -@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR}) -@Retention(RetentionPolicy.SOURCE) -public @interface InternalAPI {} From 8c928ec3645ab3f9b0e8c1386dab4be6e3978914 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 26 Dec 2020 15:25:18 +0200 Subject: [PATCH 065/183] cleanups redundant outputs Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java | 3 --- 1 file changed, 3 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 96d2720d1..04c9e4bff 100644 --- a/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java +++ b/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java @@ -80,11 +80,9 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { } if (!hasUnreleased) { - System.out.println(tag + " all the buffers are released..."); return this; } - System.out.println(tag + " await buffers to be released"); for (int i = 0; i < 100; i++) { System.gc(); parkNanos(1000); @@ -109,7 +107,6 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { return checkResult; }, tag); - System.out.println(tag + " all the buffers are released..."); } finally { tracker.clear(); } From eecbd6d383db058ed43d9832fef972ef3949e5b7 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 6 Jan 2021 13:29:13 +0200 Subject: [PATCH 066/183] cleanups examples Signed-off-by: Oleh Dokuka --- .../tcp/channel/ChannelEchoClient.java | 4 +-- .../tcp/client/RSocketClientExample.java | 2 +- .../plugins/LimitRateInterceptorExample.java | 3 +- .../tcp/requestresponse/HelloWorldClient.java | 3 +- .../tcp/resume/ResumeFileTransfer.java | 3 +- .../tcp/stream/ClientStreamingToServer.java | 32 +++++++++++-------- .../tcp/stream/ServerStreamingToClient.java | 3 +- 7 files changed, 24 insertions(+), 26 deletions(-) diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/channel/ChannelEchoClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/channel/ChannelEchoClient.java index b532c0140..463043020 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/channel/ChannelEchoClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/channel/ChannelEchoClient.java @@ -43,9 +43,7 @@ public static void main(String[] args) { .map(s -> "Echo: " + s) .map(DefaultPayload::create)); - RSocketServer.create(echoAcceptor) - .bind(TcpServerTransport.create("localhost", 7000)) - .subscribe(); + RSocketServer.create(echoAcceptor).bindNow(TcpServerTransport.create("localhost", 7000)); RSocket socket = RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000)).block(); diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/client/RSocketClientExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/client/RSocketClientExample.java index 2d19b9ce4..dfbbcde53 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/client/RSocketClientExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/client/RSocketClientExample.java @@ -34,7 +34,7 @@ public static void main(String[] args) { .bind(TcpServerTransport.create("localhost", 7000)) .delaySubscription(Duration.ofSeconds(5)) .doOnNext(cc -> logger.info("Server started on the address : {}", cc.address())) - .subscribe(); + .block(); Mono source = RSocketConnector.create() diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/plugins/LimitRateInterceptorExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/plugins/LimitRateInterceptorExample.java index 67a85b67f..5491a1aab 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/plugins/LimitRateInterceptorExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/plugins/LimitRateInterceptorExample.java @@ -39,8 +39,7 @@ public Flux requestChannel(Publisher payloads) { } })) .interceptors(registry -> registry.forResponder(LimitRateInterceptor.forResponder(64))) - .bind(TcpServerTransport.create("localhost", 7000)) - .subscribe(); + .bindNow(TcpServerTransport.create("localhost", 7000)); RSocket socket = RSocketConnector.create() diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/requestresponse/HelloWorldClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/requestresponse/HelloWorldClient.java index 85faeee82..0c372d2d8 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/requestresponse/HelloWorldClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/requestresponse/HelloWorldClient.java @@ -50,8 +50,7 @@ public Mono requestResponse(Payload p) { }; RSocketServer.create(SocketAcceptor.with(rsocket)) - .bind(TcpServerTransport.create("localhost", 7000)) - .subscribe(); + .bindNow(TcpServerTransport.create("localhost", 7000)); RSocket socket = RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000)).block(); diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java index fb2383755..ba82c7c93 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java @@ -66,8 +66,7 @@ public static void main(String[] args) { .log("server"); })) .resume(resume) - .bind(TcpServerTransport.create("localhost", 8000)) - .block(); + .bindNow(TcpServerTransport.create("localhost", 8000)); RSocket client = RSocketConnector.create() diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java index feacdbcfc..af0df3be1 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java @@ -16,12 +16,13 @@ package io.rsocket.examples.transport.tcp.stream; +import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.transport.netty.client.TcpClientTransport; -import io.rsocket.transport.netty.server.WebsocketServerTransport; +import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; import java.time.Duration; import org.slf4j.Logger; @@ -38,21 +39,24 @@ public static void main(String[] args) throws InterruptedException { payload -> Flux.interval(Duration.ofMillis(100)) .map(aLong -> DefaultPayload.create("Interval: " + aLong)))) - .bind(WebsocketServerTransport.create("localhost", 7000)) - .subscribe(); + .bindNow(TcpServerTransport.create("localhost", 7000)); RSocket socket = - RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000)).block(); - - // socket - // .requestStream(DefaultPayload.create("Hello")) - // .map(Payload::getDataUtf8) - // .doOnNext(logger::debug) - // .take(10) - // .then() - // .doFinally(signalType -> socket.dispose()) - // .then() - // .block(); + RSocketConnector.create() + .setupPayload(DefaultPayload.create("test", "test")) + .connect(TcpClientTransport.create("localhost", 7000)) + .block(); + + final Payload payload = DefaultPayload.create("Hello"); + socket + .requestStream(payload) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .doFinally(signalType -> socket.dispose()) + .then() + .block(); Thread.sleep(1000000); } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ServerStreamingToClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ServerStreamingToClient.java index f5b1e00e5..10ed34553 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ServerStreamingToClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ServerStreamingToClient.java @@ -43,8 +43,7 @@ public static void main(String[] args) { return Mono.just(new RSocket() {}); }) - .bind(TcpServerTransport.create("localhost", 7000)) - .subscribe(); + .bindNow(TcpServerTransport.create("localhost", 7000)); RSocket rsocket = RSocketConnector.create() From f2cfe20dba0fcd5a352ee14b6667239a1a6e75a9 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 6 Jan 2021 13:29:13 +0200 Subject: [PATCH 067/183] adds example of enabling websocket frame aggregation Signed-off-by: Oleh Dokuka --- .../ws/WebSocketAggregationSample.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketAggregationSample.java diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketAggregationSample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketAggregationSample.java new file mode 100644 index 000000000..89304853c --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketAggregationSample.java @@ -0,0 +1,80 @@ +/* + * Copyright 2015-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.examples.transport.ws; + +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.WebsocketDuplexConnection; +import io.rsocket.transport.netty.client.WebsocketClientTransport; +import io.rsocket.util.ByteBufPayload; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; +import reactor.netty.DisposableServer; +import reactor.netty.http.server.HttpServer; + +public class WebSocketAggregationSample { + + private static final Logger logger = LoggerFactory.getLogger(WebSocketAggregationSample.class); + + public static void main(String[] args) { + + ServerTransport.ConnectionAcceptor connectionAcceptor = + RSocketServer.create(SocketAcceptor.forRequestResponse(Mono::just)) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .asConnectionAcceptor(); + + DisposableServer server = + HttpServer.create() + .host("localhost") + .port(0) + .handle( + (req, res) -> + res.sendWebsocket( + (in, out) -> + connectionAcceptor + .apply( + new WebsocketDuplexConnection( + (Connection) in.aggregateFrames())) + .then(out.neverComplete()))) + .bindNow(); + + WebsocketClientTransport transport = + WebsocketClientTransport.create(server.host(), server.port()); + + RSocket clientRSocket = + RSocketConnector.create() + .keepAlive(Duration.ofMinutes(10), Duration.ofMinutes(10)) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(transport) + .block(); + + Flux.range(1, 100) + .concatMap(i -> clientRSocket.requestResponse(ByteBufPayload.create("Hello " + i))) + .doOnNext(payload -> logger.debug("Processed " + payload.getDataUtf8())) + .blockLast(); + clientRSocket.dispose(); + server.dispose(); + } +} From 4a64a7c898aec6e42e66a665a2538a4d72985739 Mon Sep 17 00:00:00 2001 From: Toshiaki Maki Date: Wed, 3 Feb 2021 03:44:46 +0900 Subject: [PATCH 068/183] updates sample code in RSocketConnector Javadoc (#977) Signed-off-by: Toshiaki Maki --- .../main/java/io/rsocket/core/RSocketConnector.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index 342fd9480..1c4e66477 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -61,18 +61,20 @@ *

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

    To customize connection settings before connecting: * *

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

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

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

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

    Completion * *

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

    Error * diff --git a/rsocket-core/src/main/java/io/rsocket/core/LeasePermitHandler.java b/rsocket-core/src/main/java/io/rsocket/core/LeasePermitHandler.java new file mode 100644 index 000000000..03ab7c257 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/LeasePermitHandler.java @@ -0,0 +1,20 @@ +package io.rsocket.core; + +/** Handler which enables async lease permits issuing */ +interface LeasePermitHandler { + + /** + * Called by {@link RequesterLeaseTracker} when there is an available lease + * + * @return {@code true} to indicate that lease permit was consumed successfully + */ + boolean handlePermit(); + + /** + * Called by {@link RequesterLeaseTracker} when there are no lease permit available at the moment + * and the list of awaiting {@link LeasePermitHandler} reached the configured limit + * + * @param t associated lease permit rejection exception + */ + void handlePermitError(Throwable t); +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/LeaseSpec.java b/rsocket-core/src/main/java/io/rsocket/core/LeaseSpec.java new file mode 100644 index 000000000..3947f296a --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/LeaseSpec.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import io.rsocket.lease.LeaseSender; +import reactor.core.publisher.Flux; + +public final class LeaseSpec { + + LeaseSender sender = Flux::never; + int maxPendingRequests = 256; + + LeaseSpec() {} + + public LeaseSpec sender(LeaseSender sender) { + this.sender = sender; + return this; + } + + /** + * Setup the maximum queued requests waiting for lease to be available. The default value is 256 + * + * @param maxPendingRequests if set to 0 the requester will terminate the request immediately if + * no leases is available + */ + public LeaseSpec maxPendingRequests(int maxPendingRequests) { + this.maxPendingRequests = 0; + return this; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index 1c4e66477..fe91cdb6f 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -29,13 +29,11 @@ import io.rsocket.frame.SetupFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.keepalive.KeepAliveHandler; -import io.rsocket.lease.LeaseStats; -import io.rsocket.lease.Leases; -import io.rsocket.lease.RequesterLeaseHandler; -import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.lease.TrackingLeaseSender; import io.rsocket.plugins.DuplexConnectionInterceptor; import io.rsocket.plugins.InitializingInterceptorRegistry; import io.rsocket.plugins.InterceptorRegistry; +import io.rsocket.plugins.RequestInterceptor; import io.rsocket.resume.ClientRSocketSession; import io.rsocket.resume.ResumableDuplexConnection; import io.rsocket.resume.ResumableFramesStore; @@ -94,7 +92,8 @@ public class RSocketConnector { private Retry retrySpec; private Resume resume; - private Supplier> leasesSupplier; + + @Nullable private Consumer leaseConfigurer; private int mtu = 0; private int maxInboundPayloadSize = Integer.MAX_VALUE; @@ -403,18 +402,43 @@ public RSocketConnector resume(Resume resume) { * *

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

    By default this is not enabled. * - * @param supplier supplier for a {@link Leases} * @return the same instance for method chaining * @see Lease * Semantics */ - public RSocketConnector lease(Supplier> supplier) { - this.leasesSupplier = supplier; + public RSocketConnector lease() { + return lease((config -> {})); + } + + /** + * Enables the Lease feature of the RSocket protocol where the number of requests that can be + * performed from either side are rationed via {@code LEASE} frames from the responder side. + * + *

    Example usage: + * + *

    {@code
    +   * Mono rocketMono =
    +   *         RSocketConnector.create()
    +   *                         .lease(spec -> spec.maxPendingRequests(128))
    +   *                         .connect(transport);
    +   * }
    + * + *

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

    {@code
        * RSocketServer.create(SocketAcceptor.with(new RSocket() {...}))
    -   *         .lease(Leases::new)
    +   *         .lease(spec ->
    +   *            spec.sender(() -> Flux.interval(ofSeconds(1))
    +   *                                  .map(__ -> Lease.create(ofSeconds(1), 1)))
    +   *         )
        *         .bind(TcpServerTransport.create("localhost", 7000))
        *         .subscribe();
        * }
    * *

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

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

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

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

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

    This larger representation encodes the mime type representation's length on a single byte, + * then the representation itself. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer + * @param customMimeType a custom mime type to encode + * @return the encoded mime type + */ + private static ByteBuf encodeCustomMimeType(ByteBufAllocator allocator, String customMimeType) { + ByteBuf mime = allocator.buffer(1 + customMimeType.length()); + // reserve 1 byte for the customMime length + // /!\ careful not to read that first byte, which is random at this point + mime.writerIndex(1); + + // write the custom mime in UTF8 but validate it is all ASCII-compatible + // (which produces the right result since ASCII chars are still encoded on 1 byte in UTF8) + int customMimeLength = ByteBufUtil.writeUtf8(mime, customMimeType); + if (!ByteBufUtil.isText(mime, mime.readerIndex() + 1, customMimeLength, CharsetUtil.US_ASCII)) { + mime.release(); + throw new IllegalArgumentException("custom mime type must be US_ASCII characters only"); + } + if (customMimeLength < 1 || customMimeLength > 128) { + mime.release(); + throw new IllegalArgumentException( + "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + mime.markWriterIndex(); + // go back to beginning and write the length + // encoded length is one less than actual length, since 0 is never a valid length, which gives + // wider representation range + mime.writerIndex(0); + mime.writeByte(customMimeLength - 1); + + // go back to post-mime type + mime.resetWriterIndex(); + return mime; + } + + /** + * Decode mime types from a {@link ByteBuf} that contains at least enough bytes for one mime type. + * + * @return decoded mime types + */ + public static List decode(ByteBuf buf) { + List mimeTypes = new ArrayList<>(); + while (buf.isReadable()) { + byte mimeIdOrLength = buf.readByte(); + if ((mimeIdOrLength & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK) { + byte mimeIdentifier = (byte) (mimeIdOrLength & STREAM_METADATA_LENGTH_MASK); + mimeTypes.add(WellKnownMimeType.fromIdentifier(mimeIdentifier).toString()); + } else { + int mimeLen = Byte.toUnsignedInt(mimeIdOrLength) + 1; + mimeTypes.add(buf.readCharSequence(mimeLen, CharsetUtil.US_ASCII).toString()); + } + } + return mimeTypes; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/MimeMetadataCodecTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/MimeMetadataCodecTest.java new file mode 100644 index 000000000..01bc11ab2 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/MimeMetadataCodecTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.assertj.core.util.Lists; +import org.junit.Test; + +public class MimeMetadataCodecTest { + + @Test + public void customMimeType() { + String customMimeType = "aaa/bb"; + ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, customMimeType); + List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); + Assertions.assertThat(mimeTypes.size()).isEqualTo(1); + Assertions.assertThat(customMimeType).isEqualTo(mimeTypes.get(0)); + } + + @Test + public void wellKnowMimeType() { + WellKnownMimeType wellKnownMimeType = WellKnownMimeType.APPLICATION_HESSIAN; + ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, wellKnownMimeType); + List mimes = MimeTypeMetadataCodec.decode(byteBuf); + Assertions.assertThat(mimes.size()).isEqualTo(1); + Assertions.assertThat(wellKnownMimeType).isEqualTo(WellKnownMimeType.fromString(mimes.get(0))); + } + + @Test + public void multipleAndMixMimeType() { + List mimeTypes = + Lists.newArrayList("aaa/bbb", WellKnownMimeType.APPLICATION_HESSIAN.getString()); + ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, mimeTypes); + List decodedMimeTypes = MimeTypeMetadataCodec.decode(byteBuf); + Assertions.assertThat(decodedMimeTypes).isEqualTo(mimeTypes); + } +} From c80b3cb6437046f3ccc79136a66299448f58c561 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 26 Mar 2021 10:36:07 +0000 Subject: [PATCH 088/183] Polishing contribution Closes gh-998 Signed-off-by: Rossen Stoyanchev --- .../metadata/MimeTypeMetadataCodec.java | 112 ++++++++---------- ...st.java => MimeTypeMetadataCodecTest.java} | 39 +++--- 2 files changed, 72 insertions(+), 79 deletions(-) rename rsocket-core/src/test/java/io/rsocket/metadata/{MimeMetadataCodecTest.java => MimeTypeMetadataCodecTest.java} (55%) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/MimeTypeMetadataCodec.java b/rsocket-core/src/main/java/io/rsocket/metadata/MimeTypeMetadataCodec.java index 16203e9f5..2e03bd754 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/MimeTypeMetadataCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/MimeTypeMetadataCodec.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import java.util.List; /** - * Provides support for encoding and/or decoding the per-stream MIME type to use for payload data. + * Provides support for encoding and decoding the per-stream MIME type to use for payload data. * *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    The implementation keeps the order of signals. */ -public final class UnboundedProcessor extends FluxProcessor - implements Fuseable.QueueSubscription, Fuseable { +public final class UnboundedProcessor extends Flux + implements Scannable, + Disposable, + CoreSubscriber, + Fuseable.QueueSubscription, + Fuseable { final Queue queue; final Queue priorityQueue; @@ -98,11 +103,6 @@ public UnboundedProcessor(Runnable onFinalizedHook) { this.priorityQueue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); } - @Override - public int getBufferSize() { - return Integer.MAX_VALUE; - } - @Override public Stream inners() { return hasDownstreams() ? Stream.of(Scannable.from(this.actual)) : Stream.empty(); @@ -118,7 +118,7 @@ public Object scanUnsafe(Attr key) { return isCancelled(state) || isDisposed(state); } - return super.scanUnsafe(key); + return null; } public void onNextPrioritized(ByteBuf t) { @@ -622,30 +622,7 @@ public boolean isDisposed() { return isFinalized(this.state); } - @Override - public boolean isTerminated() { - return this.done || isTerminated(this.state); - } - - @Override - @Nullable - public Throwable getError() { - //noinspection unused - final long state = this.state; - if (this.done) { - return this.error; - } else { - return null; - } - } - - @Override - public long downstreamCount() { - return hasDownstreams() ? 1L : 0L; - } - - @Override - public boolean hasDownstreams() { + boolean hasDownstreams() { final long state = this.state; return !isTerminated(state) && isSubscriberReady(state); } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java index 7ade3e59b..933ac09ca 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -253,7 +253,7 @@ void dispose(@Nullable Throwable e) { framesSaverDisposable.dispose(); activeReceivingSubscriber.dispose(); - savableFramesSender.onComplete(); + savableFramesSender.dispose(); onConnectionClosedSink.tryEmitComplete(); if (e != null) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index 6a1d07a67..327172255 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -1265,7 +1265,7 @@ void reassembleMetadata( .then(() -> rule.connection.addToReceivedBuffer(fragments.toArray(new ByteBuf[0]))) .assertNext( responsePayload -> { - PayloadAssert.assertThat(requestPayload).isEqualTo(metadataOnlyPayload).hasNoLeaks(); + PayloadAssert.assertThat(responsePayload).isEqualTo(metadataOnlyPayload).hasNoLeaks(); metadataOnlyPayload.release(); }) .thenCancel() diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java b/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java index 75aa2a5b2..b12d72b51 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java @@ -18,9 +18,18 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.util.IllegalReferenceCountException; +import org.assertj.core.api.Assertions; import org.assertj.core.presentation.StandardRepresentation; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; -public final class ByteBufRepresentation extends StandardRepresentation { +public final class ByteBufRepresentation extends StandardRepresentation + implements BeforeAllCallback { + + @Override + public void beforeAll(ExtensionContext context) { + Assertions.useRepresentation(this); + } @Override protected String fallbackToStringOf(Object object) { diff --git a/rsocket-core/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/rsocket-core/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 000000000..2b51ba0de --- /dev/null +++ b/rsocket-core/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +io.rsocket.frame.ByteBufRepresentation \ No newline at end of file diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/BaseWebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/BaseWebsocketServerTransport.java index 5f04eb575..33cff28b4 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/BaseWebsocketServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/BaseWebsocketServerTransport.java @@ -24,10 +24,7 @@ abstract class BaseWebsocketServerTransport< private static final ChannelHandler pongHandler = new PongHandler(); static Function serverConfigurer = - server -> - server.tcpConfiguration( - tcpServer -> - tcpServer.doOnConnection(connection -> connection.addHandlerLast(pongHandler))); + server -> server.doOnConnection(connection -> connection.addHandlerLast(pongHandler)); final WebsocketServerSpec.Builder specBuilder = WebsocketServerSpec.builder().maxFramePayloadLength(FRAME_LENGTH_MASK); diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java index b9b6201b8..540076704 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java @@ -41,7 +41,7 @@ public void testThatSetupWithUnSpecifiedFrameSizeShouldSetMaxFrameSize() { ArgumentCaptor httpHandlerCaptor = ArgumentCaptor.forClass(BiFunction.class); HttpServer server = Mockito.spy(HttpServer.create()); Mockito.doAnswer(a -> server).when(server).handle(httpHandlerCaptor.capture()); - Mockito.doAnswer(a -> server).when(server).tcpConfiguration(any()); + Mockito.doAnswer(a -> server).when(server).doOnConnection(any()); Mockito.doAnswer(a -> Mono.empty()).when(server).bind(); WebsocketServerTransport serverTransport = WebsocketServerTransport.create(server); From d8cccbe4e69e5ff358b0f75321fc7589e693dff9 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Fri, 25 Mar 2022 11:36:42 +0200 Subject: [PATCH 131/183] adds routing example with TaggingMetadata and CompositeMetadata (#1021) --- .../routing/CompositeMetadataExample.java | 102 ++++++++++++++++++ .../routing/RoutingMetadataExample.java | 83 ++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/CompositeMetadataExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/RoutingMetadataExample.java diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/CompositeMetadataExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/CompositeMetadataExample.java new file mode 100644 index 000000000..a0a02a946 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/CompositeMetadataExample.java @@ -0,0 +1,102 @@ +/* + * Copyright 2015-Present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.examples.transport.tcp.metadata.routing; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.metadata.CompositeMetadata; +import io.rsocket.metadata.CompositeMetadataCodec; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TaggingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.ByteBufPayload; +import java.util.Collections; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +public class CompositeMetadataExample { + static final Logger logger = LoggerFactory.getLogger(CompositeMetadataExample.class); + + public static void main(String[] args) { + RSocketServer.create( + SocketAcceptor.forRequestResponse( + payload -> { + final String route = decodeRoute(payload.sliceMetadata()); + + logger.info("Received RequestResponse[route={}]", route); + + payload.release(); + + if ("my.test.route".equals(route)) { + return Mono.just(ByteBufPayload.create("Hello From My Test Route")); + } + + return Mono.error(new IllegalArgumentException("Route " + route + " not found")); + })) + .bindNow(TcpServerTransport.create("localhost", 7000)); + + RSocket socket = + RSocketConnector.create() + // here we specify that every metadata payload will be encoded using + // CompositeMetadata layout as specified in the following subspec + // https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md + .metadataMimeType(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString()) + .connect(TcpClientTransport.create("localhost", 7000)) + .block(); + + final ByteBuf routeMetadata = + TaggingMetadataCodec.createTaggingContent( + ByteBufAllocator.DEFAULT, Collections.singletonList("my.test.route")); + final CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataCodec.encodeAndAddMetadata( + compositeMetadata, + ByteBufAllocator.DEFAULT, + WellKnownMimeType.MESSAGE_RSOCKET_ROUTING, + routeMetadata); + + socket + .requestResponse( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), compositeMetadata)) + .log() + .block(); + } + + static String decodeRoute(ByteBuf metadata) { + final CompositeMetadata compositeMetadata = new CompositeMetadata(metadata, false); + + for (CompositeMetadata.Entry metadatum : compositeMetadata) { + if (Objects.requireNonNull(metadatum.getMimeType()) + .equals(WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString())) { + return new RoutingMetadata(metadatum.getContent()).iterator().next(); + } + } + + return null; + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/RoutingMetadataExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/RoutingMetadataExample.java new file mode 100644 index 000000000..2aee18bf9 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/RoutingMetadataExample.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015-Present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.examples.transport.tcp.metadata.routing; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TaggingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.ByteBufPayload; +import java.util.Collections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +public class RoutingMetadataExample { + static final Logger logger = LoggerFactory.getLogger(RoutingMetadataExample.class); + + public static void main(String[] args) { + RSocketServer.create( + SocketAcceptor.forRequestResponse( + payload -> { + final String route = decodeRoute(payload.sliceMetadata()); + + logger.info("Received RequestResponse[route={}]", route); + + payload.release(); + + if ("my.test.route".equals(route)) { + return Mono.just(ByteBufPayload.create("Hello From My Test Route")); + } + + return Mono.error(new IllegalArgumentException("Route " + route + " not found")); + })) + .bindNow(TcpServerTransport.create("localhost", 7000)); + + RSocket socket = + RSocketConnector.create() + // here we specify that route will be encoded using + // Routing&Tagging Metadata layout specified at this + // subspec https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md + .metadataMimeType(WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()) + .connect(TcpClientTransport.create("localhost", 7000)) + .block(); + + final ByteBuf routeMetadata = + TaggingMetadataCodec.createTaggingContent( + ByteBufAllocator.DEFAULT, Collections.singletonList("my.test.route")); + socket + .requestResponse( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), routeMetadata)) + .log() + .block(); + } + + static String decodeRoute(ByteBuf metadata) { + final RoutingMetadata routingMetadata = new RoutingMetadata(metadata); + + return routingMetadata.iterator().next(); + } +} From 80a05f873c21f4e557d1980b1734c8a18fea411f Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 26 Mar 2022 11:03:04 +0200 Subject: [PATCH 132/183] adds jdk 17 instead of 16 in build matrix Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .github/workflows/gradle-all.yml | 10 +++++----- .github/workflows/gradle-main.yml | 10 +++++----- .github/workflows/gradle-pr.yml | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/gradle-all.yml b/.github/workflows/gradle-all.yml index 8826f511a..f2df20620 100644 --- a/.github/workflows/gradle-all.yml +++ b/.github/workflows/gradle-all.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -42,7 +42,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -69,7 +69,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -96,7 +96,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -123,7 +123,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: diff --git a/.github/workflows/gradle-main.yml b/.github/workflows/gradle-main.yml index 34d3e65f0..904c45fb7 100644 --- a/.github/workflows/gradle-main.yml +++ b/.github/workflows/gradle-main.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -42,7 +42,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -69,7 +69,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -96,7 +96,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -123,7 +123,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: diff --git a/.github/workflows/gradle-pr.yml b/.github/workflows/gradle-pr.yml index fd88ad76f..cecca085f 100644 --- a/.github/workflows/gradle-pr.yml +++ b/.github/workflows/gradle-pr.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -36,7 +36,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -63,7 +63,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: @@ -90,7 +90,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - jdk: [ 1.8, 11, 16 ] + jdk: [ 1.8, 11, 17 ] fail-fast: false steps: From 9a504a882a6d43bb26298660c5b0b7a2f71caeb2 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Mon, 28 Mar 2022 18:16:07 +0300 Subject: [PATCH 133/183] fixes `block()` in MetadataPushRequesterMono/FnfRequesterMono (#1044) --- .../core/FireAndForgetRequesterMono.java | 5 ++ .../core/MetadataPushRequesterMono.java | 12 +++- .../io/rsocket/core/RSocketRequesterTest.java | 63 ++++++++++++++++++- 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java b/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java index eceb0976c..a5d527f5c 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java +++ b/rsocket-core/src/main/java/io/rsocket/core/FireAndForgetRequesterMono.java @@ -185,6 +185,11 @@ public Void block(Duration m) { return block(); } + /** + * This method is deliberately non-blocking regardless it is named as `.block`. The main intent to + * keep this method along with the {@link #subscribe()} is to eliminate redundancy which comes + * with a default block method implementation. + */ @Override @Nullable public Void block() { diff --git a/rsocket-core/src/main/java/io/rsocket/core/MetadataPushRequesterMono.java b/rsocket-core/src/main/java/io/rsocket/core/MetadataPushRequesterMono.java index 226e9a0af..e2512e995 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/MetadataPushRequesterMono.java +++ b/rsocket-core/src/main/java/io/rsocket/core/MetadataPushRequesterMono.java @@ -120,6 +120,11 @@ public Void block(Duration m) { return block(); } + /** + * This method is deliberately non-blocking regardless it is named as `.block`. The main intent to + * keep this method along with the {@link #subscribe()} is to eliminate redundancy which comes + * with a default block method implementation. + */ @Override @Nullable public Void block() { @@ -133,15 +138,16 @@ public Void block() { try { final boolean hasMetadata = p.hasMetadata(); metadata = p.metadata(); - if (hasMetadata) { + if (!hasMetadata) { lazyTerminate(STATE, this); p.release(); - throw new IllegalArgumentException("Metadata push does not support metadata field"); + throw new IllegalArgumentException("Metadata push should have metadata field present"); } if (!isValidMetadata(this.maxFrameLength, metadata)) { lazyTerminate(STATE, this); p.release(); - throw new IllegalArgumentException("Too Big Payload size"); + throw new IllegalArgumentException( + String.format(INVALID_PAYLOAD_ERROR_MESSAGE, this.maxFrameLength)); } } catch (IllegalReferenceCountException e) { lazyTerminate(STATE, this); diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index 327172255..183785d2f 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -33,6 +33,7 @@ import static io.rsocket.frame.FrameType.REQUEST_RESPONSE; import static io.rsocket.frame.FrameType.REQUEST_STREAM; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @@ -81,7 +82,6 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Stream; -import org.assertj.core.api.Assertions; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -169,7 +169,7 @@ protected void hookOnSubscribe(Subscription subscription) { public void testHandleSetupException() { rule.connection.addToReceivedBuffer( ErrorFrameCodec.encode(rule.alloc(), 0, new RejectedSetupException("boom"))); - Assertions.assertThatThrownBy(() -> rule.socket.onClose().block()) + assertThatThrownBy(() -> rule.socket.onClose().block()) .isInstanceOf(RejectedSetupException.class); rule.assertHasNoLeaks(); } @@ -373,6 +373,65 @@ public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmen }); } + @ParameterizedTest + @ValueSource(ints = {128, 256, FRAME_LENGTH_MASK}) + public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmentation1( + int maxFrameLength) { + rule.setMaxFrameLength(maxFrameLength); + prepareCalls() + .forEach( + generator -> { + byte[] metadata = new byte[maxFrameLength]; + byte[] data = new byte[maxFrameLength]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + + assertThatThrownBy( + () -> { + final Publisher source = + generator.apply(rule.socket, DefaultPayload.create(data, metadata)); + + if (source instanceof Mono) { + ((Mono) source).block(); + } else { + ((Flux) source).blockLast(); + } + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(String.format(INVALID_PAYLOAD_ERROR_MESSAGE, maxFrameLength)); + + rule.assertHasNoLeaks(); + }); + } + + @Test + public void shouldRejectCallOfNoMetadataPayload() { + final ByteBuf data = rule.allocator.buffer(10); + final Payload payload = ByteBufPayload.create(data); + StepVerifier.create(rule.socket.metadataPush(payload)) + .expectSubscription() + .expectErrorSatisfies( + t -> + assertThat(t) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metadata push should have metadata field present")) + .verify(); + PayloadAssert.assertThat(payload).isReleased(); + rule.assertHasNoLeaks(); + } + + @Test + public void shouldRejectCallOfNoMetadataPayloadBlocking() { + final ByteBuf data = rule.allocator.buffer(10); + final Payload payload = ByteBufPayload.create(data); + + assertThatThrownBy(() -> rule.socket.metadataPush(payload).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metadata push should have metadata field present"); + PayloadAssert.assertThat(payload).isReleased(); + rule.assertHasNoLeaks(); + } + static Stream>> prepareCalls() { return Stream.of( RSocket::fireAndForget, From 571af15e2c9c6c89964113d2e0c6b528ffc662d5 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 28 Mar 2022 18:39:29 +0300 Subject: [PATCH 134/183] eliminates dropped error Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .../io/rsocket/resume/ResumableDuplexConnection.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java index 933ac09ca..f061857ff 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -192,7 +192,8 @@ public void sendErrorAndClose(RSocketErrorException rSocketErrorException) { t -> { framesSaverDisposable.dispose(); activeReceivingSubscriber.dispose(); - savableFramesSender.dispose(); + savableFramesSender.onComplete(); + savableFramesSender.cancel(); onConnectionClosedSink.tryEmitComplete(); onClose.tryEmitError(t); @@ -200,7 +201,8 @@ public void sendErrorAndClose(RSocketErrorException rSocketErrorException) { () -> { framesSaverDisposable.dispose(); activeReceivingSubscriber.dispose(); - savableFramesSender.dispose(); + savableFramesSender.onComplete(); + savableFramesSender.cancel(); onConnectionClosedSink.tryEmitComplete(); final Throwable cause = rSocketErrorException.getCause(); @@ -253,7 +255,8 @@ void dispose(@Nullable Throwable e) { framesSaverDisposable.dispose(); activeReceivingSubscriber.dispose(); - savableFramesSender.dispose(); + savableFramesSender.onComplete(); + savableFramesSender.cancel(); onConnectionClosedSink.tryEmitComplete(); if (e != null) { From 40c1dbd249072b69be92fc17c4d67a3cce3c4e9c Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Sat, 6 Aug 2022 14:27:46 +0300 Subject: [PATCH 135/183] adds Micrometer Observation API integration (#1056) Co-authored-by: Marcin Grzejszczak --- build.gradle | 35 ++- gradle.properties | 2 +- rsocket-examples/build.gradle | 7 + .../ObservationIntegrationTest.java | 236 ++++++++++++++++++ rsocket-micrometer/build.gradle | 2 + .../micrometer/observation/ByteBufGetter.java | 36 +++ .../micrometer/observation/ByteBufSetter.java | 33 +++ .../observation/CompositeMetadataUtils.java | 40 +++ .../DefaultRSocketObservationConvention.java | 49 ++++ ...RSocketRequesterObservationConvention.java | 62 +++++ ...RSocketResponderObservationConvention.java | 61 +++++ .../ObservationRequesterRSocketProxy.java | 224 +++++++++++++++++ .../ObservationResponderRSocketProxy.java | 167 +++++++++++++ .../micrometer/observation/PayloadUtils.java | 73 ++++++ .../observation/RSocketContext.java | 76 ++++++ .../RSocketDocumentedObservation.java | 231 +++++++++++++++++ ...RSocketRequesterObservationConvention.java | 35 +++ ...ketRequesterTracingObservationHandler.java | 124 +++++++++ ...RSocketResponderObservationConvention.java | 35 +++ ...ketResponderTracingObservationHandler.java | 149 +++++++++++ 20 files changed, 1675 insertions(+), 2 deletions(-) create mode 100644 rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufGetter.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufSetter.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/CompositeMetadataUtils.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/PayloadUtils.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketContext.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java create mode 100644 rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java diff --git a/build.gradle b/build.gradle index a3ff46a4f..2e890e032 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,8 @@ subprojects { ext['slf4j.version'] = '1.7.36' ext['jmh.version'] = '1.33' ext['junit.version'] = '5.8.1' - ext['micrometer.version'] = '1.8.4' + ext['micrometer.version'] = '1.10.0-SNAPSHOT' + ext['micrometer-tracing.version'] = '1.0.0-SNAPSHOT' ext['assertj.version'] = '3.22.0' ext['netflix.limits.version'] = '0.3.6' ext['bouncycastle-bcpkix.version'] = '1.70' @@ -77,6 +78,10 @@ subprojects { dependency "io.netty:netty-tcnative-boringssl-static:${ext['netty-boringssl.version']}" dependency "org.bouncycastle:bcpkix-jdk15on:${ext['bouncycastle-bcpkix.version']}" dependency "io.micrometer:micrometer-core:${ext['micrometer.version']}" + dependency "io.micrometer:micrometer-observation:${ext['micrometer.version']}" + dependency "io.micrometer:micrometer-test:${ext['micrometer.version']}" + dependency "io.micrometer:micrometer-tracing:${ext['micrometer-tracing.version']}" + dependency "io.micrometer:micrometer-tracing-integration-test:${ext['micrometer-tracing.version']}" dependency "org.assertj:assertj-core:${ext['assertj.version']}" dependency "org.hdrhistogram:HdrHistogram:${ext['hdrhistogram.version']}" dependency "org.slf4j:slf4j-api:${ext['slf4j.version']}" @@ -117,6 +122,7 @@ subprojects { if (version.endsWith('SNAPSHOT') || project.hasProperty('versionSuffix')) { maven { url 'https://repo.spring.io/libs-snapshot' } maven { url 'https://oss.jfrog.org/artifactory/oss-snapshot-local' } + mavenLocal() } } @@ -256,4 +262,31 @@ description = 'RSocket: Stream Oriented Messaging Passing with Reactive Stream S repositories { mavenCentral() + + maven { url 'https://repo.spring.io/snapshot' } + mavenLocal() +} + +configurations { + adoc +} + +dependencies { + adoc "io.micrometer:micrometer-docs-generator-spans:1.0.0-SNAPSHOT" + adoc "io.micrometer:micrometer-docs-generator-metrics:1.0.0-SNAPSHOT" +} + +task generateObservabilityDocs(dependsOn: ["generateObservabilityMetricsDocs", "generateObservabilitySpansDocs"]) { +} + +task generateObservabilityMetricsDocs(type: JavaExec) { + mainClass = "io.micrometer.docs.metrics.DocsFromSources" + classpath configurations.adoc + args project.rootDir.getAbsolutePath(), ".*", project.rootProject.buildDir.getAbsolutePath() +} + +task generateObservabilitySpansDocs(type: JavaExec) { + mainClass = "io.micrometer.docs.spans.DocsFromSources" + classpath configurations.adoc + args project.rootDir.getAbsolutePath(), ".*", project.rootProject.buildDir.getAbsolutePath() } diff --git a/gradle.properties b/gradle.properties index e9219dfe6..3b8caafcc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.1.2 +version=1.2.0-SNAPSHOT perfBaselineVersion=1.1.1 diff --git a/rsocket-examples/build.gradle b/rsocket-examples/build.gradle index d03524cd9..e339b170a 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -24,6 +24,11 @@ dependencies { implementation project(':rsocket-transport-local') implementation project(':rsocket-transport-netty') + implementation "io.micrometer:micrometer-core" + implementation "io.micrometer:micrometer-tracing" + implementation project(":rsocket-micrometer") + testImplementation 'org.awaitility:awaitility' + implementation 'com.netflix.concurrency-limits:concurrency-limits-core' runtimeOnly 'ch.qos.logback:logback-classic' @@ -33,6 +38,8 @@ dependencies { testImplementation 'org.mockito:mockito-core' testImplementation 'org.assertj:assertj-core' testImplementation 'io.projectreactor:reactor-test' + testImplementation "io.micrometer:micrometer-test" + testImplementation "io.micrometer:micrometer-tracing-integration-test" testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' } diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java new file mode 100644 index 000000000..2bf5e42e7 --- /dev/null +++ b/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java @@ -0,0 +1,236 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.integration.observation; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.core.tck.MeterRegistryAssert; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.test.SampleTestRunner; +import io.micrometer.tracing.test.reporter.BuildingBlocks; +import io.micrometer.tracing.test.simple.SpansAssert; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.micrometer.observation.ByteBufGetter; +import io.rsocket.micrometer.observation.ByteBufSetter; +import io.rsocket.micrometer.observation.ObservationRequesterRSocketProxy; +import io.rsocket.micrometer.observation.ObservationResponderRSocketProxy; +import io.rsocket.micrometer.observation.RSocketRequesterTracingObservationHandler; +import io.rsocket.micrometer.observation.RSocketResponderTracingObservationHandler; +import io.rsocket.plugins.RSocketInterceptor; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.Deque; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class ObservationIntegrationTest extends SampleTestRunner { + private static final MeterRegistry registry = new SimpleMeterRegistry(); + private static final ObservationRegistry observationRegistry = ObservationRegistry.create(); + + static { + observationRegistry + .observationConfig() + .observationHandler(new DefaultMeterObservationHandler(registry)); + } + + private final RSocketInterceptor requesterInterceptor; + private final RSocketInterceptor responderInterceptor; + + ObservationIntegrationTest() { + super(SampleRunnerConfig.builder().build(), observationRegistry, registry); + requesterInterceptor = + reactiveSocket -> new ObservationRequesterRSocketProxy(reactiveSocket, observationRegistry); + + responderInterceptor = + reactiveSocket -> new ObservationResponderRSocketProxy(reactiveSocket, observationRegistry); + } + + private CloseableChannel server; + private RSocket client; + private AtomicInteger counter; + + @Override + public BiConsumer>> + customizeObservationHandlers() { + return (buildingBlocks, observationHandlers) -> { + observationHandlers.addFirst( + new RSocketRequesterTracingObservationHandler( + buildingBlocks.getTracer(), + buildingBlocks.getPropagator(), + new ByteBufSetter(), + false)); + observationHandlers.addFirst( + new RSocketResponderTracingObservationHandler( + buildingBlocks.getTracer(), + buildingBlocks.getPropagator(), + new ByteBufGetter(), + false)); + }; + } + + @AfterEach + public void teardown() { + if (server != null) { + server.dispose(); + } + } + + private void testRequest() { + counter.set(0); + client.requestResponse(DefaultPayload.create("REQUEST", "META")).block(); + assertThat(counter).as("Server did not see the request.").hasValue(1); + } + + private void testStream() { + counter.set(0); + client.requestStream(DefaultPayload.create("start")).blockLast(); + + assertThat(counter).as("Server did not see the request.").hasValue(1); + } + + private void testRequestChannel() { + counter.set(0); + client.requestChannel(Mono.just(DefaultPayload.create("start"))).blockFirst(); + assertThat(counter).as("Server did not see the request.").hasValue(1); + } + + private void testFireAndForget() { + counter.set(0); + client.fireAndForget(DefaultPayload.create("start")).subscribe(); + Awaitility.await().atMost(Duration.ofSeconds(50)).until(() -> counter.get() == 1); + assertThat(counter).as("Server did not see the request.").hasValue(1); + } + + @Override + public SampleTestRunnerConsumer yourCode() { + return (bb, meterRegistry) -> { + counter = new AtomicInteger(); + server = + RSocketServer.create( + (setup, sendingSocket) -> { + sendingSocket.onClose().subscribe(); + + return Mono.just( + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + payload.release(); + counter.incrementAndGet(); + return Mono.just(DefaultPayload.create("RESPONSE", "METADATA")); + } + + @Override + public Flux requestStream(Payload payload) { + payload.release(); + counter.incrementAndGet(); + return Flux.range(1, 10_000) + .map(i -> DefaultPayload.create("data -> " + i)); + } + + @Override + public Flux requestChannel(Publisher payloads) { + counter.incrementAndGet(); + return Flux.from(payloads); + } + + @Override + public Mono fireAndForget(Payload payload) { + payload.release(); + counter.incrementAndGet(); + return Mono.empty(); + } + }); + }) + .interceptors(registry -> registry.forResponder(responderInterceptor)) + .bind(TcpServerTransport.create("localhost", 0)) + .block(); + + client = + RSocketConnector.create() + .interceptors(registry -> registry.forRequester(requesterInterceptor)) + .connect(TcpClientTransport.create(server.address())) + .block(); + + testRequest(); + + testStream(); + + testRequestChannel(); + + testFireAndForget(); + + // @formatter:off + SpansAssert.assertThat(bb.getFinishedSpans()) + .haveSameTraceId() + // "request_*" + "handle" x 4 + .hasNumberOfSpansEqualTo(8) + .hasNumberOfSpansWithNameEqualTo("handle", 4) + .forAllSpansWithNameEqualTo("handle", span -> span.hasTagWithKey("rsocket.request-type")) + .hasASpanWithNameIgnoreCase("request_stream") + .thenASpanWithNameEqualToIgnoreCase("request_stream") + .hasTag("rsocket.request-type", "REQUEST_STREAM") + .backToSpans() + .hasASpanWithNameIgnoreCase("request_channel") + .thenASpanWithNameEqualToIgnoreCase("request_channel") + .hasTag("rsocket.request-type", "REQUEST_CHANNEL") + .backToSpans() + .hasASpanWithNameIgnoreCase("request_fnf") + .thenASpanWithNameEqualToIgnoreCase("request_fnf") + .hasTag("rsocket.request-type", "REQUEST_FNF") + .backToSpans() + .hasASpanWithNameIgnoreCase("request_response") + .thenASpanWithNameEqualToIgnoreCase("request_response") + .hasTag("rsocket.request-type", "REQUEST_RESPONSE"); + + MeterRegistryAssert.assertThat(registry) + .hasTimerWithNameAndTags( + "rsocket.response", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_RESPONSE"))) + .hasTimerWithNameAndTags( + "rsocket.fnf", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_FNF"))) + .hasTimerWithNameAndTags( + "rsocket.request", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_RESPONSE"))) + .hasTimerWithNameAndTags( + "rsocket.channel", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_CHANNEL"))) + .hasTimerWithNameAndTags( + "rsocket.stream", + Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_STREAM"))); + // @formatter:on + }; + } +} diff --git a/rsocket-micrometer/build.gradle b/rsocket-micrometer/build.gradle index 128aa1aa5..77bdcd08f 100644 --- a/rsocket-micrometer/build.gradle +++ b/rsocket-micrometer/build.gradle @@ -22,7 +22,9 @@ plugins { dependencies { api project(':rsocket-core') + api 'io.micrometer:micrometer-observation' api 'io.micrometer:micrometer-core' + compileOnly 'io.micrometer:micrometer-tracing' implementation 'org.slf4j:slf4j-api' diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufGetter.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufGetter.java new file mode 100644 index 000000000..09c8ba316 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufGetter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.micrometer.tracing.propagation.Propagator; +import io.netty.buffer.ByteBuf; +import io.netty.util.CharsetUtil; +import io.rsocket.metadata.CompositeMetadata; + +public class ByteBufGetter implements Propagator.Getter { + + @Override + public String get(ByteBuf carrier, String key) { + final CompositeMetadata compositeMetadata = new CompositeMetadata(carrier, false); + for (CompositeMetadata.Entry entry : compositeMetadata) { + if (key.equals(entry.getMimeType())) { + return entry.getContent().toString(CharsetUtil.UTF_8); + } + } + return null; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufSetter.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufSetter.java new file mode 100644 index 000000000..678bdb1ed --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ByteBufSetter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.micrometer.tracing.propagation.Propagator; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.metadata.CompositeMetadataCodec; + +public class ByteBufSetter implements Propagator.Setter { + + @Override + public void set(CompositeByteBuf carrier, String key, String value) { + final ByteBufAllocator alloc = carrier.alloc(); + CompositeMetadataCodec.encodeAndAddMetadataWithCompression( + carrier, alloc, key, ByteBufUtil.writeUtf8(alloc, value)); + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/CompositeMetadataUtils.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/CompositeMetadataUtils.java new file mode 100644 index 000000000..357be8f15 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/CompositeMetadataUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.micrometer.core.lang.Nullable; +import io.netty.buffer.ByteBuf; +import io.rsocket.metadata.CompositeMetadata; + +final class CompositeMetadataUtils { + + private CompositeMetadataUtils() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + @Nullable + static ByteBuf extract(ByteBuf metadata, String key) { + final CompositeMetadata compositeMetadata = new CompositeMetadata(metadata, false); + for (CompositeMetadata.Entry entry : compositeMetadata) { + final String entryKey = entry.getMimeType(); + if (key.equals(entryKey)) { + return entry.getContent(); + } + } + return null; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java new file mode 100644 index 000000000..c7ba1c772 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.rsocket.frame.FrameType; + +/** + * Default {@link RSocketRequesterObservationConvention} implementation. + * + * @author Marcin Grzejszczak + * @since 2.0.0 + */ +class DefaultRSocketObservationConvention { + + private final RSocketContext rSocketContext; + + public DefaultRSocketObservationConvention(RSocketContext rSocketContext) { + this.rSocketContext = rSocketContext; + } + + String getName() { + if (this.rSocketContext.frameType == FrameType.REQUEST_FNF) { + return "rsocket.fnf"; + } else if (this.rSocketContext.frameType == FrameType.REQUEST_STREAM) { + return "rsocket.stream"; + } else if (this.rSocketContext.frameType == FrameType.REQUEST_CHANNEL) { + return "rsocket.channel"; + } + return "%s"; + } + + protected RSocketContext getRSocketContext() { + return this.rSocketContext; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java new file mode 100644 index 000000000..824023b86 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java @@ -0,0 +1,62 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.util.StringUtils; +import io.micrometer.observation.Observation; +import io.rsocket.frame.FrameType; + +/** + * Default {@link RSocketRequesterObservationConvention} implementation. + * + * @author Marcin Grzejszczak + * @since 2.0.0 + */ +public class DefaultRSocketRequesterObservationConvention + extends DefaultRSocketObservationConvention implements RSocketRequesterObservationConvention { + + public DefaultRSocketRequesterObservationConvention(RSocketContext rSocketContext) { + super(rSocketContext); + } + + @Override + public KeyValues getLowCardinalityKeyValues(RSocketContext context) { + KeyValues values = + KeyValues.of( + RSocketDocumentedObservation.ResponderTags.REQUEST_TYPE.withValue( + context.frameType.name())); + if (StringUtils.isNotBlank(context.route)) { + values = + values.and(RSocketDocumentedObservation.ResponderTags.ROUTE.withValue(context.route)); + } + return values; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext; + } + + @Override + public String getName() { + if (getRSocketContext().frameType == FrameType.REQUEST_RESPONSE) { + return "rsocket.request"; + } + return super.getName(); + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java new file mode 100644 index 000000000..a6fa387b1 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.util.StringUtils; +import io.micrometer.observation.Observation; +import io.rsocket.frame.FrameType; + +/** + * Default {@link RSocketRequesterObservationConvention} implementation. + * + * @author Marcin Grzejszczak + * @since 2.0.0 + */ +public class DefaultRSocketResponderObservationConvention + extends DefaultRSocketObservationConvention implements RSocketResponderObservationConvention { + + public DefaultRSocketResponderObservationConvention(RSocketContext rSocketContext) { + super(rSocketContext); + } + + @Override + public KeyValues getLowCardinalityKeyValues(RSocketContext context) { + KeyValues tags = + KeyValues.of( + RSocketDocumentedObservation.ResponderTags.REQUEST_TYPE.withValue( + context.frameType.name())); + if (StringUtils.isNotBlank(context.route)) { + tags = tags.and(RSocketDocumentedObservation.ResponderTags.ROUTE.withValue(context.route)); + } + return tags; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext; + } + + @Override + public String getName() { + if (getRSocketContext().frameType == FrameType.REQUEST_RESPONSE) { + return "rsocket.response"; + } + return super.getName(); + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java new file mode 100644 index 000000000..f0d72cbee --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java @@ -0,0 +1,224 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.micrometer.common.util.StringUtils; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.DocumentedObservation; +import io.netty.buffer.ByteBuf; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.RSocketProxy; +import java.util.Iterator; +import java.util.function.Function; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** + * Tracing representation of a {@link RSocketProxy} for the requester. + * + * @author Marcin Grzejszczak + * @author Oleh Dokuka + * @since 3.1.0 + */ +public class ObservationRequesterRSocketProxy extends RSocketProxy { + + private final ObservationRegistry observationRegistry; + + private RSocketRequesterObservationConvention observationConvention; + + public ObservationRequesterRSocketProxy(RSocket source, ObservationRegistry observationRegistry) { + super(source); + this.observationRegistry = observationRegistry; + } + + @Override + public Mono fireAndForget(Payload payload) { + return setObservation( + super::fireAndForget, + payload, + FrameType.REQUEST_FNF, + RSocketDocumentedObservation.RSOCKET_REQUESTER_FNF); + } + + @Override + public Mono requestResponse(Payload payload) { + return setObservation( + super::requestResponse, + payload, + FrameType.REQUEST_RESPONSE, + RSocketDocumentedObservation.RSOCKET_REQUESTER_REQUEST_RESPONSE); + } + + Mono setObservation( + Function> input, + Payload payload, + FrameType frameType, + DocumentedObservation observation) { + return Mono.deferContextual( + contextView -> { + if (contextView.hasKey(Observation.class)) { + Observation parent = contextView.get(Observation.class); + try (Observation.Scope scope = parent.openScope()) { + return observe(input, payload, frameType, observation); + } + } + return observe(input, payload, frameType, observation); + }); + } + + private String route(Payload payload) { + if (payload.hasMetadata()) { + try { + ByteBuf extracted = + CompositeMetadataUtils.extract( + payload.sliceMetadata(), WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()); + final RoutingMetadata routingMetadata = new RoutingMetadata(extracted); + final Iterator iterator = routingMetadata.iterator(); + return iterator.next(); + } catch (Exception e) { + + } + } + return null; + } + + private Mono observe( + Function> input, + Payload payload, + FrameType frameType, + DocumentedObservation obs) { + String route = route(payload); + RSocketContext rSocketContext = + new RSocketContext( + payload, payload.sliceMetadata(), frameType, route, RSocketContext.Side.REQUESTER); + Observation observation = + obs.start( + this.observationConvention, + new DefaultRSocketRequesterObservationConvention(rSocketContext), + rSocketContext, + observationRegistry); + setContextualName(frameType, route, observation); + Payload newPayload = payload; + if (rSocketContext.modifiedPayload != null) { + newPayload = rSocketContext.modifiedPayload; + } + return input + .apply(newPayload) + .doOnError(observation::error) + .doFinally(signalType -> observation.stop()); + } + + private Observation observation(ContextView contextView) { + if (contextView.hasKey(Observation.class)) { + return contextView.get(Observation.class); + } + return null; + } + + @Override + public Flux requestStream(Payload payload) { + return Flux.deferContextual( + contextView -> + setObservation( + super::requestStream, + payload, + contextView, + FrameType.REQUEST_STREAM, + RSocketDocumentedObservation.RSOCKET_REQUESTER_REQUEST_STREAM)); + } + + @Override + public Flux requestChannel(Publisher inbound) { + return Flux.from(inbound) + .switchOnFirst( + (firstSignal, flux) -> { + final Payload firstPayload = firstSignal.get(); + if (firstPayload != null) { + return setObservation( + p -> super.requestChannel(flux.skip(1).startWith(p)), + firstPayload, + firstSignal.getContextView(), + FrameType.REQUEST_CHANNEL, + RSocketDocumentedObservation.RSOCKET_REQUESTER_REQUEST_CHANNEL); + } + return flux; + }); + } + + private Flux setObservation( + Function> input, + Payload payload, + ContextView contextView, + FrameType frameType, + DocumentedObservation obs) { + Observation parentObservation = observation(contextView); + if (parentObservation == null) { + return observationFlux(input, payload, frameType, obs); + } + try (Observation.Scope scope = parentObservation.openScope()) { + return observationFlux(input, payload, frameType, obs); + } + } + + private Flux observationFlux( + Function> input, + Payload payload, + FrameType frameType, + DocumentedObservation obs) { + return Flux.deferContextual( + contextView -> { + String route = route(payload); + RSocketContext rSocketContext = + new RSocketContext( + payload, + payload.sliceMetadata(), + frameType, + route, + RSocketContext.Side.REQUESTER); + Observation newObservation = + obs.start( + this.observationConvention, + new DefaultRSocketRequesterObservationConvention(rSocketContext), + rSocketContext, + this.observationRegistry); + setContextualName(frameType, route, newObservation); + return input + .apply(rSocketContext.modifiedPayload) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + }); + } + + private void setContextualName(FrameType frameType, String route, Observation newObservation) { + if (StringUtils.isNotBlank(route)) { + newObservation.contextualName(frameType.name() + " " + route); + } else { + newObservation.contextualName(frameType.name()); + } + } + + public void setObservationConvention(RSocketRequesterObservationConvention convention) { + this.observationConvention = convention; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java new file mode 100644 index 000000000..968ddc014 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java @@ -0,0 +1,167 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.micrometer.common.util.StringUtils; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.netty.buffer.ByteBuf; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.RSocketProxy; +import java.util.Iterator; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Tracing representation of a {@link RSocketProxy} for the responder. + * + * @author Marcin Grzejszczak + * @author Oleh Dokuka + * @since 3.1.0 + */ +public class ObservationResponderRSocketProxy extends RSocketProxy { + + private final ObservationRegistry observationRegistry; + + private RSocketResponderObservationConvention observationConvention; + + public ObservationResponderRSocketProxy(RSocket source, ObservationRegistry observationRegistry) { + super(source); + this.observationRegistry = observationRegistry; + } + + @Override + public Mono fireAndForget(Payload payload) { + // called on Netty EventLoop + // there can't be observation in thread local here + ByteBuf sliceMetadata = payload.sliceMetadata(); + String route = route(payload, sliceMetadata); + RSocketContext rSocketContext = + new RSocketContext( + payload, + payload.sliceMetadata(), + FrameType.REQUEST_FNF, + route, + RSocketContext.Side.RESPONDER); + Observation newObservation = + startObservation(RSocketDocumentedObservation.RSOCKET_RESPONDER_FNF, rSocketContext); + return super.fireAndForget(rSocketContext.modifiedPayload) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + } + + private Observation startObservation( + RSocketDocumentedObservation observation, RSocketContext rSocketContext) { + return observation.start( + this.observationConvention, + new DefaultRSocketResponderObservationConvention(rSocketContext), + rSocketContext, + this.observationRegistry); + } + + @Override + public Mono requestResponse(Payload payload) { + ByteBuf sliceMetadata = payload.sliceMetadata(); + String route = route(payload, sliceMetadata); + RSocketContext rSocketContext = + new RSocketContext( + payload, + payload.sliceMetadata(), + FrameType.REQUEST_RESPONSE, + route, + RSocketContext.Side.RESPONDER); + Observation newObservation = + startObservation( + RSocketDocumentedObservation.RSOCKET_RESPONDER_REQUEST_RESPONSE, rSocketContext); + return super.requestResponse(rSocketContext.modifiedPayload) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + } + + @Override + public Flux requestStream(Payload payload) { + ByteBuf sliceMetadata = payload.sliceMetadata(); + String route = route(payload, sliceMetadata); + RSocketContext rSocketContext = + new RSocketContext( + payload, sliceMetadata, FrameType.REQUEST_STREAM, route, RSocketContext.Side.RESPONDER); + Observation newObservation = + startObservation( + RSocketDocumentedObservation.RSOCKET_RESPONDER_REQUEST_STREAM, rSocketContext); + return super.requestStream(rSocketContext.modifiedPayload) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + .switchOnFirst( + (firstSignal, flux) -> { + final Payload firstPayload = firstSignal.get(); + if (firstPayload != null) { + ByteBuf sliceMetadata = firstPayload.sliceMetadata(); + String route = route(firstPayload, sliceMetadata); + RSocketContext rSocketContext = + new RSocketContext( + firstPayload, + firstPayload.sliceMetadata(), + FrameType.REQUEST_CHANNEL, + route, + RSocketContext.Side.RESPONDER); + Observation newObservation = + startObservation( + RSocketDocumentedObservation.RSOCKET_RESPONDER_REQUEST_CHANNEL, + rSocketContext); + if (StringUtils.isNotBlank(route)) { + newObservation.contextualName(rSocketContext.frameType.name() + " " + route); + } + return super.requestChannel(flux.skip(1).startWith(rSocketContext.modifiedPayload)) + .doOnError(newObservation::error) + .doFinally(signalType -> newObservation.stop()); + } + return flux; + }); + } + + private String route(Payload payload, ByteBuf headers) { + if (payload.hasMetadata()) { + try { + final ByteBuf extract = + CompositeMetadataUtils.extract( + headers, WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()); + if (extract != null) { + final RoutingMetadata routingMetadata = new RoutingMetadata(extract); + final Iterator iterator = routingMetadata.iterator(); + return iterator.next(); + } + } catch (Exception e) { + + } + } + return null; + } + + public void setObservationConvention(RSocketResponderObservationConvention convention) { + this.observationConvention = convention; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/PayloadUtils.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/PayloadUtils.java new file mode 100644 index 000000000..e5286a53f --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/PayloadUtils.java @@ -0,0 +1,73 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.Payload; +import io.rsocket.metadata.CompositeMetadata; +import io.rsocket.metadata.CompositeMetadata.Entry; +import io.rsocket.metadata.CompositeMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.ByteBufPayload; +import io.rsocket.util.DefaultPayload; +import java.util.HashSet; +import java.util.Set; + +final class PayloadUtils { + + private PayloadUtils() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + static CompositeByteBuf cleanTracingMetadata(Payload payload, Set fields) { + Set fieldsWithDefaultZipkin = new HashSet<>(fields); + fieldsWithDefaultZipkin.add(WellKnownMimeType.MESSAGE_RSOCKET_TRACING_ZIPKIN.getString()); + final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + if (payload.hasMetadata()) { + try { + final CompositeMetadata entries = new CompositeMetadata(payload.metadata(), false); + for (Entry entry : entries) { + if (!fieldsWithDefaultZipkin.contains(entry.getMimeType())) { + CompositeMetadataCodec.encodeAndAddMetadataWithCompression( + metadata, + ByteBufAllocator.DEFAULT, + entry.getMimeType(), + entry.getContent().retain()); + } + } + } catch (Exception e) { + + } + } + return metadata; + } + + static Payload payload(Payload payload, CompositeByteBuf metadata) { + final Payload newPayload; + try { + if (payload instanceof ByteBufPayload) { + newPayload = ByteBufPayload.create(payload.data().retain(), metadata); + } else { + newPayload = DefaultPayload.create(payload.data().retain(), metadata); + } + } finally { + payload.release(); + } + return newPayload; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketContext.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketContext.java new file mode 100644 index 000000000..8622cdfa5 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketContext.java @@ -0,0 +1,76 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.micrometer.common.lang.Nullable; +import io.micrometer.observation.Observation; +import io.netty.buffer.ByteBuf; +import io.rsocket.Payload; +import io.rsocket.frame.FrameType; + +public class RSocketContext extends Observation.Context { + + final Payload payload; + + final ByteBuf metadata; + + final FrameType frameType; + + final String route; + + final Side side; + + Payload modifiedPayload; + + RSocketContext( + Payload payload, ByteBuf metadata, FrameType frameType, @Nullable String route, Side side) { + this.payload = payload; + this.metadata = metadata; + this.frameType = frameType; + this.route = route; + this.side = side; + } + + public enum Side { + REQUESTER, + RESPONDER + } + + public Payload getPayload() { + return payload; + } + + public ByteBuf getMetadata() { + return metadata; + } + + public FrameType getFrameType() { + return frameType; + } + + public String getRoute() { + return route; + } + + public Side getSide() { + return side; + } + + public Payload getModifiedPayload() { + return modifiedPayload; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java new file mode 100644 index 000000000..18440ad81 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java @@ -0,0 +1,231 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.docs.DocumentedObservation; + +enum RSocketDocumentedObservation implements DocumentedObservation { + + /** Observation created on the RSocket responder side. */ + RSOCKET_RESPONDER { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + }, + + /** Observation created on the RSocket requester side for Fire and Forget frame type. */ + RSOCKET_REQUESTER_FNF { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketRequesterObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return RequesterTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket responder side for Fire and Forget frame type. */ + RSOCKET_RESPONDER_FNF { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ResponderTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket requester side for Request Response frame type. */ + RSOCKET_REQUESTER_REQUEST_RESPONSE { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketRequesterObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return RequesterTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket responder side for Request Response frame type. */ + RSOCKET_RESPONDER_REQUEST_RESPONSE { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ResponderTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket requester side for Request Stream frame type. */ + RSOCKET_REQUESTER_REQUEST_STREAM { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketRequesterObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return RequesterTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket responder side for Request Stream frame type. */ + RSOCKET_RESPONDER_REQUEST_STREAM { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ResponderTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket requester side for Request Channel frame type. */ + RSOCKET_REQUESTER_REQUEST_CHANNEL { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketRequesterObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return RequesterTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }, + + /** Observation created on the RSocket responder side for Request Channel frame type. */ + RSOCKET_RESPONDER_REQUEST_CHANNEL { + @Override + public Class> + getDefaultConvention() { + return DefaultRSocketResponderObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ResponderTags.values(); + } + + @Override + public String getPrefix() { + return "rsocket."; + } + }; + + enum RequesterTags implements KeyName { + + /** Name of the RSocket route. */ + ROUTE { + @Override + public String asString() { + return "rsocket.route"; + } + }, + + /** Name of the RSocket request type. */ + REQUEST_TYPE { + @Override + public String asString() { + return "rsocket.request-type"; + } + }, + + /** Name of the RSocket content type. */ + CONTENT_TYPE { + @Override + public String asString() { + return "rsocket.content-type"; + } + } + } + + enum ResponderTags implements KeyName { + + /** Name of the RSocket route. */ + ROUTE { + @Override + public String asString() { + return "rsocket.route"; + } + }, + + /** Name of the RSocket request type. */ + REQUEST_TYPE { + @Override + public String asString() { + return "rsocket.request-type"; + } + } + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java new file mode 100644 index 000000000..512c19abf --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.micrometer.observation.Observation; + +/** + * {@link Observation.ObservationConvention} for RSocket requester {@link RSocketContext}. + * + * @author Marcin Grzejszczak + * @since 2.0.0 + */ +public interface RSocketRequesterObservationConvention + extends Observation.ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext + && ((RSocketContext) context).side == RSocketContext.Side.REQUESTER; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java new file mode 100644 index 000000000..3f6b5dc52 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java @@ -0,0 +1,124 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.TracingObservationHandler; +import io.micrometer.tracing.internal.EncodingUtils; +import io.micrometer.tracing.propagation.Propagator; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.Payload; +import io.rsocket.metadata.TracingMetadataCodec; +import java.util.HashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RSocketRequesterTracingObservationHandler + implements TracingObservationHandler { + private static final Logger log = + LoggerFactory.getLogger(RSocketRequesterTracingObservationHandler.class); + + private final Propagator propagator; + + private final Propagator.Setter setter; + + private final Tracer tracer; + + private final boolean isZipkinPropagationEnabled; + + public RSocketRequesterTracingObservationHandler( + Tracer tracer, + Propagator propagator, + Propagator.Setter setter, + boolean isZipkinPropagationEnabled) { + this.tracer = tracer; + this.propagator = propagator; + this.setter = setter; + this.isZipkinPropagationEnabled = isZipkinPropagationEnabled; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext + && ((RSocketContext) context).side == RSocketContext.Side.REQUESTER; + } + + @Override + public Tracer getTracer() { + return this.tracer; + } + + @Override + public void onStart(RSocketContext context) { + Payload payload = context.payload; + Span.Builder spanBuilder = this.tracer.spanBuilder(); + Span span = spanBuilder.kind(Span.Kind.PRODUCER).start(); + log.debug("Extracted result from context or thread local {}", span); + // TODO: newmetadata returns an empty composite byte buf + final CompositeByteBuf newMetadata = + PayloadUtils.cleanTracingMetadata(payload, new HashSet<>(propagator.fields())); + TraceContext traceContext = span.context(); + if (this.isZipkinPropagationEnabled) { + injectDefaultZipkinRSocketHeaders(newMetadata, traceContext); + } + this.propagator.inject(traceContext, newMetadata, this.setter); + context.modifiedPayload = PayloadUtils.payload(payload, newMetadata); + getTracingContext(context).setSpan(span); + } + + @Override + public void onError(RSocketContext context) { + context.getError().ifPresent(throwable -> getRequiredSpan(context).error(throwable)); + } + + @Override + public void onStop(RSocketContext context) { + Span span = getRequiredSpan(context); + tagSpan(context, span); + span.name(context.getContextualName()).end(); + } + + private void injectDefaultZipkinRSocketHeaders( + CompositeByteBuf newMetadata, TraceContext traceContext) { + TracingMetadataCodec.Flags flags = + traceContext.sampled() == null + ? TracingMetadataCodec.Flags.UNDECIDED + : traceContext.sampled() + ? TracingMetadataCodec.Flags.SAMPLE + : TracingMetadataCodec.Flags.NOT_SAMPLE; + String traceId = traceContext.traceId(); + long[] traceIds = EncodingUtils.fromString(traceId); + long[] spanId = EncodingUtils.fromString(traceContext.spanId()); + long[] parentSpanId = EncodingUtils.fromString(traceContext.parentId()); + boolean isTraceId128Bit = traceIds.length == 2; + if (isTraceId128Bit) { + TracingMetadataCodec.encode128( + newMetadata.alloc(), + traceIds[0], + traceIds[1], + spanId[0], + EncodingUtils.fromString(traceContext.parentId())[0], + flags); + } else { + TracingMetadataCodec.encode64( + newMetadata.alloc(), traceIds[0], spanId[0], parentSpanId[0], flags); + } + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java new file mode 100644 index 000000000..9599429f5 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.micrometer.observation.Observation; + +/** + * {@link Observation.ObservationConvention} for RSocket responder {@link RSocketContext}. + * + * @author Marcin Grzejszczak + * @since 2.0.0 + */ +public interface RSocketResponderObservationConvention + extends Observation.ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext + && ((RSocketContext) context).side == RSocketContext.Side.RESPONDER; + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java new file mode 100644 index 000000000..ae06a4307 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java @@ -0,0 +1,149 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.TracingObservationHandler; +import io.micrometer.tracing.internal.EncodingUtils; +import io.micrometer.tracing.propagation.Propagator; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.Payload; +import io.rsocket.frame.FrameType; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TracingMetadata; +import io.rsocket.metadata.TracingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import java.util.HashSet; +import java.util.Iterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RSocketResponderTracingObservationHandler + implements TracingObservationHandler { + + private static final Logger log = + LoggerFactory.getLogger(RSocketResponderTracingObservationHandler.class); + + private final Propagator propagator; + + private final Propagator.Getter getter; + + private final Tracer tracer; + + private final boolean isZipkinPropagationEnabled; + + public RSocketResponderTracingObservationHandler( + Tracer tracer, + Propagator propagator, + Propagator.Getter getter, + boolean isZipkinPropagationEnabled) { + this.tracer = tracer; + this.propagator = propagator; + this.getter = getter; + this.isZipkinPropagationEnabled = isZipkinPropagationEnabled; + } + + @Override + public void onStart(RSocketContext context) { + Span handle = consumerSpanBuilder(context.payload, context.metadata, context.frameType); + CompositeByteBuf bufs = + PayloadUtils.cleanTracingMetadata(context.payload, new HashSet<>(propagator.fields())); + context.modifiedPayload = PayloadUtils.payload(context.payload, bufs); + getTracingContext(context).setSpan(handle); + } + + @Override + public void onError(RSocketContext context) { + context.getError().ifPresent(throwable -> getRequiredSpan(context).error(throwable)); + } + + @Override + public void onStop(RSocketContext context) { + Span span = getRequiredSpan(context); + tagSpan(context, span); + span.end(); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RSocketContext + && ((RSocketContext) context).side == RSocketContext.Side.RESPONDER; + } + + @Override + public Tracer getTracer() { + return this.tracer; + } + + private Span consumerSpanBuilder(Payload payload, ByteBuf headers, FrameType requestType) { + Span.Builder consumerSpanBuilder = consumerSpanBuilder(payload, headers); + log.debug("Extracted result from headers {}", consumerSpanBuilder); + String name = "handle"; + if (payload.hasMetadata()) { + try { + final ByteBuf extract = + CompositeMetadataUtils.extract( + headers, WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()); + if (extract != null) { + final RoutingMetadata routingMetadata = new RoutingMetadata(extract); + final Iterator iterator = routingMetadata.iterator(); + name = requestType.name() + " " + iterator.next(); + } + } catch (Exception e) { + + } + } + return consumerSpanBuilder.kind(Span.Kind.CONSUMER).name(name).start(); + } + + private Span.Builder consumerSpanBuilder(Payload payload, ByteBuf headers) { + if (this.isZipkinPropagationEnabled && payload.hasMetadata()) { + try { + ByteBuf extract = + CompositeMetadataUtils.extract( + headers, WellKnownMimeType.MESSAGE_RSOCKET_TRACING_ZIPKIN.getString()); + if (extract != null) { + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(extract); + Span.Builder builder = this.tracer.spanBuilder(); + String traceId = EncodingUtils.fromLong(tracingMetadata.traceId()); + long traceIdHigh = tracingMetadata.traceIdHigh(); + if (traceIdHigh != 0L) { + // ExtendedTraceId + traceId = EncodingUtils.fromLong(traceIdHigh) + traceId; + } + TraceContext.Builder parentBuilder = + this.tracer + .traceContextBuilder() + .sampled(tracingMetadata.isDebug() || tracingMetadata.isSampled()) + .traceId(traceId) + .spanId(EncodingUtils.fromLong(tracingMetadata.spanId())) + .parentId(EncodingUtils.fromLong(tracingMetadata.parentId())); + return builder.setParent(parentBuilder.build()); + } else { + return this.propagator.extract(headers, this.getter); + } + } catch (Exception e) { + + } + } + return this.propagator.extract(headers, this.getter); + } +} From ef826de051864c3ee2492711ca572e2a2e1b619a Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Sat, 6 Aug 2022 14:29:46 +0300 Subject: [PATCH 136/183] updates version --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b495b6b45..96c155bc2 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ repositories { maven { url 'https://repo.spring.io/milestone' } // Reactor milestones (if needed) } dependencies { - implementation 'io.rsocket:rsocket-core:1.1.1' - implementation 'io.rsocket:rsocket-transport-netty:1.1.1' + implementation 'io.rsocket:rsocket-core:1.1.2' + implementation 'io.rsocket:rsocket-transport-netty:1.1.2' } ``` @@ -44,8 +44,8 @@ repositories { maven { url 'https://repo.spring.io/snapshot' } // Reactor snapshots (if needed) } dependencies { - implementation 'io.rsocket:rsocket-core:1.1.2-SNAPSHOT' - implementation 'io.rsocket:rsocket-transport-netty:1.1.2-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:1.1.3-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-netty:1.1.3-SNAPSHOT' } ``` From 6426e45619ec32acef0c6c7184020776a9063ffe Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 16 Aug 2022 10:54:21 +0300 Subject: [PATCH 137/183] moves error propagation out of the synchronise to avoid deadlock (#1060) --- .../java/io/rsocket/core/RSocketRequester.java | 16 +++++++++------- .../java/io/rsocket/core/RSocketResponder.java | 11 +++++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index c10e86d56..bf298706a 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -345,15 +345,17 @@ private void terminate(Throwable e) { requesterLeaseTracker.dispose(e); } + final Collection activeStreamsCopy; synchronized (this) { final IntObjectMap activeStreams = this.activeStreams; - final Collection activeStreamsCopy = new ArrayList<>(activeStreams.values()); - for (FrameHandler handler : activeStreamsCopy) { - if (handler != null) { - try { - handler.handleError(e); - } catch (Throwable ignored) { - } + activeStreamsCopy = new ArrayList<>(activeStreams.values()); + } + + for (FrameHandler handler : activeStreamsCopy) { + if (handler != null) { + try { + handler.handleError(e); + } catch (Throwable ignored) { } } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index b2f084f51..ce4fe70a3 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -185,15 +185,18 @@ final void doOnDispose() { requestHandler.dispose(); } - private synchronized void cleanUpSendingSubscriptions() { - final IntObjectMap activeStreams = this.activeStreams; - final Collection activeStreamsCopy = new ArrayList<>(activeStreams.values()); + private void cleanUpSendingSubscriptions() { + final Collection activeStreamsCopy; + synchronized (this) { + final IntObjectMap activeStreams = this.activeStreams; + activeStreamsCopy = new ArrayList<>(activeStreams.values()); + } + for (FrameHandler handler : activeStreamsCopy) { if (handler != null) { handler.handleCancel(); } } - activeStreams.clear(); } final void handleFrame(ByteBuf frame) { From 1fe2d644acf15d2c8e08606b3161c8def1477d50 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 16 Aug 2022 10:54:21 +0300 Subject: [PATCH 138/183] moves error propagation out of the synchronise to avoid deadlock (#1060) (cherry picked from commit 6426e45619ec32acef0c6c7184020776a9063ffe) --- .../java/io/rsocket/core/RSocketRequester.java | 16 +++++++++------- .../java/io/rsocket/core/RSocketResponder.java | 11 +++++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index c10e86d56..bf298706a 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -345,15 +345,17 @@ private void terminate(Throwable e) { requesterLeaseTracker.dispose(e); } + final Collection activeStreamsCopy; synchronized (this) { final IntObjectMap activeStreams = this.activeStreams; - final Collection activeStreamsCopy = new ArrayList<>(activeStreams.values()); - for (FrameHandler handler : activeStreamsCopy) { - if (handler != null) { - try { - handler.handleError(e); - } catch (Throwable ignored) { - } + activeStreamsCopy = new ArrayList<>(activeStreams.values()); + } + + for (FrameHandler handler : activeStreamsCopy) { + if (handler != null) { + try { + handler.handleError(e); + } catch (Throwable ignored) { } } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index b2f084f51..ce4fe70a3 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -185,15 +185,18 @@ final void doOnDispose() { requestHandler.dispose(); } - private synchronized void cleanUpSendingSubscriptions() { - final IntObjectMap activeStreams = this.activeStreams; - final Collection activeStreamsCopy = new ArrayList<>(activeStreams.values()); + private void cleanUpSendingSubscriptions() { + final Collection activeStreamsCopy; + synchronized (this) { + final IntObjectMap activeStreams = this.activeStreams; + activeStreamsCopy = new ArrayList<>(activeStreams.values()); + } + for (FrameHandler handler : activeStreamsCopy) { if (handler != null) { handler.handleCancel(); } } - activeStreams.clear(); } final void handleFrame(ByteBuf frame) { From d330a324a7c506091b8e1da22b8676db1bee5042 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Sat, 20 Aug 2022 13:02:47 +0300 Subject: [PATCH 139/183] improves BaseDuplexConnection and fixes PingClient impl (#1062) --- .../io/rsocket/internal/BaseDuplexConnection.java | 11 ++++------- .../src/main/java/io/rsocket/test/PingClient.java | 10 +++++++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java index 98bed7ba7..fc679c259 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java @@ -22,13 +22,10 @@ import reactor.core.publisher.Sinks; public abstract class BaseDuplexConnection implements DuplexConnection { - protected Sinks.Empty onClose = Sinks.empty(); + protected final Sinks.Empty onClose = Sinks.empty(); + protected final UnboundedProcessor sender = new UnboundedProcessor(onClose::tryEmitEmpty); - protected UnboundedProcessor sender = new UnboundedProcessor(); - - public BaseDuplexConnection() { - onClose().doFinally(s -> doOnClose()).subscribe(); - } + public BaseDuplexConnection() {} @Override public void sendFrame(int streamId, ByteBuf frame) { @@ -48,7 +45,7 @@ public final Mono onClose() { @Override public final void dispose() { - onClose.tryEmitEmpty(); + doOnClose(); } @Override diff --git a/rsocket-test/src/main/java/io/rsocket/test/PingClient.java b/rsocket-test/src/main/java/io/rsocket/test/PingClient.java index 9017e854b..14740950a 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/PingClient.java +++ b/rsocket-test/src/main/java/io/rsocket/test/PingClient.java @@ -63,8 +63,8 @@ Flux pingPong( BiFunction> interaction, int count, final Recorder histogram) { - return client - .flatMapMany( + return Flux.usingWhen( + client, rsocket -> Flux.range(1, count) .flatMap( @@ -78,7 +78,11 @@ Flux pingPong( histogram.recordValue(diff); }); }, - 64)) + 64), + rsocket -> { + rsocket.dispose(); + return rsocket.onClose(); + }) .doOnError(Throwable::printStackTrace); } } From 95ad3b78ed788c4cf479cc9a7199fb4537ce5d72 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 20 Aug 2022 14:32:27 +0300 Subject: [PATCH 140/183] fixes version name Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3b8caafcc..1334b1a16 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.2.0-SNAPSHOT +version=1.2.0 perfBaselineVersion=1.1.1 From 81429871ba1da2052a789afc843a450453cbcae0 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Sat, 20 Aug 2022 14:34:16 +0300 Subject: [PATCH 141/183] updates GA config Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- .github/workflows/gradle-all.yml | 2 +- .github/workflows/gradle-main.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gradle-all.yml b/.github/workflows/gradle-all.yml index f2df20620..abbd14106 100644 --- a/.github/workflows/gradle-all.yml +++ b/.github/workflows/gradle-all.yml @@ -5,7 +5,7 @@ on: # but only for the non master/1.0.x branches push: branches-ignore: - - 1.0.x + - 1.1.x - master jobs: diff --git a/.github/workflows/gradle-main.yml b/.github/workflows/gradle-main.yml index 904c45fb7..469ccb103 100644 --- a/.github/workflows/gradle-main.yml +++ b/.github/workflows/gradle-main.yml @@ -2,11 +2,11 @@ name: Main Branches Java CI on: # Trigger the workflow on push - # but only for the master/1.0.x branch + # but only for the master/1.1.x branch push: branches: - master - - 1.0.x + - 1.1.x jobs: build: From 187cf547e99c2306102d44cc3e66834016d40164 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sun, 21 Aug 2022 23:31:22 +0300 Subject: [PATCH 142/183] fixes build Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 2e890e032..6a2ca537d 100644 --- a/build.gradle +++ b/build.gradle @@ -106,6 +106,7 @@ subprojects { maven { url 'https://repo.spring.io/milestone' content { + includeGroup "io.micrometer" includeGroup "io.projectreactor" includeGroup "io.projectreactor.netty" } @@ -114,6 +115,7 @@ subprojects { maven { url 'https://repo.spring.io/snapshot' content { + includeGroup "io.micrometer" includeGroup "io.projectreactor" includeGroup "io.projectreactor.netty" } From 52f458310f9ab6a721c5c8120cad5dd2e3446100 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Wed, 7 Sep 2022 18:32:20 +0300 Subject: [PATCH 143/183] ensures setupframe is available for future use (#1046) --- .../src/main/java/io/rsocket/core/RSocketConnector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index edd13b48c..432c0f0f5 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -590,7 +590,7 @@ public Mono connect(Supplier transportSupplier) { dataMimeType, setupPayload); - sourceConnection.sendFrame(0, setupFrame.retain()); + sourceConnection.sendFrame(0, setupFrame.retainedSlice()); return clientSetup .init(sourceConnection) From af021d9fd9b0ff02ed9bb2f3406523816b5070d5 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Wed, 7 Sep 2022 22:42:17 +0300 Subject: [PATCH 144/183] adds message counting to protect against malicious overflow (#1067) Co-authored-by: Rossen Stoyanchev --- .../core/RequestChannelRequesterFlux.java | 25 +++ .../RequestChannelResponderSubscriber.java | 64 ++++++++ .../core/RequestStreamRequesterFlux.java | 32 ++++ .../core/RequestChannelRequesterFluxTest.java | 73 +++++++++ ...RequestChannelResponderSubscriberTest.java | 152 +++++++++++++++++- .../core/RequestStreamRequesterFluxTest.java | 83 +++++++++- 6 files changed, 425 insertions(+), 4 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java index 809125402..aab491793 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java @@ -86,6 +86,8 @@ final class RequestChannelRequesterFlux extends Flux Context cachedContext; CoreSubscriber inboundSubscriber; boolean inboundDone; + long requested; + long produced; CompositeByteBuf frames; @@ -138,6 +140,8 @@ public final void request(long n) { return; } + this.requested = Operators.addCap(this.requested, n); + long previousState = addRequestN(STATE, this, n, this.requesterLeaseTracker == null); if (isTerminated(previousState)) { return; @@ -706,6 +710,27 @@ public final void handlePayload(Payload value) { return; } + final long produced = this.produced; + if (this.requested == produced) { + value.release(); + if (!tryCancel()) { + return; + } + + final Throwable cause = + Exceptions.failWithOverflow( + "The number of messages received exceeds the number requested"); + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_CHANNEL, cause); + } + + this.inboundSubscriber.onError(cause); + return; + } + + this.produced = produced + 1; + this.inboundSubscriber.onNext(value); } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java index 8dac9858d..32128fee4 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelResponderSubscriber.java @@ -88,6 +88,8 @@ final class RequestChannelResponderSubscriber extends Flux boolean inboundDone; boolean outboundDone; + long requested; + long produced; public RequestChannelResponderSubscriber( int streamId, @@ -179,6 +181,8 @@ public void request(long n) { return; } + this.requested = Operators.addCap(this.requested, n); + long previousState = StateUtils.addRequestN(STATE, this, n); if (isTerminated(previousState)) { // full termination can be the result of both sides completion / cancelFrame / remote or local @@ -196,6 +200,9 @@ public void request(long n) { Payload firstPayload = this.firstPayload; if (firstPayload != null) { this.firstPayload = null; + + this.produced++; + inboundSubscriber.onNext(firstPayload); } @@ -216,6 +223,8 @@ public void request(long n) { final Payload firstPayload = this.firstPayload; this.firstPayload = null; + this.produced++; + inboundSubscriber.onNext(firstPayload); inboundSubscriber.onComplete(); @@ -238,6 +247,9 @@ public void request(long n) { final Payload firstPayload = this.firstPayload; this.firstPayload = null; + + this.produced++; + inboundSubscriber.onNext(firstPayload); previousState = markFirstFrameSent(STATE, this); @@ -416,6 +428,58 @@ final void handlePayload(Payload p) { return; } + final long produced = this.produced; + if (this.requested == produced) { + p.release(); + + this.inboundDone = true; + + final Throwable cause = + Exceptions.failWithOverflow( + "The number of messages received exceeds the number requested"); + boolean wasThrowableAdded = Exceptions.addThrowable(INBOUND_ERROR, this, cause); + + long previousState = markTerminated(STATE, this); + if (isTerminated(previousState)) { + if (!wasThrowableAdded) { + Operators.onErrorDropped(cause, this.inboundSubscriber.currentContext()); + } + return; + } + + this.requesterResponderSupport.remove(this.streamId, this); + + this.connection.sendFrame( + streamId, + ErrorFrameCodec.encode( + this.allocator, streamId, new CanceledException(cause.getMessage()))); + + if (!isSubscribed(previousState)) { + final Payload firstPayload = this.firstPayload; + this.firstPayload = null; + firstPayload.release(); + } else if (isFirstFrameSent(previousState) && !isInboundTerminated(previousState)) { + Throwable inboundError = Exceptions.terminate(INBOUND_ERROR, this); + if (inboundError != TERMINATED) { + //noinspection ConstantConditions + this.inboundSubscriber.onError(inboundError); + } + } + + // this is downstream subscription so need to cancel it just in case error signal has not + // reached it + // needs for disconnected upstream and downstream case + this.outboundSubscription.cancel(); + + final RequestInterceptor interceptor = requestInterceptor; + if (interceptor != null) { + interceptor.onTerminate(this.streamId, FrameType.REQUEST_CHANNEL, cause); + } + return; + } + + this.produced = produced + 1; + this.inboundSubscriber.onNext(p); } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java index 424451a58..55ec43feb 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java @@ -65,6 +65,8 @@ final class RequestStreamRequesterFlux extends Flux CoreSubscriber inboundSubscriber; CompositeByteBuf frames; boolean done; + long requested; + long produced; RequestStreamRequesterFlux(Payload payload, RequesterResponderSupport requesterResponderSupport) { this.allocator = requesterResponderSupport.getAllocator(); @@ -134,6 +136,8 @@ public final void request(long n) { return; } + this.requested = Operators.addCap(this.requested, n); + final RequesterLeaseTracker requesterLeaseTracker = this.requesterLeaseTracker; final boolean leaseEnabled = requesterLeaseTracker != null; final long previousState = addRequestN(STATE, this, n, !leaseEnabled); @@ -295,6 +299,34 @@ public final void handlePayload(Payload p) { return; } + final long produced = this.produced; + if (this.requested == produced) { + p.release(); + + long previousState = markTerminated(STATE, this); + if (isTerminated(previousState)) { + return; + } + + final int streamId = this.streamId; + this.requesterResponderSupport.remove(streamId, this); + + final IllegalStateException cause = + Exceptions.failWithOverflow( + "The number of messages received exceeds the number requested"); + this.connection.sendFrame(streamId, CancelFrameCodec.encode(this.allocator, streamId)); + + final RequestInterceptor requestInterceptor = this.requestInterceptor; + if (requestInterceptor != null) { + requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, cause); + } + + this.inboundSubscriber.onError(cause); + return; + } + + this.produced = produced + 1; + this.inboundSubscriber.onNext(p); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelRequesterFluxTest.java b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelRequesterFluxTest.java index e39b0d690..c1e0a6876 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelRequesterFluxTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelRequesterFluxTest.java @@ -16,6 +16,7 @@ package io.rsocket.core; import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; +import static io.rsocket.frame.FrameType.CANCEL; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -40,6 +41,7 @@ import java.util.stream.Stream; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -513,6 +515,77 @@ public void errorShouldTerminateExecution(String terminationMode) { stateAssert.isTerminated(); } + @Test + public void failOnOverflow() { + final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); + final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + final TestPublisher publisher = TestPublisher.create(); + + final RequestChannelRequesterFlux requestChannelRequesterFlux = + new RequestChannelRequesterFlux(publisher, activeStreams); + final StateAssert stateAssert = + StateAssert.assertThat(requestChannelRequesterFlux); + + // state machine check + + stateAssert.isUnsubscribed(); + activeStreams.assertNoActiveStreams(); + + final AssertSubscriber assertSubscriber = + requestChannelRequesterFlux.subscribeWith(AssertSubscriber.create(0)); + activeStreams.assertNoActiveStreams(); + + // state machine check + stateAssert.hasSubscribedFlagOnly(); + + assertSubscriber.request(1); + stateAssert.hasSubscribedFlag().hasRequestN(1).hasNoFirstFrameSentFlag(); + activeStreams.assertNoActiveStreams(); + + Payload payload1 = TestRequesterResponderSupport.randomPayload(allocator); + + publisher.next(payload1.retain()); + + FrameAssert.assertThat(sender.awaitFrame()) + .typeOf(FrameType.REQUEST_CHANNEL) + .hasPayload(payload1) + .hasRequestN(1) + .hasNoLeaks(); + payload1.release(); + + stateAssert.hasSubscribedFlag().hasRequestN(1).hasFirstFrameSentFlag(); + activeStreams.assertHasStream(1, requestChannelRequesterFlux); + + publisher.assertMaxRequested(1); + + Payload nextPayload = TestRequesterResponderSupport.genericPayload(allocator); + requestChannelRequesterFlux.handlePayload(nextPayload); + + Payload unrequestedPayload = TestRequesterResponderSupport.genericPayload(allocator); + requestChannelRequesterFlux.handlePayload(unrequestedPayload); + + final ByteBuf cancelFrame = sender.awaitFrame(); + FrameAssert.assertThat(cancelFrame) + .isNotNull() + .typeOf(CANCEL) + .hasClientSideStreamId() + .hasStreamId(1) + .hasNoLeaks(); + + assertSubscriber + .assertValuesWith(p -> PayloadAssert.assertThat(p).isSameAs(nextPayload).hasNoLeaks()) + .assertError() + .assertErrorMessage("The number of messages received exceeds the number requested"); + + publisher.assertWasCancelled(); + + activeStreams.assertNoActiveStreams(); + // state machine check + stateAssert.isTerminated(); + Assertions.assertThat(sender.isEmpty()).isTrue(); + } + /* * +--------------------------------+ * | Racing Test Cases | diff --git a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java index 32af4e3b6..890458caf 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java @@ -263,6 +263,143 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String comp allocator.assertHasNoLeaks(); } + @Test + public void failOnOverflow() { + final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); + final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + final Payload firstPayload = TestRequesterResponderSupport.genericPayload(allocator); + final TestPublisher publisher = TestPublisher.create(); + + final RequestChannelResponderSubscriber requestChannelResponderSubscriber = + new RequestChannelResponderSubscriber(1, 1, firstPayload, activeStreams); + final StateAssert stateAssert = + StateAssert.assertThat(requestChannelResponderSubscriber); + activeStreams.activeStreams.put(1, requestChannelResponderSubscriber); + + // state machine check + stateAssert.isUnsubscribed().hasRequestN(0); + activeStreams.assertHasStream(1, requestChannelResponderSubscriber); + + publisher.subscribe(requestChannelResponderSubscriber); + publisher.assertMaxRequested(1); + // state machine check + stateAssert.isUnsubscribed().hasRequestN(0); + + final AssertSubscriber assertSubscriber = + requestChannelResponderSubscriber.subscribeWith(AssertSubscriber.create(0)); + Assertions.assertThat(firstPayload.refCnt()).isOne(); + + // state machine check + stateAssert.hasSubscribedFlagOnly().hasRequestN(0); + + assertSubscriber.request(1); + + // state machine check + stateAssert.hasSubscribedFlag().hasFirstFrameSentFlag().hasRequestN(1); + + // should not send requestN since 1 is remaining + Assertions.assertThat(sender.isEmpty()).isTrue(); + + assertSubscriber.request(1); + + stateAssert.hasSubscribedFlag().hasRequestN(2).hasFirstFrameSentFlag(); + + // should not send requestN since 1 is remaining + FrameAssert.assertThat(sender.awaitFrame()) + .typeOf(REQUEST_N) + .hasStreamId(1) + .hasRequestN(1) + .hasNoLeaks(); + + Payload nextPayload = TestRequesterResponderSupport.genericPayload(allocator); + requestChannelResponderSubscriber.handlePayload(nextPayload); + + Payload unrequestedPayload = TestRequesterResponderSupport.genericPayload(allocator); + requestChannelResponderSubscriber.handlePayload(unrequestedPayload); + + final ByteBuf cancelErrorFrame = sender.awaitFrame(); + FrameAssert.assertThat(cancelErrorFrame) + .isNotNull() + .typeOf(ERROR) + .hasData("The number of messages received exceeds the number requested") + .hasClientSideStreamId() + .hasStreamId(1) + .hasNoLeaks(); + + assertSubscriber + .assertValuesWith( + p -> PayloadAssert.assertThat(p).isSameAs(firstPayload).hasNoLeaks(), + p -> PayloadAssert.assertThat(p).isSameAs(nextPayload).hasNoLeaks()) + .assertErrorMessage("The number of messages received exceeds the number requested"); + + Assertions.assertThat(firstPayload.refCnt()).isZero(); + Assertions.assertThat(nextPayload.refCnt()).isZero(); + Assertions.assertThat(unrequestedPayload.refCnt()).isZero(); + stateAssert.isTerminated(); + activeStreams.assertNoActiveStreams(); + + Assertions.assertThat(sender.isEmpty()).isTrue(); + allocator.assertHasNoLeaks(); + } + + @Test + public void failOnOverflowBeforeFirstPayloadIsSent() { + final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); + final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + final Payload firstPayload = TestRequesterResponderSupport.genericPayload(allocator); + final TestPublisher publisher = TestPublisher.create(); + + final RequestChannelResponderSubscriber requestChannelResponderSubscriber = + new RequestChannelResponderSubscriber(1, 1, firstPayload, activeStreams); + final StateAssert stateAssert = + StateAssert.assertThat(requestChannelResponderSubscriber); + activeStreams.activeStreams.put(1, requestChannelResponderSubscriber); + + // state machine check + stateAssert.isUnsubscribed().hasRequestN(0); + activeStreams.assertHasStream(1, requestChannelResponderSubscriber); + + publisher.subscribe(requestChannelResponderSubscriber); + publisher.assertMaxRequested(1); + // state machine check + stateAssert.isUnsubscribed().hasRequestN(0); + + final AssertSubscriber assertSubscriber = + requestChannelResponderSubscriber.subscribeWith(AssertSubscriber.create(0)); + Assertions.assertThat(firstPayload.refCnt()).isOne(); + + // state machine check + stateAssert.hasSubscribedFlagOnly().hasRequestN(0); + + Payload unrequestedPayload = TestRequesterResponderSupport.genericPayload(allocator); + requestChannelResponderSubscriber.handlePayload(unrequestedPayload); + + final ByteBuf cancelErrorFrame = sender.awaitFrame(); + FrameAssert.assertThat(cancelErrorFrame) + .isNotNull() + .typeOf(ERROR) + .hasData("The number of messages received exceeds the number requested") + .hasClientSideStreamId() + .hasStreamId(1) + .hasNoLeaks(); + + assertSubscriber.request(1); + + assertSubscriber + .assertValuesWith(p -> PayloadAssert.assertThat(p).isSameAs(firstPayload).hasNoLeaks()) + .assertErrorMessage("The number of messages received exceeds the number requested"); + + Assertions.assertThat(firstPayload.refCnt()).isZero(); + Assertions.assertThat(unrequestedPayload.refCnt()).isZero(); + stateAssert.isTerminated(); + activeStreams.assertNoActiveStreams(); + + Assertions.assertThat(sender.isEmpty()).isTrue(); + allocator.assertHasNoLeaks(); + } + /* * +--------------------------------+ * | Racing Test Cases | @@ -664,7 +801,7 @@ public void shouldHaveNoLeaksOnReassemblyAndCancelRacing(String terminationMode) ; final TestPublisher publisher = TestPublisher.createNoncompliant(DEFER_CANCELLATION, CLEANUP_ON_TERMINATE); - final AssertSubscriber assertSubscriber = new AssertSubscriber<>(1); + final AssertSubscriber assertSubscriber = new AssertSubscriber<>(2); Payload firstPayload = TestRequesterResponderSupport.genericPayload(allocator); final RequestChannelResponderSubscriber requestOperator = @@ -725,8 +862,17 @@ public void shouldHaveNoLeaksOnReassemblyAndCancelRacing(String terminationMode) assertSubscriber.assertTerminated().assertError(); } - final ByteBuf frame = sender.awaitFrame(); - FrameAssert.assertThat(frame) + final ByteBuf requstFrame = sender.awaitFrame(); + FrameAssert.assertThat(requstFrame) + .isNotNull() + .typeOf(REQUEST_N) + .hasRequestN(1) + .hasClientSideStreamId() + .hasStreamId(1) + .hasNoLeaks(); + + final ByteBuf terminalFrame = sender.awaitFrame(); + FrameAssert.assertThat(terminalFrame) .isNotNull() .typeOf(terminationMode.equals("cancel") ? CANCEL : ERROR) .hasClientSideStreamId() diff --git a/rsocket-core/src/test/java/io/rsocket/core/RequestStreamRequesterFluxTest.java b/rsocket-core/src/test/java/io/rsocket/core/RequestStreamRequesterFluxTest.java index 88dd5441e..8702d1a80 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequestStreamRequesterFluxTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequestStreamRequesterFluxTest.java @@ -926,7 +926,7 @@ public void shouldErrorOnIncorrectRefCntInGivenPayloadLatePhase() { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); final TestDuplexConnection sender = activeStreams.getDuplexConnection(); - ; + final Payload payload = ByteBufPayload.create(""); final RequestStreamRequesterFlux requestStreamRequesterFlux = @@ -1129,6 +1129,87 @@ static Stream> shouldErrorIfNoAvailabilityS .isInstanceOf(RuntimeException.class)); } + @Test + public void failOnOverflow() { + final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); + final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); + final TestDuplexConnection sender = activeStreams.getDuplexConnection(); + final Payload payload = TestRequesterResponderSupport.genericPayload(allocator); + + final RequestStreamRequesterFlux requestStreamRequesterFlux = + new RequestStreamRequesterFlux(payload, activeStreams); + final StateAssert stateAssert = + StateAssert.assertThat(requestStreamRequesterFlux); + + // state machine check + + stateAssert.isUnsubscribed(); + activeStreams.assertNoActiveStreams(); + + final AssertSubscriber assertSubscriber = + requestStreamRequesterFlux.subscribeWith(AssertSubscriber.create(0)); + Assertions.assertThat(payload.refCnt()).isOne(); + activeStreams.assertNoActiveStreams(); + // state machine check + stateAssert.hasSubscribedFlagOnly(); + + assertSubscriber.request(1); + + Assertions.assertThat(payload.refCnt()).isZero(); + activeStreams.assertHasStream(1, requestStreamRequesterFlux); + + // state machine check + stateAssert.hasSubscribedFlag().hasRequestN(1).hasFirstFrameSentFlag(); + + final ByteBuf frame = sender.awaitFrame(); + FrameAssert.assertThat(frame) + .isNotNull() + .hasPayloadSize( + "testData".getBytes(CharsetUtil.UTF_8).length + + "testMetadata".getBytes(CharsetUtil.UTF_8).length) + .hasMetadata("testMetadata") + .hasData("testData") + .hasNoFragmentsFollow() + .hasRequestN(1) + .typeOf(FrameType.REQUEST_STREAM) + .hasClientSideStreamId() + .hasStreamId(1) + .hasNoLeaks(); + + Assertions.assertThat(sender.isEmpty()).isTrue(); + + Payload requestedPayload = TestRequesterResponderSupport.randomPayload(allocator); + requestStreamRequesterFlux.handlePayload(requestedPayload); + + Payload unrequestedPayload = TestRequesterResponderSupport.randomPayload(allocator); + requestStreamRequesterFlux.handlePayload(unrequestedPayload); + + final ByteBuf cancelFrame = sender.awaitFrame(); + FrameAssert.assertThat(cancelFrame) + .isNotNull() + .typeOf(FrameType.CANCEL) + .hasClientSideStreamId() + .hasStreamId(1) + .hasNoLeaks(); + + assertSubscriber + .assertValuesWith(p -> PayloadAssert.assertThat(p).isEqualTo(requestedPayload).hasNoLeaks()) + .assertError() + .assertErrorMessage("The number of messages received exceeds the number requested"); + + PayloadAssert.assertThat(requestedPayload).isReleased(); + PayloadAssert.assertThat(unrequestedPayload).isReleased(); + + Assertions.assertThat(payload.refCnt()).isZero(); + activeStreams.assertNoActiveStreams(); + + Assertions.assertThat(sender.isEmpty()).isTrue(); + + // state machine check + stateAssert.isTerminated(); + allocator.assertHasNoLeaks(); + } + @Test public void checkName() { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); From f15c14a06bd5b36db532e059c678f02aa05d08be Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 13 Sep 2022 19:25:37 +0300 Subject: [PATCH 145/183] Adds reflection hints for native-image support 1.1.x (#1073) --- .../rsocket-core/reflect-config.json | 130 ++++++++++++++++++ .../reflect-config.json | 16 +++ 2 files changed, 146 insertions(+) create mode 100644 rsocket-core/src/main/resources/META-INF/native-image/io.rsocket/rsocket-core/reflect-config.json create mode 100644 rsocket-transport-netty/src/main/resources/META-INF/native-image/io.rsocket/rsocket-transport-netty/reflect-config.json diff --git a/rsocket-core/src/main/resources/META-INF/native-image/io.rsocket/rsocket-core/reflect-config.json b/rsocket-core/src/main/resources/META-INF/native-image/io.rsocket/rsocket-core/reflect-config.json new file mode 100644 index 000000000..0a3844451 --- /dev/null +++ b/rsocket-core/src/main/resources/META-INF/native-image/io.rsocket/rsocket-core/reflect-config.json @@ -0,0 +1,130 @@ +[ + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.BaseLinkedQueueConsumerNodeRef" + }, + "name": "io.rsocket.internal.jctools.queues.BaseLinkedQueueConsumerNodeRef", + "fields": [ + { + "name": "consumerNode" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.BaseLinkedQueueProducerNodeRef" + }, + "name": "io.rsocket.internal.jctools.queues.BaseLinkedQueueProducerNodeRef", + "fields": [ + { + "name": "producerNode" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields" + }, + "name": "io.rsocket.internal.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields", + "fields": [ + { + "name": "producerLimit" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.BaseMpscLinkedArrayQueueConsumerFields" + }, + "name": "io.rsocket.internal.jctools.queues.BaseMpscLinkedArrayQueueConsumerFields", + "fields": [ + { + "name": "consumerIndex" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.BaseMpscLinkedArrayQueueProducerFields" + }, + "name": "io.rsocket.internal.jctools.queues.BaseMpscLinkedArrayQueueProducerFields", + "fields": [ + { + "name": "producerIndex" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.LinkedQueueNode" + }, + "name": "io.rsocket.internal.jctools.queues.LinkedQueueNode", + "fields": [ + { + "name": "next" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.MpscArrayQueueConsumerIndexField" + }, + "name": "io.rsocket.internal.jctools.queues.MpscArrayQueueConsumerIndexField", + "fields": [ + { + "name": "consumerIndex" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.MpscArrayQueueProducerIndexField" + }, + "name": "io.rsocket.internal.jctools.queues.MpscArrayQueueProducerIndexField", + "fields": [ + { + "name": "producerIndex" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.MpscArrayQueueProducerLimitField" + }, + "name": "io.rsocket.internal.jctools.queues.MpscArrayQueueProducerLimitField", + "fields": [ + { + "name": "producerLimit" + } + ] + }, + { + "condition": { + "typeReachable": "io.rsocket.internal.jctools.queues.UnsafeAccess" + }, + "name": "sun.misc.Unsafe", + "fields": [ + { + "name": "theUnsafe" + } + ], + "queriedMethods": [ + { + "name": "getAndAddLong", + "parameterTypes": [ + "java.lang.Object", + "long", + "long" + ] + }, + { + "name": "getAndSetObject", + "parameterTypes": [ + "java.lang.Object", + "long", + "java.lang.Object" + ] + } + ] + } +] \ No newline at end of file diff --git a/rsocket-transport-netty/src/main/resources/META-INF/native-image/io.rsocket/rsocket-transport-netty/reflect-config.json b/rsocket-transport-netty/src/main/resources/META-INF/native-image/io.rsocket/rsocket-transport-netty/reflect-config.json new file mode 100644 index 000000000..3a2baa440 --- /dev/null +++ b/rsocket-transport-netty/src/main/resources/META-INF/native-image/io.rsocket/rsocket-transport-netty/reflect-config.json @@ -0,0 +1,16 @@ +[ + { + "condition": { + "typeReachable": "io.rsocket.transport.netty.RSocketLengthCodec" + }, + "name": "io.rsocket.transport.netty.RSocketLengthCodec", + "queryAllPublicMethods": true + }, + { + "condition": { + "typeReachable": "io.rsocket.transport.netty.server.BaseWebsocketServerTransport$PongHandler" + }, + "name": "io.rsocket.transport.netty.server.BaseWebsocketServerTransport$PongHandler", + "queryAllPublicMethods": true + } +] \ No newline at end of file From bde2a1b03f77c4f9165a202b0da00756b543d50c Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 13 Sep 2022 21:22:18 +0300 Subject: [PATCH 146/183] updates versions Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- build.gradle | 12 ++++++------ gradle.properties | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index a3ff46a4f..8399c27e1 100644 --- a/build.gradle +++ b/build.gradle @@ -16,8 +16,8 @@ plugins { id 'com.github.sherter.google-java-format' version '0.9' apply false - id 'me.champeau.jmh' version '0.6.6' apply false - id 'io.spring.dependency-management' version '1.0.11.RELEASE' apply false + id 'me.champeau.jmh' version '0.6.7' apply false + id 'io.spring.dependency-management' version '1.0.13.RELEASE' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false id 'io.github.reyerizo.gradle.jcstress' version '0.8.13' apply false id 'com.github.vlsi.gradle-extensions' version '1.76' apply false @@ -33,14 +33,14 @@ subprojects { apply plugin: 'com.github.sherter.google-java-format' apply plugin: 'com.github.vlsi.gradle-extensions' - ext['reactor-bom.version'] = '2020.0.17' + ext['reactor-bom.version'] = '2020.0.23' ext['logback.version'] = '1.2.10' - ext['netty-bom.version'] = '4.1.75.Final' - ext['netty-boringssl.version'] = '2.0.51.Final' + ext['netty-bom.version'] = '4.1.81.Final' + ext['netty-boringssl.version'] = '2.0.54.Final' ext['hdrhistogram.version'] = '2.1.12' ext['mockito.version'] = '4.4.0' ext['slf4j.version'] = '1.7.36' - ext['jmh.version'] = '1.33' + ext['jmh.version'] = '1.35' ext['junit.version'] = '5.8.1' ext['micrometer.version'] = '1.8.4' ext['assertj.version'] = '3.22.0' diff --git a/gradle.properties b/gradle.properties index e9219dfe6..7b5ac2349 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.1.2 -perfBaselineVersion=1.1.1 +version=1.1.3 +perfBaselineVersion=1.1.2 From 84af16070106f2727cb906ad4551e75b6cdc48d4 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Wed, 14 Sep 2022 00:56:34 +0300 Subject: [PATCH 147/183] Adapts to ObservationConvention location change (#1071) --- .../ObservationIntegrationTest.java | 12 +++++++++++- .../RSocketDocumentedObservation.java | 19 ++++++++++--------- ...RSocketRequesterObservationConvention.java | 5 +++-- ...RSocketResponderObservationConvention.java | 5 +++-- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java index 2bf5e42e7..870ecf0cd 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/observation/ObservationIntegrationTest.java @@ -69,7 +69,7 @@ public class ObservationIntegrationTest extends SampleTestRunner { private final RSocketInterceptor responderInterceptor; ObservationIntegrationTest() { - super(SampleRunnerConfig.builder().build(), observationRegistry, registry); + super(SampleRunnerConfig.builder().build()); requesterInterceptor = reactiveSocket -> new ObservationRequesterRSocketProxy(reactiveSocket, observationRegistry); @@ -233,4 +233,14 @@ public Mono fireAndForget(Payload payload) { // @formatter:on }; } + + @Override + protected MeterRegistry getMeterRegistry() { + return registry; + } + + @Override + protected ObservationRegistry getObservationRegistry() { + return observationRegistry; + } } diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java index 18440ad81..5d6661baf 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java @@ -18,6 +18,7 @@ import io.micrometer.common.docs.KeyName; import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.docs.DocumentedObservation; enum RSocketDocumentedObservation implements DocumentedObservation { @@ -25,7 +26,7 @@ enum RSocketDocumentedObservation implements DocumentedObservation { /** Observation created on the RSocket responder side. */ RSOCKET_RESPONDER { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketResponderObservationConvention.class; } @@ -34,7 +35,7 @@ enum RSocketDocumentedObservation implements DocumentedObservation { /** Observation created on the RSocket requester side for Fire and Forget frame type. */ RSOCKET_REQUESTER_FNF { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketRequesterObservationConvention.class; } @@ -53,7 +54,7 @@ public String getPrefix() { /** Observation created on the RSocket responder side for Fire and Forget frame type. */ RSOCKET_RESPONDER_FNF { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketResponderObservationConvention.class; } @@ -72,7 +73,7 @@ public String getPrefix() { /** Observation created on the RSocket requester side for Request Response frame type. */ RSOCKET_REQUESTER_REQUEST_RESPONSE { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketRequesterObservationConvention.class; } @@ -91,7 +92,7 @@ public String getPrefix() { /** Observation created on the RSocket responder side for Request Response frame type. */ RSOCKET_RESPONDER_REQUEST_RESPONSE { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketResponderObservationConvention.class; } @@ -110,7 +111,7 @@ public String getPrefix() { /** Observation created on the RSocket requester side for Request Stream frame type. */ RSOCKET_REQUESTER_REQUEST_STREAM { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketRequesterObservationConvention.class; } @@ -129,7 +130,7 @@ public String getPrefix() { /** Observation created on the RSocket responder side for Request Stream frame type. */ RSOCKET_RESPONDER_REQUEST_STREAM { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketResponderObservationConvention.class; } @@ -148,7 +149,7 @@ public String getPrefix() { /** Observation created on the RSocket requester side for Request Channel frame type. */ RSOCKET_REQUESTER_REQUEST_CHANNEL { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketRequesterObservationConvention.class; } @@ -167,7 +168,7 @@ public String getPrefix() { /** Observation created on the RSocket responder side for Request Channel frame type. */ RSOCKET_RESPONDER_REQUEST_CHANNEL { @Override - public Class> + public Class> getDefaultConvention() { return DefaultRSocketResponderObservationConvention.class; } diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java index 512c19abf..83bbef9b1 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java @@ -17,15 +17,16 @@ package io.rsocket.micrometer.observation; import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; /** - * {@link Observation.ObservationConvention} for RSocket requester {@link RSocketContext}. + * {@link ObservationConvention} for RSocket requester {@link RSocketContext}. * * @author Marcin Grzejszczak * @since 2.0.0 */ public interface RSocketRequesterObservationConvention - extends Observation.ObservationConvention { + extends ObservationConvention { @Override default boolean supportsContext(Observation.Context context) { diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java index 9599429f5..f2bbf8716 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java @@ -17,15 +17,16 @@ package io.rsocket.micrometer.observation; import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; /** - * {@link Observation.ObservationConvention} for RSocket responder {@link RSocketContext}. + * {@link ObservationConvention} for RSocket responder {@link RSocketContext}. * * @author Marcin Grzejszczak * @since 2.0.0 */ public interface RSocketResponderObservationConvention - extends Observation.ObservationConvention { + extends ObservationConvention { @Override default boolean supportsContext(Observation.Context context) { From ac96b8ebb324c08916cb17dbba55ec4cead8dfe9 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 30 Aug 2022 12:02:46 +0300 Subject: [PATCH 148/183] introduces `onClose` listener for RSocketClient Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .../rsocket/core/ReconnectMonoStressTest.java | 2 +- .../io/rsocket/core/DefaultRSocketClient.java | 15 ++++ .../java/io/rsocket/core/RSocketClient.java | 9 ++- .../io/rsocket/core/RSocketClientAdapter.java | 5 ++ .../loadbalance/LoadbalanceRSocketClient.java | 5 ++ .../io/rsocket/loadbalance/PooledRSocket.java | 23 +++++- .../io/rsocket/loadbalance/RSocketPool.java | 19 ++++- .../core/DefaultRSocketClientTests.java | 52 ++++++++++-- .../rsocket/loadbalance/LoadbalanceTest.java | 80 +++++++++++++++++++ 9 files changed, 199 insertions(+), 11 deletions(-) diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java index e01b1d704..1d3b72170 100644 --- a/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java @@ -517,7 +517,7 @@ public void arbiter(IIIIII_Result r) { id = {"1, 0, 1, 0, 1, 2"}, expect = ACCEPTABLE) @State - public static class SubscribeBlockRace extends BaseStressTest { + public static class SubscribeBlockConnectRace extends BaseStressTest { String receivedValue; diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java index 4dc250158..5119814cd 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java @@ -35,6 +35,7 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.MonoOperator; import reactor.core.publisher.Operators; +import reactor.core.publisher.Sinks; import reactor.util.annotation.Nullable; import reactor.util.context.Context; @@ -65,6 +66,8 @@ class DefaultRSocketClient extends ResolvingOperator final Mono source; + final Sinks.Empty onDisposeSink; + volatile Subscription s; static final AtomicReferenceFieldUpdater S = @@ -72,12 +75,18 @@ class DefaultRSocketClient extends ResolvingOperator DefaultRSocketClient(Mono source) { this.source = unwrapReconnectMono(source); + this.onDisposeSink = Sinks.empty(); } private Mono unwrapReconnectMono(Mono source) { return source instanceof ReconnectMono ? ((ReconnectMono) source).getSource() : source; } + @Override + public Mono onClose() { + return this.onDisposeSink.asMono(); + } + @Override public Mono source() { return Mono.fromDirect(this); @@ -194,6 +203,12 @@ protected void doOnValueExpired(RSocket value) { @Override protected void doOnDispose() { Operators.terminate(S, this); + final RSocket value = this.value; + if (value != null) { + value.onClose().subscribe(null, onDisposeSink::tryEmitError, onDisposeSink::tryEmitEmpty); + } else { + onDisposeSink.tryEmitEmpty(); + } } static final class FlatMapMain implements CoreSubscriber, Context, Scannable { diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java index 81392e661..b21a06b8b 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java @@ -15,12 +15,13 @@ */ package io.rsocket.core; +import io.rsocket.Closeable; import io.rsocket.Payload; import io.rsocket.RSocket; import org.reactivestreams.Publisher; -import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import sun.reflect.generics.reflectiveObjects.NotImplementedException; /** * Contract for performing RSocket requests. @@ -74,7 +75,11 @@ * @since 1.1 * @see io.rsocket.loadbalance.LoadbalanceRSocketClient */ -public interface RSocketClient extends Disposable { +public interface RSocketClient extends Closeable { + + default Mono onClose() { + return Mono.error(new NotImplementedException()); + } /** Return the underlying source used to obtain a shared {@link RSocket} connection. */ Mono source(); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java index cc94f4102..1537da3f8 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java @@ -46,6 +46,11 @@ public Mono source() { return Mono.just(rsocket); } + @Override + public Mono onClose() { + return rsocket.onClose(); + } + @Override public Mono fireAndForget(Mono payloadMono) { return payloadMono.flatMap(rsocket::fireAndForget); diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java index 1b677edba..0f70df06a 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java @@ -40,6 +40,11 @@ private LoadbalanceRSocketClient(RSocketPool rSocketPool) { this.rSocketPool = rSocketPool; } + @Override + public Mono onClose() { + return rSocketPool.onClose(); + } + /** Return {@code Mono} that selects an RSocket from the underlying pool. */ @Override public Mono source() { diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java index 1e7f09ec4..a77329d31 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/PooledRSocket.java @@ -26,6 +26,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; +import reactor.core.publisher.Sinks; import reactor.util.context.Context; /** Default implementation of {@link RSocket} stored in {@link RSocketPool} */ @@ -35,6 +36,7 @@ final class PooledRSocket extends ResolvingOperator final RSocketPool parent; final Mono rSocketSource; final LoadbalanceTarget loadbalanceTarget; + final Sinks.Empty onCloseSink; volatile Subscription s; @@ -46,6 +48,7 @@ final class PooledRSocket extends ResolvingOperator this.parent = parent; this.rSocketSource = rSocketSource; this.loadbalanceTarget = loadbalanceTarget; + this.onCloseSink = Sinks.unsafe().empty(); } @Override @@ -155,6 +158,12 @@ void doCleanup(Throwable t) { break; } } + + if (t == ON_DISPOSE) { + this.onCloseSink.tryEmitEmpty(); + } else { + this.onCloseSink.tryEmitError(t); + } } @Override @@ -165,6 +174,13 @@ protected void doOnValueExpired(RSocket value) { @Override protected void doOnDispose() { Operators.terminate(S, this); + + final RSocket value = this.value; + if (value != null) { + value.onClose().subscribe(null, onCloseSink::tryEmitError, onCloseSink::tryEmitEmpty); + } else { + onCloseSink.tryEmitEmpty(); + } } @Override @@ -193,7 +209,12 @@ public Mono metadataPush(Payload payload) { } LoadbalanceTarget target() { - return loadbalanceTarget; + return this.loadbalanceTarget; + } + + @Override + public Mono onClose() { + return this.onCloseSink.asMono(); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java index bf6f53830..59d9678d0 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/RSocketPool.java @@ -16,6 +16,7 @@ package io.rsocket.loadbalance; import io.netty.util.ReferenceCountUtil; +import io.rsocket.Closeable; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.core.RSocketConnector; @@ -28,16 +29,18 @@ import java.util.ListIterator; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.stream.Collectors; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; +import reactor.core.publisher.Sinks; import reactor.util.annotation.Nullable; class RSocketPool extends ResolvingOperator - implements CoreSubscriber> { + implements CoreSubscriber>, Closeable { static final AtomicReferenceFieldUpdater ACTIVE_SOCKETS = AtomicReferenceFieldUpdater.newUpdater( @@ -49,6 +52,7 @@ class RSocketPool extends ResolvingOperator final DeferredResolutionRSocket deferredResolutionRSocket = new DeferredResolutionRSocket(this); final RSocketConnector connector; final LoadbalanceStrategy loadbalanceStrategy; + final Sinks.Empty onAllClosedSink = Sinks.unsafe().empty(); volatile PooledRSocket[] activeSockets; volatile Subscription s; @@ -64,6 +68,11 @@ public RSocketPool( targetPublisher.subscribe(this); } + @Override + public Mono onClose() { + return onAllClosedSink.asMono(); + } + @Override protected void doOnDispose() { Operators.terminate(S, this); @@ -72,6 +81,14 @@ protected void doOnDispose() { for (RSocket rSocket : activeSockets) { rSocket.dispose(); } + + if (activeSockets.length > 0) { + Mono.whenDelayError( + Arrays.stream(activeSockets).map(RSocket::onClose).collect(Collectors.toList())) + .subscribe(null, onAllClosedSink::tryEmitError, onAllClosedSink::tryEmitEmpty); + } else { + onAllClosedSink.tryEmitEmpty(); + } } @Override diff --git a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java index 9085f1d8f..56ddc2456 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -29,6 +29,7 @@ import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.subscriber.AssertSubscriber; import io.rsocket.util.ByteBufPayload; +import io.rsocket.util.RSocketProxy; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; @@ -457,6 +458,43 @@ public void shouldDisposeOriginalSource() { Assertions.assertThat(rule.socket.isDisposed()).isTrue(); } + @Test + public void shouldReceiveOnCloseNotificationOnDisposeOriginalSource() { + Sinks.Empty onCloseDelayer = Sinks.empty(); + ClientSocketRule rule = + new ClientSocketRule() { + @Override + protected RSocket newRSocket() { + return new RSocketProxy(super.newRSocket()) { + @Override + public Mono onClose() { + return super.onClose().and(onCloseDelayer.asMono()); + } + }; + } + }; + rule.init(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + rule.client.source().subscribe(assertSubscriber); + rule.delayer.run(); + assertSubscriber.assertTerminated().assertValueCount(1); + + rule.client.dispose(); + + Assertions.assertThat(rule.client.isDisposed()).isTrue(); + + AssertSubscriber onCloseSubscriber = AssertSubscriber.create(); + + rule.client.onClose().subscribe(onCloseSubscriber); + onCloseSubscriber.assertNotTerminated(); + + onCloseDelayer.tryEmitEmpty(); + + onCloseSubscriber.assertTerminated().assertComplete(); + + Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + } + @Test public void shouldDisposeOriginalSourceIfRacing() { for (int i = 0; i < RaceTestConstants.REPEATS; i++) { @@ -485,7 +523,7 @@ public void shouldDisposeOriginalSourceIfRacing() { } } - public static class ClientSocketRule extends AbstractSocketRule { + public static class ClientSocketRule extends AbstractSocketRule { protected RSocketClient client; protected Runnable delayer; @@ -498,14 +536,16 @@ protected void doInit() { producer = Sinks.one(); client = new DefaultRSocketClient( - producer - .asMono() - .doOnCancel(() -> socket.dispose()) - .doOnDiscard(Disposable.class, Disposable::dispose)); + Mono.defer( + () -> + producer + .asMono() + .doOnCancel(() -> socket.dispose()) + .doOnDiscard(Disposable.class, Disposable::dispose))); } @Override - protected RSocketRequester newRSocket() { + protected RSocket newRSocket() { return new RSocketRequester( connection, PayloadDecoder.ZERO_COPY, diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java index 5780737cc..fcd3ae4a9 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java @@ -19,6 +19,7 @@ import io.rsocket.RSocket; import io.rsocket.RaceTestConstants; import io.rsocket.core.RSocketConnector; +import io.rsocket.internal.subscriber.AssertSubscriber; import io.rsocket.plugins.RSocketInterceptor; import io.rsocket.test.util.TestClientTransport; import io.rsocket.transport.ClientTransport; @@ -319,6 +320,85 @@ public Flux requestChannel(Publisher source) { Assertions.assertThat(counter.get()).isEqualTo(3); } + @Test + public void shouldNotifyOnCloseWhenAllTheActiveSubscribersAreClosed() { + final AtomicInteger counter = new AtomicInteger(); + final ClientTransport mockTransport = Mockito.mock(ClientTransport.class); + final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + + Sinks.Empty onCloseSocket1 = Sinks.empty(); + Sinks.Empty onCloseSocket2 = Sinks.empty(); + + RSocket socket1 = + new RSocket() { + @Override + public Mono onClose() { + return onCloseSocket1.asMono(); + } + + @Override + public Mono fireAndForget(Payload payload) { + return Mono.empty(); + } + }; + RSocket socket2 = + new RSocket() { + @Override + public Mono onClose() { + return onCloseSocket2.asMono(); + } + + @Override + public Mono fireAndForget(Payload payload) { + return Mono.empty(); + } + }; + + Mockito.when(rSocketConnectorMock.connect(Mockito.any(ClientTransport.class))) + .then(im -> Mono.just(socket1)) + .then(im -> Mono.just(socket2)) + .then(im -> Mono.never().doOnCancel(() -> counter.incrementAndGet())); + + final TestPublisher> source = TestPublisher.create(); + final RSocketPool rSocketPool = + new RSocketPool(rSocketConnectorMock, source, new RoundRobinLoadbalanceStrategy()); + + source.next( + Arrays.asList( + LoadbalanceTarget.from("1", mockTransport), + LoadbalanceTarget.from("2", mockTransport), + LoadbalanceTarget.from("3", mockTransport))); + + StepVerifier.create(rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE)) + .expectSubscription() + .expectComplete() + .verify(Duration.ofSeconds(2)); + + StepVerifier.create(rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE)) + .expectSubscription() + .expectComplete() + .verify(Duration.ofSeconds(2)); + + rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); + + rSocketPool.dispose(); + + AssertSubscriber onCloseSubscriber = + rSocketPool.onClose().subscribeWith(AssertSubscriber.create()); + + onCloseSubscriber.assertNotTerminated(); + + onCloseSocket1.tryEmitEmpty(); + + onCloseSubscriber.assertNotTerminated(); + + onCloseSocket2.tryEmitEmpty(); + + onCloseSubscriber.assertTerminated().assertComplete(); + + Assertions.assertThat(counter.get()).isOne(); + } + static class TestRSocket extends RSocketProxy { final Sinks.Empty sink = Sinks.empty(); From 01f54580b657e373398c06908fcfef0de3d762e0 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 9 Sep 2022 13:07:33 +0300 Subject: [PATCH 149/183] introduces `.connect` method Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Co-authored-by: Rossen Stoyanchev Signed-off-by: Oleh Dokuka --- .../rsocket/core/ReconnectMonoStressTest.java | 5 + .../java/io/rsocket/core/RSocketClient.java | 11 ++ .../io/rsocket/core/RSocketClientAdapter.java | 5 + .../io/rsocket/core/ResolvingOperator.java | 24 ++++ .../loadbalance/LoadbalanceRSocketClient.java | 5 + .../loadbalance/ResolvingOperator.java | 24 ++++ .../core/DefaultRSocketClientTests.java | 114 ++++++++++++++++++ 7 files changed, 188 insertions(+) diff --git a/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java index 1d3b72170..ef79d344d 100644 --- a/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java @@ -543,6 +543,11 @@ void subscribe() { reconnectMono.subscribe(stressSubscriber); } + @Actor + void connect() { + reconnectMono.resolvingInner.connect(); + } + @Arbiter public void arbiter(IIIIII_Result r) { r.r1 = stressSubscription.subscribes; diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java index b21a06b8b..32e3c229d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java @@ -77,6 +77,17 @@ */ public interface RSocketClient extends Closeable { + /** + * Connect to the remote rsocket endpoint, if not yet connected. This method is a shortcut for + * {@code RSocketClient#source().subscribe()}. + * + * @return {@code true} if an attempt to connect was triggered or if already connected, or {@code + * false} if the client is terminated. + */ + default boolean connect() { + throw new NotImplementedException(); + } + default Mono onClose() { return Mono.error(new NotImplementedException()); } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java index 1537da3f8..ae8b7da97 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketClientAdapter.java @@ -41,6 +41,11 @@ public RSocket rsocket() { return rsocket; } + @Override + public boolean connect() { + throw new UnsupportedOperationException("Connect does not apply to a server side RSocket"); + } + @Override public Mono source() { return Mono.just(rsocket); diff --git a/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java b/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java index 85c4a17a7..50bef5b70 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java @@ -331,6 +331,30 @@ protected void doOnDispose() { // no ops } + public final boolean connect() { + for (; ; ) { + final BiConsumer[] a = this.subscribers; + + if (a == TERMINATED) { + return false; + } + + if (a == READY) { + return true; + } + + if (a != EMPTY_UNSUBSCRIBED) { + // do nothing if already started + return true; + } + + if (SUBSCRIBERS.compareAndSet(this, a, EMPTY_SUBSCRIBED)) { + this.doSubscribe(); + return true; + } + } + } + final int add(BiConsumer ps) { for (; ; ) { BiConsumer[] a = this.subscribers; diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java index 0f70df06a..21ea3d836 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java @@ -45,6 +45,11 @@ public Mono onClose() { return rSocketPool.onClose(); } + @Override + public boolean connect() { + return rSocketPool.connect(); + } + /** Return {@code Mono} that selects an RSocket from the underlying pool. */ @Override public Mono source() { diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/ResolvingOperator.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/ResolvingOperator.java index a25bcc584..52f16e166 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/ResolvingOperator.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/ResolvingOperator.java @@ -327,6 +327,30 @@ protected void doOnDispose() { // no ops } + public final boolean connect() { + for (; ; ) { + final BiConsumer[] a = this.subscribers; + + if (a == TERMINATED) { + return false; + } + + if (a == READY) { + return true; + } + + if (a != EMPTY_UNSUBSCRIBED) { + // do nothing if already started + return true; + } + + if (SUBSCRIBERS.compareAndSet(this, a, EMPTY_SUBSCRIBED)) { + this.doSubscribe(); + return true; + } + } + } + final int add(BiConsumer ps) { for (; ; ) { BiConsumer[] a = this.subscribers; diff --git a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java index 56ddc2456..3c95fd65c 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -28,6 +28,7 @@ import io.rsocket.frame.PayloadFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.subscriber.AssertSubscriber; +import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.ByteBufPayload; import io.rsocket.util.RSocketProxy; import java.time.Duration; @@ -495,6 +496,88 @@ public Mono onClose() { Assertions.assertThat(rule.socket.isDisposed()).isTrue(); } + @Test + public void shouldResolveOnStartSource() { + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + Assertions.assertThat(rule.client.connect()).isTrue(); + rule.client.source().subscribe(assertSubscriber); + rule.delayer.run(); + assertSubscriber.assertTerminated().assertValueCount(1); + + rule.client.dispose(); + + Assertions.assertThat(rule.client.isDisposed()).isTrue(); + + AssertSubscriber assertSubscriber1 = AssertSubscriber.create(); + + rule.client.onClose().subscribe(assertSubscriber1); + + assertSubscriber1.assertTerminated().assertComplete(); + + Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + } + + @Test + public void shouldNotStartIfAlreadyDisposed() { + Assertions.assertThat(rule.client.connect()).isTrue(); + Assertions.assertThat(rule.client.connect()).isTrue(); + rule.delayer.run(); + + rule.client.dispose(); + + Assertions.assertThat(rule.client.connect()).isFalse(); + + Assertions.assertThat(rule.client.isDisposed()).isTrue(); + + AssertSubscriber assertSubscriber1 = AssertSubscriber.create(); + + rule.client.onClose().subscribe(assertSubscriber1); + + assertSubscriber1.assertTerminated().assertComplete(); + + Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + } + + @Test + public void shouldBeRestartedIfSourceWasClosed() { + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + AssertSubscriber terminateSubscriber = AssertSubscriber.create(); + + Assertions.assertThat(rule.client.connect()).isTrue(); + rule.client.source().subscribe(assertSubscriber); + rule.client.onClose().subscribe(terminateSubscriber); + + rule.delayer.run(); + + assertSubscriber.assertTerminated().assertValueCount(1); + + rule.socket.dispose(); + + terminateSubscriber.assertNotTerminated(); + Assertions.assertThat(rule.client.isDisposed()).isFalse(); + + rule.connection = new TestDuplexConnection(rule.allocator); + rule.socket = rule.newRSocket(); + rule.producer = Sinks.one(); + + AssertSubscriber assertSubscriber2 = AssertSubscriber.create(); + + Assertions.assertThat(rule.client.connect()).isTrue(); + rule.client.source().subscribe(assertSubscriber2); + + rule.delayer.run(); + + assertSubscriber2.assertTerminated().assertValueCount(1); + + rule.client.dispose(); + + terminateSubscriber.assertTerminated().assertComplete(); + + Assertions.assertThat(rule.client.connect()).isFalse(); + + Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + } + @Test public void shouldDisposeOriginalSourceIfRacing() { for (int i = 0; i < RaceTestConstants.REPEATS; i++) { @@ -523,6 +606,37 @@ public void shouldDisposeOriginalSourceIfRacing() { } } + @Test + public void shouldStartOriginalSourceOnceIfRacing() { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + ClientSocketRule rule = new ClientSocketRule(); + + rule.init(); + + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + + RaceTestUtils.race( + () -> rule.client.source().subscribe(assertSubscriber), () -> rule.client.connect()); + + Assertions.assertThat(rule.producer.currentSubscriberCount()).isOne(); + + rule.delayer.run(); + + assertSubscriber.assertTerminated(); + + rule.client.dispose(); + + Assertions.assertThat(rule.client.isDisposed()).isTrue(); + Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + AssertSubscriber assertSubscriber1 = AssertSubscriber.create(); + + rule.client.onClose().subscribe(assertSubscriber1); + + assertSubscriber1.assertTerminated().assertComplete(); + } + } + public static class ClientSocketRule extends AbstractSocketRule { protected RSocketClient client; From 980468854d1475bfab5da6ce7aa7d5e8b147f1af Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 14 Sep 2022 13:33:55 +0300 Subject: [PATCH 150/183] updates JMH lib version Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- benchmarks/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index 0b8bc601b..74e571d1f 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -14,8 +14,8 @@ dependencies { compileOnly "io.rsocket:rsocket-transport-local:${perfBaselineVersion}" compileOnly "io.rsocket:rsocket-transport-netty:${perfBaselineVersion}" - implementation "org.openjdk.jmh:jmh-core:1.21" - annotationProcessor "org.openjdk.jmh:jmh-generator-annprocess:1.21" + implementation "org.openjdk.jmh:jmh-core:1.35" + annotationProcessor "org.openjdk.jmh:jmh-generator-annprocess:1.35" current project(':rsocket-core') current project(':rsocket-transport-local') From 000f6da716fca29ac830d0b004082edd9d80a0b2 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 14 Sep 2022 13:34:28 +0300 Subject: [PATCH 151/183] improves `BaseDuplexConnection` and related subclasses Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- .../internal/BaseDuplexConnection.java | 2 +- .../transport/netty/TcpDuplexConnection.java | 20 ++++++++----------- .../netty/WebsocketDuplexConnection.java | 20 ++++++++----------- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java index fc679c259..09026356f 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java @@ -39,7 +39,7 @@ public void sendFrame(int streamId, ByteBuf frame) { protected abstract void doOnClose(); @Override - public final Mono onClose() { + public Mono onClose() { return onClose.asMono(); } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java index 85874f44d..901d1ba9a 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java @@ -26,6 +26,7 @@ import java.net.SocketAddress; import java.util.Objects; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.netty.Connection; /** An implementation of {@link DuplexConnection} that connects via TCP. */ @@ -67,24 +68,19 @@ protected void doOnClose() { connection.dispose(); } + @Override + public Mono onClose() { + return super.onClose().and(connection.onDispose()); + } + @Override public void sendErrorAndClose(RSocketErrorException e) { final ByteBuf errorFrame = ErrorFrameCodec.encode(alloc(), 0, e); connection .outbound() .sendObject(FrameLengthCodec.encode(alloc(), errorFrame.readableBytes(), errorFrame)) - .then() - .subscribe( - null, - t -> onClose.tryEmitError(t), - () -> { - final Throwable cause = e.getCause(); - if (cause == null) { - onClose.tryEmitEmpty(); - } else { - onClose.tryEmitError(cause); - } - }); + .subscribe(connection.disposeSubscriber()); + sender.onComplete(); } @Override diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java index 140cfc59f..542ff3599 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java @@ -25,6 +25,7 @@ import java.net.SocketAddress; import java.util.Objects; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.netty.Connection; /** @@ -72,6 +73,11 @@ protected void doOnClose() { connection.dispose(); } + @Override + public Mono onClose() { + return super.onClose().and(connection.onDispose()); + } + @Override public Flux receive() { return connection.inbound().receive(); @@ -83,17 +89,7 @@ public void sendErrorAndClose(RSocketErrorException e) { connection .outbound() .sendObject(new BinaryWebSocketFrame(errorFrame)) - .then() - .subscribe( - null, - t -> onClose.tryEmitError(t), - () -> { - final Throwable cause = e.getCause(); - if (cause == null) { - onClose.tryEmitEmpty(); - } else { - onClose.tryEmitError(cause); - } - }); + .subscribe(connection.disposeSubscriber()); + sender.onComplete(); } } From 32da1318870753f331cc0e1750381d1db4172986 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 25 Oct 2022 18:43:50 +0200 Subject: [PATCH 152/183] 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 153/183] 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 154/183] 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 155/183] 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 156/183] 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 157/183] 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 158/183] updates libs versions and test run config Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- build.gradle | 14 +- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 263 ++++++++++++++--------- gradlew.bat | 14 +- rsocket-transport-netty/build.gradle | 5 + 6 files changed, 179 insertions(+), 119 deletions(-) diff --git a/build.gradle b/build.gradle index cdfdbaa2b..079a0e1d9 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ plugins { id 'com.github.sherter.google-java-format' version '0.9' apply false id 'me.champeau.jmh' version '0.6.7' apply false - id 'io.spring.dependency-management' version '1.0.13.RELEASE' apply false + id 'io.spring.dependency-management' version '1.0.15.RELEASE' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false id 'io.github.reyerizo.gradle.jcstress' version '0.8.13' apply false id 'com.github.vlsi.gradle-extensions' version '1.76' apply false @@ -33,17 +33,17 @@ subprojects { apply plugin: 'com.github.sherter.google-java-format' apply plugin: 'com.github.vlsi.gradle-extensions' - ext['reactor-bom.version'] = '2020.0.23' + ext['reactor-bom.version'] = '2020.0.31-SNAPSHOT' ext['logback.version'] = '1.2.10' - ext['netty-bom.version'] = '4.1.81.Final' - ext['netty-boringssl.version'] = '2.0.54.Final' + ext['netty-bom.version'] = '4.1.90.Final' + ext['netty-boringssl.version'] = '2.0.59.Final' ext['hdrhistogram.version'] = '2.1.12' ext['mockito.version'] = '4.4.0' ext['slf4j.version'] = '1.7.36' ext['jmh.version'] = '1.35' ext['junit.version'] = '5.8.1' - ext['micrometer.version'] = '1.10.0-RC1' - ext['micrometer-tracing.version'] = '1.0.0-RC1' + ext['micrometer.version'] = '1.10.0' + ext['micrometer-tracing.version'] = '1.0.0' ext['assertj.version'] = '3.22.0' ext['netflix.limits.version'] = '0.3.6' ext['bouncycastle-bcpkix.version'] = '1.70' @@ -174,8 +174,6 @@ subprojects { } } - forkEvery = 1 - if (isCiServer) { def stdout = new LinkedList() beforeTest { TestDescriptor td -> diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 21931 zcmaI6V~n8R6E)b==Cp0wc2C>3ZQD=Vwry+L)3)8ywrx-EZ{KV-`6rwGaFa@IRdPR6 z)u~hG4$gort%Eht{y(MI%kt z0Y0nYm>z`rdM7Lh=##-Ps^6h>FU7m~cgyxqs;Nqi&~ytk^e7KkJL>mWt4%qL*DKv= zcgsip(fRo@w)aGHJ&cRiJs;2cc4v+b>Y#M1j_&4}9i`o^*Uzg;mkN44%!|HxGTNmY za%+!%)BkmU@yFRSA8-3+6za3Rpa>0d>aP z|6x$gEo6tjC%O4IHwK@zhTuzcDM38z%iFcrUhI%h?s07}F{H1l!3u%>r`EgBk|m$r z87XPla{FK=fulv&qhyZ!oAD=l1}cy0X;ZOYTNqV6ux_FyBqy_7sRMe%ATeaSNf3#n zOHbG+%dn12N=ywJWtQcx6Vgpi+L_Aqs+4YL0kAFnwH`6{_7&pk8r>@_Sny}{j|w^r zLwLjOoTacOZKW)xkrBEW;+RmJLgpQK^{Q}vgg3n+^)Vw+pd)tvl37o*JRsA1Kbtr& zZNxVRV*JxYrwfU#Eet%gT$cq^7wurj4!-w)gR+f|=z6GTNnLF}F% zyYZeGV{!;%ZnkOP%w9!_VmGqu&WcTF*+vHiL}YHYZUe^Y0{djWLG^Go2y*z_pek+h zHj7WjmG0S6)jN(4zViLQbm-Ap2>C=?GRqH?R0!u95VvshKy^ew)53}k#lg#Y2yl7= z9Z^hYIZKXs3L3Yx2)!c? z;Kx4g%hVUnY!fQi3^`@vHe?08(_)T6K)gL-8ySjtjFyR1&(8SX3+N<&Mq8sLxve~z zzAV>jq2O*jsJ1)7Jh{io`FJPg@INV_KcD>*0$9G~#NO;Zs0ssiX)cDYrr>NMg|ueU zfPDk!onCalx;;Tp;eLRfhYXEb1XXOHJi=Hm#W4zEmHU^dH4Ei4`GGr`xhV#r~yJKHLGIJQyU&h%j=sVb-S?Wx&QV9@(T$Y)QhJt|4A~U}c zcsipTok4DLxZY?S?pG@X8?#Ckt%hhQ1&vrL320UYq)O%UJCrVJv!fbvGdr`yl$m&x zS5(FPkgt?3(L*qab)6Sg=}c%%Y%)(%!F*F-G6WkAyTZ$e!jKnM7X{96lH!+Zr%Gfd zM(2EUxW0s_M%j|w@E{uY3MxRqqR3)CbX6%kIhGph!o-r&l93|=XRTYv+VqLZTkF-i z?fE=YV<+!qSV+KfdFjsVP^5?Eu0prF$I^oyAKFP<9;h#ke&W<_dyrcR8uFiq!x zuhJ99bAm~;x|HpTHl66_p*LNw9Qi3V$0SxTI3TJAeP#c{s6Nb{Mm=_45nKr550Q#fz5ZEAv3 z&}MY$SXbrSQo^%cWPCD?rZ{p@@<*u|3m=;L&#_yl7Vk063P=Z6w*+mu+Pn@-mE%zg z*494lJ#6X(B_T0_GG_X=_5=SB$MfqaW?waGXzxGQbFnJ4S^*~w^C?BdgJ+-}404_s z)3Wn{!Zfk1(~redky}&R+amHQ1;KF3%5HVz9e(^EOE=b`}a?DLEs3Sax>ZOkn5mBnnu@!WcUnC|gK1(OfE7 zsX#cWxT>bc58uUVCq}{>jyg5GLQ7Nd?m_(#Hwoh!(X&#FN6Ums z+X!9VKu|p&$PWHUVcZyZlZ(LQ$U0+)dM%22Jz$<=k}+dKOCVkyyd4pZ^mEUh(l`B0 zpGQ_y25>@_cx4a9At)&sq$s8015AA~>R zUU$W#q`Km>izXR~7{ccVrRaUbl7iw9))M>FlT{V=qXl~^w!|8Q4LU_qH$|rCr}AjM z6hhys6DdDXoI^jz06n4I=OXKkt(ls9_d&!CJ9)bUGiD6Ow3^nurrxGSLzsX8KQh0%pBpSH#o z13n-moFP;!N$rQ-Nmiv>O6(@FNamVg3GzYWmDy1(i4m0}BAsaMHv3IaiR>4iA;ao} zK9abGwb(uK%%foHY(9A=>qBL^Jf12)tAiZ!gJR>0Rr~S#_-Z12NH&0B#6gQBl zWQ;zxGLAIqD0!7n6U^faRR%Ou&|QPA<)E1Jf8~WVuZ)XoSRudGC>@D#)|#tm%e`^A zD|^v{R?0es6ZS$t+@F|HQHP#ygZW;&fj(N?02&8@Ad5sH-I%`x&V0)`?5dc z$Lf$17$pl=q%9=1=ezsFkQM!G2A9o#PEQ^ubCt-5tnSz@2?M(c9_qUD+7LRJ26h&O zDbX@|*wXEoN!X)mI~9Pn?!tn^nz|4aL2wU|&*siR=lIPWU*fNkYW17WB#g9!iNn zYOH@~;oBN9K5KCW6{|kjxAOKdMs4i?Wpm&uT zUeI-Jk&(sHChg*t(I|;1$f7jtDPb%s1~8H>9bE3;Q^nn$O31%{k&)IMbz#sd8Cz1r zJ`urAk}O!Y;U`%q)0cH{@J-xYs>B9rwpK7<)& zA>_DT9h=CRaxm?#(~p;~{;rj4vF~%g;^?d?c7waRU|MiUl>f8QFDT^pV>GcJ#&tel zmau7PXprj6y(4DX(MtH-)jA2XzO7x_BINY6e)0OR@QK9V?9-+$7J2`dZ1yFyH?17QneiwTs5?R_8i%vW~j=NRA|~l z8#tikYP7IcHabK&IMU>3qSZ6x9S9o?UF~Z^-(do;OX)qQ$%~iBq^AMNXyD5wKl5&GaljASzVc#d5k zH|hy+XO5cGPNcz*)gCfW5o5F|G}EU;QRK<%Y(#KwLJ|*S#ekc^<~ZDkCNgwKgTBY= ziow^LRQcL{88KBgo1Pw;PfcZ!R#-@fr?eMn$n|@5gxO))jZeSl+y~u2wHl%e2U;VP zK>v9->T0=a!zaW5#lElaJ_J~CzuM&+JX!*Nfak$AIiwNuou@|Hxb(XZr>-vq-CDc` ziO|wR)DPuqU2oh2e$04u>uO=w%ud0pIflJc@ao&8PD^{sRRsYqP3-Ux(<3gJC6#PVyV9(iQ_TQ!$e{hBmZO2(UQ!NxhwND4s;Ow|; z3-R$W;tCcAsNqqne}Ua-W{A%Zz~lferyX9)eKDan8SG4y{5K1Y*T1s&BDCF3Pgxh) zIUCZ4T2)A9a6M-SKHBZ~z;ropiAA0P)m+h=T{-$qG;*HYeko4rVON}>+!idY} zZrJjxxKf2mK5t@oPIB$!iB}s(?G^5mBVz($^;oa1I)x)Td-8I!TLly4_gw%OC#RyK zalPpfGkYha{D-|YYjjUr6`r!T?I`oOnTn;%XX|C5ul{pFtEtKw4KHM4GPTyztB?6*e#|DZjfe=Sum9vhKmO z$Zxmjc4~UFEs}yELZ4V~I3@Mc7BN|vpMyA$6lhvXtv+g)@DX}9nZc&|0mg@MaXm`!i_F2yX`JC@XG6LSZ&?M$YY5bV&)MojT z#knO+ciCJ-N0cu*shmA0+mLjnW+e*qfBakQvp}q%q`>gqsJEa6bR#?WasO%C)5YXW@Q{@!t7wW# z;0zvdiYtIe;8o*w7jSX;5r-U1f*GfDuO(2R zyLyRLsXP27^)WCI(P^a*3m9?BVMS64pc07M?apF!Js_cQ)r~4Z>Mx0#g!FbC76K)t zb;v($uR6dHN$<5+OZEy2EV@W_F;hsf&D^*ZEhYK0S<}qR4Tg|fTi7?6?S7;z57DqjGnsM|B?}GQBIoCMW z7;?d5??`t*A!6WjoNk?_mqaiMtA5sSX@8EFPdliC*X9&Xylp?`$h9#-OO+2+)lb|| zR>aONPcokH1$^~6y1s<8#sq!O=6qIBRGYRm09r~Vt!I_TW!BteYe6OZ zWCoC38)tV!!WkK2|wwdL1&H`i=xHN(_uu}LKRS@<(G zTd8F``wfkv0N$&;k)9`N9wo<_k#wmB?9$^$NVBpeqfx^4o`83?7GIq`vJ|o9xv~;v zulzdp0$Wz>)Ewd*iw?A(Ojg(roGxfEz7brudm#=-P=|Ru_1vx7TShCRESpT8ft|fM z&IZZzDiKEWp73Xo#PA3PhkmT8V%~nM3esoNpEj=$0Kdv$udywmW;Z$q|2=LeibNS9 zNh2Sh@+hs&=^usu9&bTONeG{)9;&_@w0+d~0KQU(Io6zELe1g)_TXN_eFxQBg#_6! zP<=7RZHj87LWe#4B&@Xbz6%@$@$dtga7L2FPa;m_n_IC3l-iGwPs1!746PLaeG|XSa2z)5oyChBbAXH(` z#ymUnCbE)px)k!1G9OLY7P?Z`!jRIrITY@Gp#pjspEFz6=d+evYSyV9cgu@^FFll6 zO`%dJ**Dp~cYZH8kwsndIEy1!iS-GT{QV3?HAb5gntpJ{{0V~#%01OxmT*qCvfCE9!iY`VAQPoJSa zxc-_-U5a*#O5Hlg&~Oar(r`b%4Uzggy!k0~TeYIhlfs{Q^$iAl5Cqx-aQv=681LtF zeB(0o>9PP9wV$4+2m%Uw55q5@^K{75%JXy&bJ^XSgUj8*Z0xYBRk|mI%eprtclAL9 z|G}E~saucYQ7VD{FlMA!HH6vk0ZiKN5fP0AD4P1=bVlUqQX0<4dJ#!$^;ed{v!fy_ z_FQKC=;gO%A^-7-Q6RTC-GDjDxD{9;Hu6Sr& z;c6VJ1j=5TN64w9G&f3K^_o~}o~nCT$rv%iF{V1I3Z*e+Wu63%Bvm)L4Q2$S=B^o9(5o=31ZCmFI26hH_lnT%Sij zZxhvc1kSK2Q!_)=MZbNl6DD@zQE`_^ZNzjNDNv}l{#Gef_il-QZ4*Ecs@ z)Es=MTB>Won(zlq=IUz8ySo0=BJy6I!?^>$Umjns&SBl%Aw{k-vC*`m@=jwjLvj+w};ZAuW=)mtkL)thl>Bur^tS>&^p| zLa=P6iy0#~hgSaf4lB-!Z9&(`%(1&`AXbeXin)F~wI^LGzlp;cn7{kQ->Ie`KJ=G@ zXF3u3r~8a-Yhcs^#50ezgowq#0jDviI|k)CMX-*8ScLW&Nk8@tAi z$rNWPlV~K$Wl6dSL*NBKYr7UjL`Yy#FD-{h8Xqm|iBlf4oK)i7aT<+W$P|*0XOcWg zg}JjQ*Y~X&A&M|s1N0vrmaj!8;(q*5gvDXu;CFE5K_lF>$?!{5BF*D)nFyW@bYhrr z?8|G(l+0%8E{r$sBtw~mpfLx68$YGUOA)cZ#!t~c+=_O~&^XZLX}cBnzF-N*m?bhW z6r84_Dn|s%1CV&ISf9Wkc*;XFXgurH6vQCQNsPplMin@d0s<_UI3YblR)ZRe(Rl6J z@>o`C?Bfw8Ogn2jCF|(bIcdWX7PV6@S*8-Xbi0Y-8Li;O8g+`ZaUOL-SuwMRX=%~pG&K}Nt^i-;;w$XXxT9f~ik@na#9S**V?%q1XKkR~1TAH`Gn)sW z8T!|PCry4k12-3mJtzO6;Z7pI+YWRKL1 zvn6Jr_zD>-IKpZDXyz?h>~kiiqa>poo`)02#(dW@!g)6hyHj*W+@p37|6qp$1R?%M z+m-X#{*e)`ysA9rjpSqenZ31Of5-FFFD7-BEZ#UnqS=6l(gyC4UxX`$@)u8kcB&MY zpIRB34Y8pjz$E_1bJ+gz5&oJ%URolAX?PBkNk|>AA zUpx(ej2n5m$4p#l?kH6=mn6-}4@}s9Zo>};duh{;=2RG0g`5(wIICnhk z>e`Em6)}esmor3=VM%xM0V6v{7Gf@VkyK12gT{Mh0f5yw+PP_h<9)E!0drt8Y7sZJ z{8!FtZ1k}go8}#;EvE>JxO?_eJ?1cs&yn2BHjx{2#+{I`LRn0}-(-Jr!BKL>eVGHy zH?+k)y9@8G;4KY^ca?o6d_TWzFqYp?ur5ACalDp7@%=N@CPAy`l%4uhXDCmkVoRuwW`eiU1-T9#$;JW!%sJ!iAd(r;~|&v;7N- zIt(-u{j#%&g6AwRP<&LR)ppGcu;$w7r6rE ze{o51d)#@ZoaH)N`(1|}_};kb(nj<0QF-7B7CDn*Zrb!!T%xyeVH+t4!?}nChz!o& zmfyr$chSoyIE}{oh6|bk;7X1`Rip^mfh1N%wI4n!j{E97Mdh8bU}e52wxfF76i}fr zahs_V2zs2@eeKrA1M(2lJ#D-w``*4%PmiUG)M7^t?}9$Mkr!1anwmyh$Zk>g{=-um z`I!{yH7U6ABvunQiG0+9Ee<#l+1Jey@pX!K`%*&Cui(+3I}TzV2`_pHyi@*=?tlw z_LI#vTmc&RDc+Lf-dqy-5I$%_JKcQ2Xgv)>E}+IgKv+MBz$=0ia#Lm{G@jzrnQN$^ zwYb&7-l=T!@GEKtq=Tdsd=-h?xCJV%t z?O6BZ3ykmCuL+_kCEQ%10ClmS--QwOWe*i`@W!2ie23*ar%3N@C`vGXIT&+xkCB_N zOe6VIxB%>d!bz-P@SO$Rh`^ny*bb$B^}SEm*Kn|k|D8MJ_g2z3!NOc`dQZf&Ou;1) zC-)tFedST-JF2R45T41QuQz(+!!@>h2UJe}PG@t9y(7nd8569|o?dHf4rOH?i#uR|Kz ztxD3B2t!Acp?rVky9Ez-ObfEF%3L z6q0(u>#9?VA)H;aCPuCHgb?!jqvhwglc6%nIj;-ES`w=&RcP$&+6UC%mCnwR#Pk(= z~5t&g-t+t)q!vByWOS{)4rfRPN zT`p<|CY=TwwAR^6EbRQsA$TXVaD+m`jGe!pqtX~~-NR8h({?ypXX%}+H@7_M%UVbZ zw>p8*PA!bSE^l(u=HKn|j9JO4x}Txvuc?1hPaSvAQd`5*=GFF|6)7>AaCyyxvJ5Q2 zwwc@wsnVXS>ZUwX=6u$`cadRTa&_JRC9C$H#p;^5$^d zmP;PcAhBJ2(`H?wm}%Qyjnfa~cui&QJXclmaw3jG+GiAef~OOR-Y$CyRPpUVdG^b< zn5>5gfI{*d$R%$$6=sT&>(7@DA?tYfWE|K*mWgnonT(v_JFEJw>4vc%&@d|Z>6 zU4)DHCboPb@iyZc#pVe;JRY*goSU4JK$e^^M&ic}KGnja+k-p%cm#76f@)puY|jJq zf1!0GK&K1sR|}Ou$9RnK22M|)RjE{n6t1Fq>lJdMd8-t*nS#Qi@*>Zpf_%&B(seLY zWCh$yD+#2ez~nRp8&G(`dcp%P@XG1IdbZb@VRMrT@rAIrbrbCDp^ko*%<+~6 zi#-bxmiuS=lf7M>z3d{<-n2)$K~&2O-SAyaJ)q?fDVxe5JfB|F2?jTvUDSulW3Ru1 zSg{bb5)+;KYFHFofH1452#Rmi{{6+1F%=LEL8OSaNi{=oxf`nf01^)(F!$>3W5tsH@u^~lV*;DZZoaakHO8(jIX({XKa@e>O3D(aiFK}K~J@kimbXW zzqy;AYVRH3%Ngi4w8DP7>s%t2#? z=@*SL*KE?Ni^FNW=jz6EjZ*_#>@+MCpK2tFPZ(Uin1$YOJ!!GlhS7Zx`-(x=KA`hZ2JoaSfvBcq7e&*PV;54ELwPxW6i#?a<}0rI&P|c_6#> z0J?DEi=U5t%FA$li_wym(CRhzkJ58P$XAm+Ji%Y z{siP1^4i9G@?Z_CuZRPtann_&5CL9Zk_G`?)Z~ zx|D-tw5#T51HE$G(wE1=V07Z0r!)iRpO)-?N@L&nw#7_WXY`v(}29T$ahFy zmvAXi1~lStMASz}dKF29ZYH&}-54Jf0jap@bG_rwJ4(4ju`^PHsQ&`zxGQ`)f84{} z{lUi5=aEM2k^$hFtBA*8lhM|Yl1ofblS4U1!N19YresAM7fl2IyXVr}B2$(K z0isiAW63z%2ZlZ+WH2nmm<@*Qhp@0r=H<_9DRYaJH7(Gmf_3e9?^W6-fyOB5#oHu`VzCQl>-(0PI^F1;JxV?^&v<&PM z_YcB(Hh4+i?*e0%BGLm!*uP?AxJX3AqcA1jE}n_>#~z|RPloxrL&8n?Hi=2&(kD&_ zCq3I;kgo?O-!AO2>-%Uk57k)oV|{`=5t4g2B32t~Rwq5dw#RrKs`~zTvZD5craROM zfjd+Sp*frwkwkoWe&DlgM|zB7nbYojaw6U&-fk0ZV**1T!LLF{gemheh_ff&NEHNG6?re5 zE!hQ@uBFx@mg8-)y!;i98(+$~&Ff(>?hF|xN%NA7FSSiQ13&Mi=f#LyvtF7TIB3n1 zv~>6E%LaJwr6L7YYsvsoy*7UlfQCWwikelG72}!`lvN!5hlQv@ofd6FXG$6a>sduu zziYOeibpH#rW!Aykr9$~ZBvhai;7Ea-3IOMO+yjd6ZuwWktowc>UYxH|Sadmx51HAxeMv0Tnm|}m(gh)Mbln2b{zSkuAS`w0sLO^8WQbtLhgVN51E6Z8T8!qu1`a*xHepGf@YOSQskKtF|4^{lc z6g!(T)awGGrcRXSu`l(BI7|J6rVcA}7SL&TR%1=6Am#Yu7)>RZeC1mr3Uu31Iam&p z=%89YJ}6Ea%TW#p{8QBiFsr20dg*>NcL1h_D(tj1#a@(Mr=Lxp))U%-s(yMUBS_)F z8%m&f*Jz6Bl@2lg;Lq#<9BfYnBlRmw56NCNY)@D-Y)_nnq^D><=UqjRgOT_^8@eyl z4my>Cd_`;VuFtCgb}9tOS)Ea+V8X2kgrIR9;Q=Lzf7PzVYe$gFYiN+cJ~Kr80iXfv zKm85_V;PmHu(E}(!2oDZlLFE~d3_G#pYr`TcTf<(P(IoxHbA_O(nluhrb$hzZK5qN zH_@%9u%!57nF~X%NQ>xid8O2(EomiS7XBU9OY4ck3QB8HyqeB}&tG>G;#@Npnu~Y1 z{D4kt#W)hvck~b>zPlbxH*dQDu+~PeyBl#UE@p0>IoiHsoGJ8Z+b5+mPp_3Wt?J`Twp^J(kgtWEUg zU@Q=~P`|zTCj_uVq4H*)TlS2IM_n>I%EJB2vTfzy;hkW&UDj`>1WIcnm*zw@MG(o^UXKjFoziK zr-;AV*z+u&+-kfggV-^JjjdqtLrTEw;Rha?lqCznp^7#YsWPjjEAs!;ll4?T|K-^l z`5lTF(z)NJv{HMK&sJbtA=zsgJt`q-8S>r1=gR$WBhu zSeij(|GTkhp^d^jC)To#fvquam7-^h@|Ez^kgw;M(Wxjj;ISk6K&q(PFGmu3SecSV zB=IQYOypf`9?L!3675Cyd+Y_9CBJ5P#}URR%c`$*$Ox%$$Pv}@TO{*+BK{`(d@8(` zN?8pDO@>}l_~jh{P)*+Q;eZVxEcZ7a-qN*T96 z!m9z0%&h2mz`Pt`(YK|gikrNd5v=Gki#!;w-WS?UL2+>xD)NNj?4Y+Ff2PSFEaZ(? z(PaxIlpRqj7(q;@mm-tAr-J3poqrI$#Sh+W2-|$KsXx^Ld3}GoDQadi=Z+EISc`_( z7-kdH4Ss>kS?NlNS}l2oYc#$le6!^piHzMyb$Jun+_9~+bd=9xSkfY<=8v$0qW&E) zScUc0hui=aleU(Uq7t(AUNJ=r1zoJb<@&di8FP5gcD`t7==Z1)9-A4=qF}e^gl9|3 zh=Na%m={V1bbHsfJ({J_L%=@#U1=06x$VUJRl#Qy9=|^?1Y>lvnNs6gTY7V}*q(Ra z=-;Q8!xxMY{OPuaW}gv=1SP6Vb~@U5Q z;IyB4U^zFW2DKf#d@fuf@|^jyto#NfZ$O|CG}xFF2pi(K#N2SI<_ZWV`5DVrIWW@D zPKfM;p>zk$UpZ?e%J)N$FH-4FAtv&HNl%tANOoO4F>EVj7d7OOUscaPX)EB*BRY}P zht!V10DB=%@R#--7v!0$q3)*4@89{J@sU1`aB3xp3X#^EQD9_8csP3FNA0ogXhknG zKb6T;fpx1uUxE)ZaPmj~*x^*pB)zxPk^ zTgB5@e~tJujkqFg@vmo-=@SjXuaV#!EF1}4Z~WqGKFC4-Xq6D})9S2e?Y*z3d3(Xb z?;roGbj=?ro%{UvvkV&&1mp+(|B#W_a26!_gt8x44f20oWbD~A_Lo$q?N+Ybz8w%Ezi;q(|TVDAOdAVy}4=@(+56vo@-nf@RhY9|MGO?2iIM7??M+w$9rO5?RMHP zI<0argWj#ZgHBbW`8;mrZ!t#xAS@-7G?{=M0hg*%4s6(gM(D{UuW>wamY(luLJeDo zU(2+@KpA`;&v*@5H2mA;nB#bqFP-}jau`iQFeXp7^#V1Rbu8Q^Ac$hauCj+Glf_CE zhkh2L$@V1xoor}`U6(U)fE<+~i2`0Wt38&NX9WvG+|_g+(thGL!l?7&YWljcAsc{a z{T{85t1^z;t^ohziCj_&mQ`AckAs?$%sU5DYKwp~4RS!a&rA1$@NQAsDa*`o(*R#L zw+XMlS7nUVG;S1vhbuhxj10<&yk;o*w~$Wy04+6tj6V0*TQreK|bEGaG()-~G@|3~0(kcH7Qcnm#8(deZ%}_8U|zSD*?YIXkpwZ#Q#anpCFxE0cP=v%qp`Uf~n73$FQKgwQa*=GnBJ3P^3Aem{`Gx8XD_h8>w@4P^*&|AR<((EzK7IVjsVh%5PwfFm?1tMq(bB3@cs{+? zSqepoQ@XrPyUszw%*nmif~e~{1*sB{>wXI_I9d`fgSyZWE_itoG9%Rr$2H6=R-)&B zo!X;-#ba;)=X#D&>;51CduH&dWF=5`7s?~{Mv}{TymjvyR`aiYB;E28CO4^xxIcZO zZc=oA(#>1KcAkEk*Ee)T!`c@;c^*q<-JDE$$L@E2xxR^dk$^EpvU(DAL#GiSx0O~l zZBcJ;yVhOlw462_i<>pOt=Z;9ucEZraV+0VVLZ}lt$iuVwW2o3RwuylX#A|sn$+~^ z%bv`La&z8}&_q>uNU zjYqgq2X-ALZ!6@Bx6c5JosAop6)U|*s$urxU_tI)o$5f#;GL#jhsh)0*e&i#Qf4`u zYe6Gut(;H+HcB~QNA0zf6u}hhF*ZuqWjeNe1GmpGP%?+6~^Q1(=M@HD6>0so@gf(D+tkS*u;+>=dbj#)G!&_r>+B3HLDnAwwa(Omv5X8dt$X-sX`r#?UAwKgGyENr$TS|v0ntr z3%R(pPfD*B9G#(Ip;;){#XObR=9p$G-LTP$H!p3 z>WQi?!Saw=)9jC<;^NbZ<`EA@o^t0k6Gu~pO}bMp+EV_hWcl>0dDXgHX#R-|m5juG z8dt76{{TU+$zueJj+Nr@$BJkV=rJ$KlRLhClFs22BY==wts(sB%cF|VrZ172!qTl)*mCILG&5ScqC!MqP^ z&5_tYxv&i&w$KeKQAB_nlXgDk7*bZE#XaJW5~)WT4SQf!YBr@KJ-h9k%Uz0^7;I#? ze&dd<2V2@W;P>NBjI-KqFn1;`=o0#=TIe1RT1-hoF(+r1wf1l_YjN%@mzvrA{*8Tk z?|mZEER+>Gs7r^zu!-s(2Dhrx0S~0@bmWh|Wh?u7lvB{jP62Q>j z2ujDr;JM1(u%32C~8>M4#zo4fPDe+_fW1mOIddls+ zT_8Ab1IaHPkNs%#9{WpSE*a7*^`{ISA^pn_;ak%(l1w0*1H2e3rK&zJ!aJ;btU$?kEyN z!Smm-V8MjP5^MsNx5b}Wi=Cu0|EVRY!fdoYb7q)SKr|uesr-9U|IYCZ)+e~?#Fh?F zY75&|O+^sn(P@X^p2cK83RBv+ph??z)k$If4AC{6tKKl(Wc+I*=2;RMc@w?0>m+q# zX;q8_r=?2{Hx@nT;{A$=^KWv5N%0nD2%evF8rb|fo9IdDN#R_9s$#z*iZP|A50h88 zEld_nLt0$0P&yC8AO63Y5fX)jyou646nT!ZeI7K%6n#Q)#f@>>HygWdGu%#3V8n{2XQ~Pn0mR&4E#9lH6HPtYid>W!TC0ZM5g~HZQM_` zKgPGLpEdnEspThzLf%@w!VYkw3;uNAc@pSO?Xb4sova4vZ&zFMT+$S?P2@5F{67L; z1l1mgTg2CJ$Ztsy!R0?Jrm*c~4i+=JgbxZKa|)$zYtV&Fsm1+_5#rrN3U^KU7HP3H zAPD|SY17`{B#H+HSf4WfQX`R9z-fC z?$6TVd{68eL>skuI_txu7mxG;%&%>qRU^HuuP>ia!QW%Rz#~I>58EsIzvlk>2V5#E zy-BLz?R`#!f6-X?^#5$c2TiOggTE;nKW-qugALeU^Np{ui(?y&M47qhIq^>9Bk{K} znm;ECJkE9?dj~zhV#5_cWnJQ%2!!8qGcn4=|8(5`6+CBc)u5QvPPKXmK71ul_Q~67 zrV3j+c#(IGe8uAkwU!H3{#&V#!l$WhvB){mN-d{{q}-Ur#x8VGPhGh~lscxH#i#fi zYrUObq*@hY9O-JxQoH;qf+mo!U@^yms{_w8D37Mys4VyriF8Gg_Anu^frgcL*XWnfEUweL~u5bn8kfVq?*vLX#2Z32+U1d*oBG zmoS&GjCn__|E7M~UiAoBzo_W0@x0&gIk5N&v|I#|k6O~%Sa7x(pvP}G{%E$~hi$^x zko{{M2&4SWo>_?%6+Fa)O4_{X;1TwRjCOY5vF)hhNP;nX_MPb*CDO@^p)&;z`GjC7 zQr^vO+>^{qXaA-cXgz$7UcM0h1nv0LKG3z20|g(AK$fqYS!$mvU;81N>w5ShfyazI zwUpiH3D0glv}bOeIXN7CA686SjZbrOD~LNEb7#o<`=J}C!;{hq@;>w`Qr zhttcG#!rW$*#B;w!i~IjKO=$f*jP^Z@O_HjRQ@WgrVMltXm=%==#H0#o3X6j^fQB{ zcw+Zajmzq2g2Q|66s|Me*CVx=5KvuKqP(}0#k`kwi8~)#hbN~Lkugr>9j2#WGcZ4r zU8=HNLE8y!u$TUv%t`DTd4dOyN##hiXM*1CS?56!8KIXy|MB&lYCyvTBewCd)? z^{eDgIg&4c1DI-JV=*IqJT20I5N9JuE5aY*I zSL94+g|7B7rlIsFe&j}t(iis1=}@E#gxAIrmL422+7g4lEZGu9FE|r6oWd_lK%^uu zgi>8$KrPQ3rH2pei%u^ZdCy$%tfbIDYfS-lr8x7i?j1<%=wL|#=k8T`kz$W)vWP%T zJlrb)X(eqV)`vM(UsXd;uy~>STKi}8y&fJ|pvevVo>^<4ZOmfbw^%GuJM-JYJXDn(akqmnI zI?^-evB0ojdl21Nyt(Kn`g#VWT0Kug(@+rA_T=O7l$g~I5BX#V*$hXK5nFfsY(3^k z4867`wv00Lz?ePly|(ejQ8!(Geq6Sz#1?yq7f)~rJb6_-I$3syzR6|`S+$+awBDiJ z68N$>I|&>_B|e1E5XG*m8r*Jc4&O;pM*kFs(%!mz`>t9|wh=v=v@F|becUK2;xKOG z0KHwK(M+H2+3@Z|NOabNnfWhtBBC1S3L{%hi;T8-pJHmt%n}qRDgvBzq;vYHShA5H z$oza4h0JvL+M4g?c8v}B%gE>0(~&8@tsQk3S9n-M_5M5AtQ6%?dCQP|js$!ad-1Lt zBe7U4(g&2Y=Q2WOzCF2j`f|1IC~4jcCyPsgr~$b_!q{cGFSKUOx;tXy(obLXDYp2^ zKGB^ZXOqSl+3W9ovcorkL)ZZY@=CQtDg5*E`3LgB;qoIFcIPu*>E?$WW6;dw^0ey$ z8TrOX4RW4XaVir15600Nvi7?#!YMW$mw@rMl)TSa(}Vb)ty>_?KZX?#A=7u9p3(kT zQn+M(FKZo^6BI-qOPjG{+6`FAVfqChS$V&h*1V39iJ`@ZvHRH5#i?J=B&h=7OP_KJ zY#COQCaX|o)aiDVD0MBBnzp8W?jv1L0GsvXQlTLTWrhq4b(LK(=SK9W(k|7=bA&PS zby!}GF4aBd^HsU3%*}iT2aB>wdR?aWN93Q)%^2`+l3&}vdGD%*u~xA7~wf zbFM2|GE2Rp;;ENeT75jfD^G&FXDG zA80I6%noM1g-b__+@-d{g@^oPt_|jL2&1@s^s3SgJXxPOAMt4?{bPFM00w;1MzNc` z?XLH{;0SrWI;oA8AL98jeN7#+hdqhE^aAu&uyMrE4+%WqlS+PsvYbv1=SO(I?7w~vsvxY)uO!%%mh zl)CJZmHh3nCAVUU%Kf5^EX;MvfzPRytsARpo2E}B(aj9)N2lisg4RbL$W2`kC?T&L zxj3g!NANFu5ggg6dit7$z~Gb1x!#uYz5?Nj8g?Cz6-}DIW*MlgRa?Fc{h^wl*O~2& z_fT5tl}oXUscMRtUNq#Kl9uCJHCX&!T9xiR=hga>!`i*3Q=9i&3CRHlXIVPvYHnwM zxeVvnN)qY~F)JYO1juO=3?WnWBe` zX%bGAnNPD;9I?jS63A}cMaSxq`}s+rIb z^FHcuNqkbh_S!;l=8!1H<$Qz+IW5GmsU=9Xt;Lc)Hm#YYC(?qG+aC<^tJF1~glZ*tQ#|r>l?#sHM;S!tGTyMR(s>Nj#UrXT(>ieW`m*&%*j{lx8@O1pKTd4Uf_+-z7>?UQ45?w@Lpi*~H zMp|L=Jp(spWIh*01@&_TaNX$26t>GquTA?&hIHNXqs~YXUST!lh2CpC%z_uJm{xw} zNp-q5ApoCud?vc@_&T@ROWxtZRd87zGkR3r7Jksr#1HokCa zvnr+uohrZJB4rOMSJr%MJqdX?AG%$4eVb>@HSuV%JJ%0GzFw5kb^A4occ`^>Xp%&Z zX-hpf&#LMNy+X+4Ku84>El2Hsu5D=2t?hafAGM@I>x*HReU@0&tC#!5!)lYTD+x@i zE$3#Yw1T8`yL{E>j}pZ`%<^MJ-$WTA_&o}g4%1$s zP_M3q#IAZ(sAWBfVv*V5VG^KhgTxR9ZsAVvUM}H;Ot5OQYKpGEcso(EqOdOu_R8q=02=iqdua^e7BsZZ7$3 zn0WcxTEgj~n~ICV{=-h3XJ4t>6ke@+(yrvR#+aw~e2n;85^1Awv7(qlzO5ve+LO&o zA&NFB$YdgSpN+xPKs_4I5N0TBZ#thD>bQ4MX1$beiH~j3|6;*LAPwtHmG9n2ZY_;b ztL6RVjp~vqJ+|kik%4RRevZWC`)HdnQ#Rz--<*1ZwkkkgBJ|pc&7n%}kbNy@00%lZt$rc@(#)b=QgS z?U+1i5Xyj!&v6XHh$vWMCw`fG;K__`&a&hPW%Cl1@@pPf96a7TYlB{!wf=`6B*Gh=*gzDFFtfdtf4WLP>oJX5p_eQf*?0(n;>0rKRn#jA1NPT5> zw(Ch}efF_Vsx!LN*Nfm)R-885lIF>3Dpl1luU&4&D&%ihW6_&T?XoTxYkAGMqQ>AA z%%f)2aYtXP^tsMx@^GNvf-h52=)8ng&*6X=C?gt8E5@G&7`oWFCTdgAE@#|`F0ZOy zxirfmw^2&xuG4bqTNQ@t;MDqc6V|*NX(BGZ=@OstuaXor(e-srr4OIkn!W#==VSxg zM~Rwtj7GbokS?#AMUFLA>f545ePkpxa$q~2afpihmhdb!&AteJ>^XxT`;Jbw7h(v8 zr=|AhA_EdSvOL2_8Tt1$mZ-Tt;Y-LZ-;zqR7*?3jRXsupI|N}k5=@%L6`RCwMRjVt zy8?Q{s%RK94{L8(0m(PBsb|aF$n3iz^kPkrZNH`xI>p1R`=bW?Vp@$x^k#N!_C>uJ z9vY4xvDg?HNk?Wd;WbXa5t}`QqB-l+PZ40EKk8nT!^s@hqlRmd)_;_Swf}LQ6?cD) zyC&>zAO-ArJ-rtrV-&ah1UoUK%7jTyZmTA-4>-ies$bk?`6)3ay<39CIifUPzb(Hc zV~T$DkRb82y?^{#jPcXjt8(50YUGs5zx3|Bshu%9 zy?IN-{DiMI7=vEUoc@^|HoG04uwl@}lIz@1i1W~4A#;iY-$Ln!rM90UOi%C9L6foO}LNLbz9U+ z-$y5XOq@7PF?aKd)Fb8TIyM!ApCd=64lFN(AXA^Xhauc)xCsKSJCBfD?)6hEz9Jw ziuf8c%$@U9+i-;76S>V@^z*!u=usm|h1)`-p*z<%{7bJon|hOt8K_2NIWU9_F4t`d z>do-au?Y=m4j9*=?Rbtwlc+(8u#teEN_z7`5`1#TRr>S@eIVlBFoyXs8 za9rA?@4V4Ux5xr}t28hjG_#k!4fa4@dPUy-%8 zWiHbW-K$AkK8L7{p?0DDUX3#@AM>yF41KG~(&0?QLEZ882t2Y_^UjhKyhAh6Arqos zXr<6`!FSV7JwkL4IQnT|PA`w&F@B!|!;qzApXUlNHe%|`y=l|rPuX;f^&XQhNl3*v zR^U7zhwqPbwl)HX9863P9x|eoRS>E4`6Q&Y%>v4rtO2^SS7#ezJo~9Oxot@G#b$( zO7ZR)wco%$_0EIGXlx608?o&z-n9*)X91h_Jn~?j059P z5S;x)Vw_L_dBcdI5P+VL0bSua32_tWLC5^RLka+VmjOUb!a?goC(=}N13Ln~_w$^vj{b2$-2abD`VaZ&vH>ZG zmFyn^(|?nX{{g#Q1x%wHU&lhZm)`tP?9a|8JCSsZBkd=`}GMGUP$JAOcf1RKyy z4f$n<{Y8!W&!)uZf`}3S-^O1M8F|=W=!d_IH-i5d;e%qZe|pMENCf^eI)wc;QUiAf z&w$#K|D+>>8lc*glk}q?Kaep30UU>*AlyTu#0@+g5+>FW6Tj_`PaHbKe6U}o#1=$$ zRWz}AUGT3>Ze*UAeiip*4s*i(UH3yQulO?xBFLWpF@75sg8fy5@yGV-AUXh{Bfql$0Ui%=p8x;= delta 20228 zcmV)9K*hh*+5^MR1F$OrldI+sv!!iA2nD4BB1t=w?R8Fn-BTM?6#w09HVexJQb;J2 zwt%)Z1WKw_w4|+VfoNzbl~53^I+x@&Y`g5@W&`~j`ruPv`l3%xUwr9|2-TVPO=tQS z`1PVQI-}!*ALGZ2Gmht8!bhio(=nNxd-t4s&hK|V_U6GqAKwG;4Bj#k#|!mn!3ik_ zrO22hPS)Xnl!?=Lu>lF3k(#q6&S9tl!x%A;HSm&&C|-`7nSuJ4$YE59^9IHYTre=s z5OKV6S@;YcdCxDW%RVnTBE97Eg$3cK^U9cEs4EFalzAW+j&65w*jsWPkC!g`UfCCw zO5Uyn!d0$&7ksg3d)3Ou8Q~X&8!)gO;h(f!J2=gMa6Y*UfyaXEnPLbJc_rf7l($`R zp*lY+{7F9Rkfu5B6}dCTeOo@)l;L2`t}t{Ciz~e91Up4$uyQV~Lk_Q01Ua1Ajn|?7 zh(@JJlxns@z=LXKXpXyOQDSIG=CATao_0l$zBG}`jE>5j3|=b901S-}n;D`-&!wP2 zUby9dV2&y~%3!Vsml30cP`ozA7it+NBv*I66}&78UY1jWdU6e`wOI9ivOL-|>AVJS zd+FTx$n~OF2yD+K7Hw47V%4E3dBjb{rFJ(2UcjAonz2oa>ngM0Rmmr7OP0~~IQG5tB7)^aJ|vBTnEa#V$ph32lSjAf7@}u^U7WSwm{qtIqY&UWXQswUCzHzlQ^ob3$K*Nwi~!@1h}u=~P0eD&9uZp#BM>Gwu2c z8t>mx-~&X^s-^J+>PY@fMg4^u^DB=xke(NrbKnJm~`@K z)tKx?dRdheQ#+ZIrjn|IHgL{efYm^G(G5|{Yl5%P%>!;7Qo+B>V*vx?>q zHd-H1(f;0{)yHdSaXhEcLd0BpK948WKQCQA$Ww;qzfem91PTBE2nYZG06_rJx=!pY z0{{TP1^@sw0F$BU9g|$>8Gn^k-BS`#6#rdB7uQ9JP*d|GH3L*o`_eR1G0H+EP}A&X zg&o|&U0RmZf2e2c0iB%bp{6f;=nrb9>D(0=L`RK>d(J)Qch3Etv*%t8{(k%fUHT13R$(*^aXr`KwP2FISW;9JPLTNdhRR}W>(T!9vWys0265KT8Ohz$+)B2{C z*5zdP$poVeO)15UQh)fSZX`>5s;)6~d3}*r@>@BmDQ56=5M^*?c-|v7FT#pR%UUWJ zHw{%w5y(LxQ%~q=hH4AHm{o|sGj7U>*RyiQs#U-=L#Ox5A_hl!2W?ve4DIIt8N|4r zGZIQz<$ZJ>xdNP@ga$N9c!);=9!r?P6A4cd5il!Z4)Y9+<$qO7<zVyx zaM3Kpls7pgOYnv53~yT5-dj2n$I^EnLsIj*FM?yJjK=1dR~ULOnzw`{eU-&ngiNKZ z$U-Qobk9)3$A7#yf}SJ%@gc3^Ez#(U^?OgcPev35f={=pADW0t32HlQDjUVKsoUl@ z)p?=ZlyvwM-~~fnY;Vnm^2KTNZ7r;ReF~iPdQ>W#4lLO8KZ&@dKc@#e-*It zdjy6nvlX7Hm;hL9D;9-6X--}j~&BVY%46YL69u9Lk=-;Ux z;fbbyP)h>@3IG5I2mk;8K>#EG$$sMx003AZ001EXlcDG%f2~>xcpJxcevbsPOK^EX z5+&$_WgQd`(2{kMmTZxtBuKnOkd!G|l2^czgau;Z#X=OFIF9YeiR~zMY$tJ?rf!?2 zZksrfoCuUf+e(kft=%^15%);j^hl4iO;6XolCb{_79c=Ew9~KpgxQ%lZ{BFl_{?3a(OokD-_p*s2p4=thZd+5vbk7D|t zMDx!o{fmcQq<>ZD-^BB6(fqq;-Vx1zc<4*?pC0-zfBJ9H{7*Sl|3IZ5dgwqdD=Vrf0R9D#O-KUw@nbM2YU|p^d9XwHPqQ3 z3ikGZt?M5BtlkpS?%&_pe<~C_h7ku# ziRy`|s;|HIK!0Z_bgJVZWS5GF!Mcv#o}SK*0cbci5bW;k9UM5-9qj4~hB`5`FNDP# ze`}b0{hfRF6=h&@$IQ`Dv5ys9rZw6!YUz=f(K2D_iG*Rbbje9rs$krsj~h%L^o9&8 z88zcfHHmrtXf7t_M(%@T_ifR5)ZW9?UcZ0^^Sw8pvT2CP)nP_pWOY|GZuF$aPaD>N zemZ6d|C?bwHl$loF?NV9dn}3|uUg1tf0$@4XxWdm-S@hUm0>eJ5*)YL?_?gYo=HC8>`XgH~*g|GL@~ z-mmZhg%2tmRQQm>hjYwPrqy!-f3s<>^H&uRLX&Y@KUZLLN{FdL^xE}gG(0ymHWdy0 zd?$$%@PumLagjPeGe9ayGqcu^;(^}6^jb4C3#%=9+HeZfASdJGON&8 znzuqCe3zU+$hw$n0T1C+Ot+1}oF~>6k5=KfrRU-j8`T7aPM8*U<1G*;%YkWeeNhP> zK^rpS5pi@>WCjkv*3M4lXl^r^f#PyAnNQqng;6w~keRZ=hA37*L>7qxLXJj{&;`*v zq0tBFL5&`wltvFzim7b@e-vByE@vPla<@hwqVpPkjGjOQ#%wzgNC@N-n^(9;<6gRo zVipt0*%_w5LVD+)tU^_v!bddj=a9w&JgD&yAJyntdP<{9^peJR@-Xl-TeR&GdW=YZ zX#+d*AuWGO$Ui2U;~L+^Cp5ZDX^q~XH{n-daI*}g#wYm{PRs>tf7keK)-^sYnlNK% z@QB8vJf?6|<9qmw#xY^{=NaZyL0sf2Ar6pm|ba)N154tRQV>5a2ItHD2^C;fQ~ z1H$Zt!uM)yaZ+QO5mr+8ti}_Z(D zPc-jC@%u+~I5X1ff44I-HGV(6DQvnQm7ZTk8h-#2{D5daD7^BZ=shHwhchca1m7-z zf`FA-Bl~eGKw;kGqW#hkzis*xx|KBiLML6P*O|&>{%L%kA7MIwbZ>u8u;+k(Fe!F) zaA2U%FEQ0$2&#VbtYP`}IGmj{!Z?!sv$!dgWX~->7Wogze{}FiP#RYBbV~39{CzP4 zh$@yPqj04^l~WiBphkr{(~92bK)5?&ghnsZRgFK)AJO+>V}n@KyK;ji2O?!+(PV`-)BtUS|e6A)l$Ol!y0!~rv3B>6KM ze?kC(1n9t72d*_|#x+vML(d0mY^ z$)5t86+xQdzT9nZ)j}Y;8TX-EvL)z19!{cScOGoN_;n#4jN*Ch`E}h@5dMKN%bdtu zOP3TqbeRtSzul_EWhOrxCqWmmioyUdmfDl@EML$~Qp)W9=e*E)l7{UZgNRt(wV;4c z%BU1-e{~DQjH_$1hyLsp+C6?I619@@B7Y2JWt-A}InL}|5`|Ge|LX3mFMeYcb5+=G zJU?*D=g2I$D0{K1e&gO0?)$Tj+F0biSNtud7Rw!Z&Ow6Pd3{jYAtmdP9K8x&Daf6r zd2T7ZT8n!m#3HtK_DukO3PQFe-y6#6kGG3qe@#KU$*D@|+IPbug4^(Nk2T?#nH29~VlocV&F|?(?;QMXbNHPL`9l1vojXh#7EY!d ze@sZnl_T(>@R%XMQbGTqnY1&#J^;AW(?ve0=p9KJ;+POszTeVE$K?$>@t%@*J|*~n zTPCb_qk!~Sa!x*E-E=HttVBK$)O^25Vq2y*3ZT(9pUrtyfs;h8IO5j7OCYlfgk!U> zS$7m!b9~;Kd@1u@+?L&F4$g?i&zfftf4^NtoN;{NG|Ii|35T^$+T#0LU9laC&j>5) zI~GbokrlJ=aqbb*8rSVPRu$R&4U@Z#Zlcw6jF?O+Cm-3ALjNogmCyt&r*kx!8{dcV z`|`%`$N2ud@dq$|pkVA3uVd(Y#T%J?KI}a4QiZ1nypPa_(S8J@K`J8`p5+aVf85kO zMSMw$c~ml%plul^|QWD^<}pT1?yFydAWj zc1oMJW+dlq+K{tpgWPV3>^&rHe-b@moeNaFS~}LGfQpir1;atKoT_s-~%O zn5U@f3RMf6N~KLzQcfGy&~92mw@Vwe%zDR$C-H-Z8sX-T(^JruadW9$S>2STnl#lO zZ4i6*&Tcj%xE;>!K!2YU?9VL8ZLXT0re~zGYWf6y5-UF?l`)+{|Jkgvf6}w$mY;N6 zxrbZJ8n4izG%ap*Pt%g&X{sBB;-yoxtjFh0ldsj)(CBkb(Q^2HMXTa-c~|N&)YpvDAg(yOZulm|0c)p6>f1fd2q}0NT-`|6J|t#8HBks@JAgD9L^Ovmdlb|=X&5zsHyx)i-9;mG z0(E~9wR{R`dNF?*f8|{CO66vW0$@K8-aBwBJw9(Pxlh3F!CDiwb>7p)V_RQdrC` zK+X)+FUZA`>*l%{7_SuN1NhBgj|G$DOtC^+BMM!d0k+f>V{ra}1}FVU1tIE zJOs+av=-PVe*#Icv;i-sA5eiXVMgL@x`B^PKZu+ zb_4xpPmMh}5ZjXju{|_}1S!GlopePa^po-gDft0ae;~4pbN<`}rkCkz#-AL6Qa5HU zz-@hLI?~4uRO}YG%wIOVjbzGM~#=hRI{Y zrH$UZ(sTk0$G=7=FJk50Vx?ZV(&yr0+^sGduG0cN5w8*iy^oF{`29G9AHXx?qXu|} zP);h!e_g0KS5dpje+h0P>eFboNIWJQ-=d9l>vl$mISo~}9exU)9em$2d6~sTJ zCTZ_UOuj*HI(B{=N_Pk9-3UWJ9zxM;nCOWmueg4b|J zT$6h{m@&xNn;QqhZ^+1K2*hv7y?Jp={FdCC56AyE&HbuE~%)* z`{ew8{VG0y530yuSj7k)Qt>cG6?{m+WfkjjMa5mX>c@xWhL8C1Q3W59pC6ZtpHT5h ze5wkc#%B~fA~`=RAxZumJ}-wasQ4njq~go?ih{4I*p9CW%<9YL_EsHf@W=4X#!XStb|ln30kc0mU*+yDdiEiXq)f z8T?q7uV*wKYiczU2|d{_jot0=5U4V0CQlPcZdhBqq32x6HWIsYqVfP*$F>neF^B9J z{YhT)q#hb=|>C#RYYaf;EG!wM5B5n>0NM+}GMIquWa$ilB z(tg&6rfrk_i@f*`6mm(ox1Ws~t~m<6&fw_%{l#t&xG7v1kiwaat?Ej0m0nQ9-cTIQ z+N?tPGNy$~*!*#3rPM8#5lO>t+PAlhYl3p-7Z7{SC2jp|&K~lF@)B*A*&5e>Q>ixN z_%<`0>~FU$$Ns53wjMpXQy+42Ucom6R)r^yYKf{_Cbj8CAyj+Jv=uen^qyIAzE((q zOal*yHuFp}ZtDFS_M%6_6OqKyU<)_jy!xmWme;hjvKfn(){0Ki*@DmM>;-}1_@k7+9 zrv@2B4L`%r75qZOFYzl4F+4@X5Kd`0fu}0?wT9o|w*qrK%<7WmI3DNWb{Ec2<(1N* zzbo|M82@hF9&Aaaj0CgBl6=3H!yg3dJ(#z$R;6rCq`#POu0emqp9Hjj{5+yb?#>nC zS;1d4{1t!G@OK&9f8d&if8rX;!=20vYmq=z!IppF-*Vq$3jU+var{@IAR)vQ zMU-j6C(0F3p$SF!nNK%3LG;vkPV7x5?O4LdEfQZ;YC@G-_>NO~O;ia@U~{XUOqzD6 z-=L8RhAyQh-sRr6#+%mX<|CktTZ=1;F_3$Yl@huiCJPcGg1T{>?Y?*o6oZ`SjJz&`R?PB&= zyC`j1Z+0all7ntrPM&3Kpc8jbSfp9SdeXwxi?n@hZAPy9FLbnjC zDQTgTYUhOp7xkEb(qMSs(`t)llXm!qz+MG)tSfo07L-p%fMPgS(DXxIY2zuvt=XPy zUM1I&bBpIyrq~6I9+1Uts*@QMmshhorsl+VrqaZ6Qd+lo9Nm~#=H^VgvGgvyB`ajv zrOP{(W*I|qUEUY06#3VOCly^U%=*b~rB`aksZOnRO{dX+Hp?{YMVslq02YoZpJGU@ zn0>CPm}kRS>Ao(9>mK=DaqmTZ>Xe|4uM%(e_10MVh!n|PC36=|w|GZ36c+Oc3z%&> zMZJhqUOQ*!JF9olGSA3+qvIVJzMkly;auB|Q)xX;2hGUmcl+6fhJ$3_(NE|M-0dFT zKjg8;D{?b`JoZXW+>VuG=E=tF95$*_Hefh)ztE&H3-g%?9Vn$zY1_=czMM>zq~g9) zQeWv8_MNdF;vC)e@VeQU!sU?%+y$K&|*pHhb z|Ca>tA&3ZeLSPqXQ&7cucivp%e0ScwhVwmn^J(yZ^P4wyj=iKb@mKJ-ym1&)E;%gw zI952s5cYG_Tm~G#6Zl(+J{%+$H;a3yR26AgM^F}7Is)HL4&}Q>QPDRHrP&wsW#B&$ z^p#&mWnWpKs;AEv(0VeMnnCqAxki$wN%DbF)N*H_xja}d_tph{jTuaDt{B0LW+kYQ zS}}@$nPi!j!R!ozL9Wbc_6PmTM=)1T<~3I?8^Qc$HK;a@VnJW9aukAN;HE%m7&nh% zVPDWcj9Z4WXcUVHv?PQ2akIB0z_FfQ4%5&ERAVV-VHxIQIo4nWImD3`!ktd+M)^ECOm|iygCqQ!LJ60MbQoon z^B{B_Bi9}z5k)^;ew17Wjx!tvoj!m;D3o;vua1Wq$Me+U1Wpp|0`-d{!EhuUIRYlX z`S!?0IZCW4(lR=76yd(cK*KN^N3fJW%#xPok;WZTO~rt1s6z*q(0pmsOcx3km4Neq zb<{CRl`p=mz_r=5s$%?>x&JN}CD)F;-&}tfXq})o|ZwoZ+mFJI~@AweO@=XYnL{&10&$ zt54=%Eqr?wta}WV3hoMZDHN*8M`zaHJ|_==1&x8K47S~i>2BmX>Byi{sy%`_F6qXy zyioU3tbsYqwQ+YYaB|QUS_UzPV)&xXidml(Q$339M6aQ!VeBZ5&dEHu>MWdKDod`X z{|}RcoA?AdY!u>?f1EFWSb2ODcNPEsvd1iw0n$)H7igPWY;!N+DulyALNRR;AR&YV zq@C;zn<29^>+CFndQfexN4@KndY@QDrPypj(Z>5gYrSvLR;@=p>mT-0MSX8(H`(1R zDKVeV{?2#(-uu4y`%TXM=b?uItinI$VFN5~lH9zI8=IRHH;#;d7NjK{krBd(grhQK zqZrQ96n<_;MNyi7(nUe3*(^Kchl!K1rnyb`Zsl2^-k4ekly zwM_cD5MIx+-XP7v3#nkgZ7I zJ>0xk!uvvae+VCc2;qYvd`LzUKFk{*VQD8Md{o9d+%MA!KPKVh5>86^gh0g+)mULz zQPmjGlQ-#xCa|F6uzEy|=vIX18wJXlCZ?yHHr*Cjl$+W5VA|0wv)4AJm`u%y^mexs z(`8H+wai0$JZ-B?Cs5mA+3`r+R%3=18L`!5QnMp{Uf-I3PfGmZVl_QO>Z-NtdeRAj zN>7=gn(;^v5twme2s%T0YQ;){<)yT=n<+;%45r(po4T__;I5k42n(H1YL+|eB_C?0 z)wO#C{H<1uyuPqQH?^*GVon^u~eHiVj7kjBgO&3T1q{nwH0GcajayAc0@A>k97D7 zPde=zkq)9I!BvH>JC@A3ueykKQ=vPy5byjRM~x1DcdAL3MZ%{foRaVWSvzHVO2TP@ z%X7|jBf4|&uoh+A^Lq5SsXA$!)NP$fkY@m8M>K8Qn(0JZ$%(A4ggtVPmA0dr=cHV0 znwX40v)zmuR*In1sX0SdOv0xXJcuy`+i{bEP1vkp3pdZhjS9A6n}SxfDcFGw$;wxy zU>v)D1eO#-bX!_CVw$aB0%sIFgtHXaCTm#1XL!B?pH=WMCKY^+o6qyw7w|;|U&5Ca zd<9>X@HGWr$2kSxz&9m4qTpM2RKd3~Dd9T`zKib(1e%hn?I`#@en3X$06@B{S>X>Q z{7Au%nd>L`sf3>?_&I)|;5>e*;8%D|!Q=RUwSwQU{@)@_m}%1t&0%)JA9>uekCC7! z@H+{=SMUe?QNfe=lY&3vFGO4dn1rZSD{aK8P0OiHo44!9YD%DL$D&R&352>eHD#GC zB=xU+;J@MT3ZBBGz|v{&b*D{7PiRv@*;jOgo$Tc0vn4BOFUE|(m9v6I;QC8U(Ol4f zv!#p5c40bD(V1RocQh(omYwsGYf+w;mR?*bp*Cu3s^jLaz=o2Qwq%W*QJ;J@TqNhm zHD{N~r}pwdqIs8^(2BEg`Zi$MCY6!Kni6Gq#!?pM#29icZ%N?Vno?!IxPF)GskR)@ zTyv>z1@)9?=R&e`>tM<<(vG%Eb%w})F={lbrRbtsNmo^T&R0<3G3HR2vuc}JZNqG8 z3px2TIo?&wTRN7dd5dG2hwPqXDMzEL+^5-g{spm%PUg`0G&PZD^=j64^s+2U%Y2Bgdv>#nm}fp7Li$wUtE$Q0&lN#`9dXoPh%6rnygx7k1wq^Gv*piR)4P{eeHmOfiuLswRF0yV76dPP8;d4zd1u1}7LOuCUDc`6SVH|38H9;`X`d&w z@;*rZ6Y%>s)7(FSWnIgEM=?CB3CpKUXz_>rSy5sFS7u2ouOfoR46Y`k4641&Ygl~P ze+JL-A?)|0UE7zlcmgY0+}-C2v;@L|Gq_G*6q|W;y`bl1s3lmWq=uA)gLF*KnyjL5 za00b`C;mH`l^n>RE`xg3M?czZ$ZnK*Y8y}Bww6GV=m?4QEM(z-l`FleFFS26P?*QI ziY+3AtEULUft(#a@<%-dgb(M2WT|T{jjJZGhT^fd&z+n)i*@}xx?&tROhSl*A|aCW zG4FRVbvLeY|A$e7)r9Xggr~LWp%43gG#ezm#|hsfg!1Er?>hbrt9D2ng*eA}TUp#>U@f7xp zf8v0+fLmQmAA(H!mti;d5Q0xqPW6&@Kgq7gUK#~S(th-jZ2?Ag7W*~garD!!z=*h) za%(^F9vAoE09owA)1neDz(JC79u2PK0BHjhqWAW0BTuM*w9*Xu0`^G=N$7qC3+CTQ zb~!@A-~}v}5S0*n2CauBH2n(){*4GxK_fOl2|84# z0ssIR1^@s7EtAm57L&m08k0_?4S(H2Eq)-Pd`nv@TMC*G#TZG9CPgnWP4I45hO)5z z$Zo6tBX79SM558eOE3IU#xt`^TS^TyP0!5Po%cEK*_quxe}DV}P{JbvapYxKPEaVw z@Ia<3I*M{!HIP6_#~OoC_4vLkUN&liVYGb2-*d}pST7t`Jf;f=+;Q8U*nbwj&#SZ| z6RdD~y=v{WJf~izReHFJ!F*NsTikWG4uyTJ(z@`rT<-hAXV}bMROiYKuWAJ*tPdV< zHic(}l!aaz)zP*Z`&4AC?9|2Uc5P31Z~309Ts3U&R=DTLJiMsa&P?lm+qNlT*vOvm zaG2`xCr;gIJ!P2hgA8b@LVspkhYnR7rh?)472!Dtj@W02W^?ZtQadefA8+$!*p$Il zCkv~^B10j2Ww>NTJ{G%xk_2oF0q8#(XP`9++8i2m{sbk@+ERUWGG)@(X|z3C$g<*>R4x3x}qZ!6SytILxyzM+nc>3VSl$6CjXC7n^eIJ zy-^8z@gm4b2Q+!Y*Y|8po*oNPhVgTE1|K=$8&ALnXkbp|Kex*epiboI=h7 zGECwQpk@-z)J!%Tp?@De`LN7$8s)uI{wuWK(6vv{q9=4A+T(Sx$7?DC-=lvFk>oR$ z9>FwK4R}__i;?ZvNng+7J)9V3C5Oawn7<$PzyHjLZ_>yfFz~w5=7j zt*u&W(N?Wp=z^`NBxowy+FDzeYHe$+yWJPNecFD0rA`0mzM07+c?kIX_=Wr4yZ794 z&++2nm2ybq=^o;rJQ$=P z&yca1(##6-Y(7((aFFNl+#ns`v!t1)adD8Q@O+_Ppm9lnOM`S5muXxcr0HA{q`SFN zdKSuCmAoy|cyW-z918LhUK*r2UM8Q*rCA}(%JFoJ&kpb^jjLsNb&%f6Yozm>0DrHQ z=ea?C2iF9+Rz~VX`gKBBAEY8)AK>%kxk2NGAg$z$8lNAeRag$4jnZtArb+m0mZ@6; z{7&iFs&TW%+XB2jz&oU4XOL?7UDC7!>3QCz@otUZEw2{@>3n`qkT&v#8ebHo&BA>n z8v$;Wk2YymYTO>A?QCk?5#&zpl7A)=q@B`pagcU%D8PH<8I@*bkYgMVa3aXve91Vr zI4LUG0Zz&DQW2;}1=#_tPKjGLr+zYu;vWYQbrN!y4<>$=RgJ?b-VT6Iw)nKYA3p?`Jt>ua_* zZo6<@L-V$+4Yk|1HEeFWa7)d$4NL`%7aNxvRZ%0}S=DS?k$C57rU`Wk;TN}e7}1m& z;C)Q~Xri;zw3uczCalh?PRnSInpHiP(cNuYRgG#8GXw33o_I82v@^|iBWzfg9+y?R z4ZEubBF0*y!g;RSge|!=n13|g>}`vtl95Zz^^vGq)7EAtlbejVp=7Ia<4}LX31H`6 z6NyLcwM_3Rc?-SXT9cEDUAlwGTbF1znI<(x;$~AS)@oYY3=E0~5^Y9whhatJJKgEE zyCU%1OxKkiUqkv}n`Iidxh|5lnO3=Ku+w?Mp&gOVlx5hFM0|CrqDpKcu4v00 zXDU5qR?w&&%UhAwlzeZuqD&JV_Hom$+P<{`B!#&o&0WTlesJ<>|P~)r6 z-8j0NY1v7wJa5b_tgOk(>mpWGs9~LTwfL?`w|v8vz=_!{(~=rr4Yy#hEfs}%a|E7S zGLlQFTl6r*G?5Yj%?v#y{Od}@I*4hW}8@9nT4(uHXn5K=9s#ZxNj&8P%wmqAS zZiO?AuhICU8hu&gk1akz%i1t?|c))l7%4 z5-TarU0yO)v6Cu}$kt+x)8GW7%}yCn1(k8hM9OM2RX~h4d%Mjx+iX`OfvAH?s2X<1 zQ?BYhK?_JH?SCFgs?j4@q&dGQj~-T_P4U;)p*TLH6`6zF7^STmQ1Pb1ybTG(AEWYEEB*! zW4BwL@Br=_{TeP##rH;_@tLl1mg@nZ8Mm#ztP_-hF|`U=tX@VWt-%A?93jDyVX`@= zUoxYxihl^QigK9M$5Sygo7z1}EN{Ch`-`?WlPZhGuI@mRKcVp_0oArdcVAAXVp>?@ zn!(&q2voHRCCab*OMba#mX34M=%R~zI4L2i& z>nhngDZ^;FFj{l^jB@L!46hX@=XKI-li{^ecz;!%4zFFqlh2mP?>vRcr<-Z>dY2Bb zvPxE2ecDLK4X5#GR*M&%wz`-dY*rcGiHS@FzEH??dW;^|>38&dogSbEb$Xdz(dm2i zuufOdM|AoSeORY{8qnz)z77kYR@Ew#uGi@*x>~0zX`jY7==>?(uk)w*MvXrs9|v^4 ziGOd_`Lld8YRI=h`(k1CIh}9eTcJX(hKTp(4K=R)i8_q2i z!V8L%3&QOQGZ~I2>@X@;+la)&M!XMX7Js(agru{D;rjGm8@3bS4rKDM*^6yC+817& zrR!UWDq~o<&8-)sTj#s^9-WVHzs>`E2h#;76e7KL5=$h)v9~9I&PVxPjqlTWkiW&W z@#Gqd>kLbnW_1s{%mU~8`It_hr`vUYfFIQP+b}72?U1r3(x!5IIMLxYHQZsqxqt0* zisQxc7J%E8CT8@7yNpZCaI0y?!?qFYmLeeB6S2D%7RS};z>003wonuc=N7nO)MwS<;(uguJd;qvQeDcB)1CEYTe?oHR$c*{aE&VV#ti8E z9ljCu%`m>Urs8%aW@hUU3A%?+6%1$J8p|^JBn9jIU3yXH@A1Pre_!4nfdlCUiHTrq zB%Y3AVekV~0Vk@UMxZ-$D)6;+#S$oaJS&$k*ZGHtHE?-U=f@b~`-A{~s(*WB#}sne zyqz(ff5cA;qo<_#@d%}|m7mT}i$%O*Pl>XhWXMKVa611~$Y#HF5vTFbbbf|^uJf~! zJB!BVn6wGX>Jq7FyNVo?xro6`og3~RE_A~k39C9R`R5lJKd1Ba;utNFTo^~ir|}Cq zzsN6X{Ibrk@T)ril7EHa9)G7k)cM!^8=Zg4ze7ptS`q2=Xa2p;e-KChk^hvj@R+hq z=hr0l{aM^RbF>pSkErLS<)-1>A+i5o#2tUt=^yk@o&UyP0t!!@{FxS$Lil8MXS(oou7Tdx zol3zdvDJAftKRShOAvI~>y<0scA+-XYNxE6dj;t?(kHYU*Rz(w1$I|}5eGNBst&>l zxJF#`IOPHq91jJR27mE3Xt>zr`k?eA*E?RItX=T6yS7wrj8fh0hAp)hIvmLP+Mto$EPObh9wIisL(->yE$6DA`{A|s>Sbu8vb6mwb_4A!K8QBZ# zdf>@17R&nCE$nL(2^%3`bZJiCtB`&si`naB{Bd$=wcfdk$_E6; zs1Zr7%ha~8r+?87M4ff=dqXTS8N$>V@kAW8Y1ENsYKhB*iHe4#SX#u*b=2_fkk(^F zY}6gt3{-69Wb&e%1U2#&b(;I-gsgYQ@KE}eOL_wmwT zXPSRXORmmva&~ChSmZ8ndvo?jsGNb-Dw{PXdXUx)$$yzOa%o)G&=7%U@8*sZV7fuw zLM9yyxn9eKN^-q5@LRR~~(k3gpfK?*(!Jp`KUL zKJ~ncuEz5W&|X6yMf)*)T@DUjJm-}S(73We3bquC&!H%ibQWZoN3*FIZ}aI|jDM6lJn0kkNh0+oGO>CSsq)mD$mK!r zb#y&?M4F=%Bn{8C<^42i6Pn3QW%tlTyyRDVL*9NWsP@U@jA}pnCxrZiG^M31J4&HV zgEYORe1K*&c~*GyC)2kA)xJV+-mNsVGUV&0nJc`7-dl$LS`qSj3ZdkzgG0Zn?5EiW zNw4>LtF@5UPiJ{= zqwyi%wzKrt9Hj(HZXp(JKmRFCHdN>LN_&`#>5R_dc}0JM+Z3qZafu2(UHJ^C?DS% zh50zm*QoAInlQqZ{WOq<<`8)LM1TG=c+l5Wc`q$sIzUqjO1$?|X^W?#`6!hgrSdy5 zQi05KD~2jZ4|(pTg?R*s3Yw2n)%QWPXcUnQEWT68AikJSB5oUP+Iyh$eA(j+J)m1)BHDwh8w4~ZwDIvP_CRz-%F56kK zTvG~`H@A4vv7))fSJ~VG)QZB@zCl{q67mhu*$*7f;?L1}KbE_Z#v|yaz|bMONug#b zo@WCywZO==DrEim4$`-wpMMY3E9pCUlk`)UwL={}q*A23Nx0^zx9_U3KCF@`{|gLr zHT)huoOOJjis6lev0a}B{nkHzyv{IaTY=skyg}&Qqjs)ToCkW3uKzc<; zyO-AHkrR9`R*ZJ;*TMe~41Na`{RT|~fENS8s~}n}-Zub*8Rsshe18=N8Yq|1_vsNR z0Lik7ZcIBofSKavysUGbp8L@w3YU{-2uV!K1jaFqetWgH+T~Uhs|qs@cR%gH+q%zn|(_JO%6E^@4i%9IjvAJV56; zd3*!%IC4HHS?ZXBRZ1JqQFyuKN>-U1u?`{|u6)q#hpD5Mo^bz&tH zXzr)xoyd=3;%!X_X{N(=2VSvL9Hn>lQ;T%$5)Xy3S7?K@8yw$Va6v!4M_`CYKV8^Q z7afK+g$S^#XuEpefF$C;a2HKQdmpSl2>acSo%0wd9s~rxO@GyZSuJ3-6EM06SlI(_ zTt+{@{3pTbQ3U8?@X3?l_Beq21on*|VPAL(!2L01{zQ4S8*tr;-RKF7N$?ee{wb{1 zMYBM48P;4%Tj^;~d$Hd6^i%p7d~*$GpP`?lZ$CWy3_YvB{!kVJ52>(5K?@B1LV>*y zsCq67_Ie5ghJUbbAmd~F)oE0#(eoN@)R0igYwr(ft6W5tfb#4KZpVo$n$TSsL^kE)|+6GC%+e^ON6JexJ1(RgVZI( zLBv7w#ecHH?2#QNf|n=*b}=Vr;s>Zml&g@B1k~%NZiK?qgLDbv$Z8oeHbBV%vQaIC zywa5l`3LAyiK(80G{K3ko{;vy!J$vdqP@}?P;a3C^0AjLz|L<$I*V-e48kW;Ozy)j zv@dJGCV-9TFBttBO{V`ru6`ZH`3H>mPdbPGmw(`OAHF!n)~vc$-)Y>}}q*=5a=J&T+;h(To%g=;-uHc;^UOW(@!EUT z32#VNR=r>WZ;MT_Bp~v-7-|(FV@#zG5kUJK^ILKwPAdM$MPB&Lg{X%fF7e%GE04SE z(253pWgqdTD{uQY97Jn8_ju9c)lbTEl|TF~`|Ye>eHa3r9%1p9{U5k8rw4 z%7>IAEzQq%_O3k*x%0+3sWW0FFndLv(0NItE89N*f;HBn`*m+9N1A$B#w77b)DEQq z#=l-jn&q4d^g87q9U?4!*vTW$6@ACy%$br>jZ8xfBrmU06r!r?ZhRvKMOv_pLOadO z_n^1jCqCxi?+O%q-pmdZtVUngG=1>1I z98J}DeJzyEz@S1b3CL?Q!EGE}52NGv=B8~^(oH$4DUK4q+2}V~d?iVLjyq?i{xUO_vQ`Ha|Ls*U!VDyUyAvz^5|9{!h`CfY)#R zBLrrqG-6#!tWf%GDH9o~MqiI*Ufng`_1w*SE77j-R@N*tRoQ#e{1#4SCL={2d#3y| zB~S86pv;o6Rb`v3hqs7riCb}ghq4QW<>C3qE**B#n$n|l)&nuMp9rRh!V;8nwdlmq zuRoT2XJ*FusKbT-Erl>Yv8aO%tl8GJOSkwjXS*U8`LmyZT4i z!tRe%rAcVOk-fA4^+8qHBgV}Z_P}}6hUvT}k2B@#VVMGUXM>dIJ*v2)hM~t;dWS^f zYTl0KqJ87HYi`AaTQ-YP^cnBF+`fqBX)h5$-|Ic!Fwd%d#x@l&6c z$Jd)TI=U_9YST~>k#dCdkjRavH_9cnt9^B=tHP#-TIY$nomT7n zWM21SH~d7R2mS=n0Ht|L^t3}&FQezZq;duZBVIZ)ayBtIWKd$k9CuZ}GsFB<@2nLU zW`@wj`+P{jMNQr{t1bO9Dx}3+QPKRALd7t7p|echE5~h7!?(ig)gZ=vwPC~hE}=Pc zEVpx1(v~R4J*cMoz1;F%lh72RYUc_UjkL2ZoQthTB2%R-K%j;0=9ktBB-svTl3n}` z{j;d0v{!;oC%^WrG~ig(?c6Q}clYVG*^0%xEb4C&p7(tYu(BaY6+eDNDDobG#;S=J zzE^$Z;a^-?w6`dPs)1)i zXKGa%yvl0lV$~kh5jn(Fv|?Qrt#0zPuupP25HMY^?CV-@rX6sKhF^*%h@uo`6fL_t zmjL^ku*iGkj*g!+-u7{jg)Ekn@Z@Z(2p|DxVw3b2BQIz8`F7qs?hPScuN~`lAl#rG zHSdm2kQ!ed?~updv~ruh_`q(W+og|aKsS)Sff@Y9i@l(!-?>d!AxbgiZE{(%s7Frq z|B#7n-zLOOcHp+9v0|ULwK1_6luk%XayjO$p2GE(7Y{FWCZ780 z7nFOeY08*)bh|9E>BZ=%@5%31uD}-k*7D^inva)qHc9m1@%OmfuU?u&l4sS?!)#ltHs>y+k+FktWBH z23`q4ou+ZMI8OIK0RGE+e;uQV2GOxS((C&2w7{m6h@z;VJ;>_l_vSJ6`Zb=o zq@X?689ur;(Hf_f&%$*_QQL!BFA}Rs{9P}NoazTIKX_5~@(Nt3#<7;2j8~#bm3f?d z)#_L!UD)TQmY^V2<1JGu(#MlH)GSA3tru8 z&Be$X4o>&T zFz3PIp>{{q{WoR7$tKC?)@qZYrFao}#%YAQYPpwi0kakb#FE;jHnofu#pH55J#njd zYc;rza}rAzn|*GrqmzI|;p%G=(?aQ=UET7(sc8r@5dYeHXf5y~pQ}h$ia6pbEaTet z#iPCli?~;}qOKRjpmjl5)oG)r6V? zHNE&;K}nn7)JfJl5puyZJ93N2P|kVy9~*~oHlJ62(mQOTXXxYnw<1(JvtqbPI7fs_ z<|nSUxS?(IS?CMOk%P~%ap=Plyu#}4V3b*($QC4I7ZY+g#=3>%CKQJizarNe+>m8h z*Huj0jALg{bye9M-dL*Fkww%5BRMLNfw1XqvS*nvQa@~kIGYPdWuH! zKL`r)AjOw|3fyoioKcN#aqr`OUSJxQ*u1=~fH^o3FdHtFIS;hMi!V`^7_2;4C5siKj4rKx+(MrSy?^zaADG@E+v>8vVHd z%Dg;Oh!!AraDyrw_%Y4~(1oxuf!@>uADzTNEoUPe!T@Z~vmvM@v<^Wa!(cR^`7Z^Y z@zDW{RB&}c5pl2_&MyideT3zo;y4nBgT+05hvL{l$QW8GNM)IqG@v<1Y7duE1pr;J zhv`djXrTnvM9G2!lS^!f1KWXqH3s&}UFsije|L}|unzi~D^g7Y%lwG_Y*S0Hh)*mB z1cE%^cUFlC1%S}+X(7@Fpni2K^kx}_`0w>RNC+H&a<%?xi5@_bWuZ}4RF$QHY5&I{ zsBte11hNk1ohTph6SoB;Ed#{C;OiHGkA~S(LotaJS!k>)*r+^^w1R?WdIHn$Wq?sK z8?e0sho<^~DguyQWdqKQvmpMPWne=>zZwGvY9$zuf{6f2qbR6R2#^Pp1T@A_(2Nkk zcT5%98UoyB;RU3b;J_GYH3q6HHKv4qlR5%JGO{1Wjg;do~nGG}N0Q_?v&=Dda;i \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,101 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f93..53a6b238d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 0dd02b180..17756dbc6 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -52,4 +52,9 @@ jar { } } +test { + minHeapSize = "512m" + maxHeapSize = "4096m" +} + description = 'Reactor Netty RSocket transport implementations (TCP, Websocket)' From c65683ecc0df0c12b6c7c049c1e3cb3b705fc212 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 5 Apr 2023 20:47:53 +0300 Subject: [PATCH 159/183] 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 160/183] 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 161/183] 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 162/183] 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 163/183] 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 164/183] 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 165/183] 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 166/183] 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 167/183] ensures `onClose` awaits all underlying components to be closed (#1085) --- ...nMemoryResumableFramesStoreStressTest.java | 118 ++++++++++++++++++ .../core/ClientServerInputMultiplexer.java | 46 ++++++- .../io/rsocket/core/RSocketConnector.java | 18 ++- .../io/rsocket/core/RSocketRequester.java | 97 ++++++++++++-- .../io/rsocket/core/RSocketResponder.java | 24 +++- .../java/io/rsocket/core/RSocketServer.java | 12 +- .../java/io/rsocket/core/ServerSetup.java | 4 +- .../core/SetupHandlingDuplexConnection.java | 5 + .../rsocket/internal/UnboundedProcessor.java | 9 +- .../rsocket/keepalive/KeepAliveHandler.java | 8 -- .../rsocket/resume/ClientRSocketSession.java | 14 +-- .../resume/InMemoryResumableFramesStore.java | 15 +-- .../resume/ResumableDuplexConnection.java | 20 +++ .../core/DefaultRSocketClientTests.java | 7 +- .../io/rsocket/core/RSocketLeaseTest.java | 11 +- .../core/RSocketRequesterSubscribersTest.java | 9 +- .../io/rsocket/core/RSocketRequesterTest.java | 10 +- .../io/rsocket/core/RSocketResponderTest.java | 5 +- .../java/io/rsocket/core/RSocketTest.java | 12 +- .../io/rsocket/core/SetupRejectionTest.java | 11 +- rsocket-examples/build.gradle | 2 +- .../transport/local/LocalClientTransport.java | 6 +- .../local/LocalDuplexConnection.java | 9 +- .../transport/netty/TcpDuplexConnection.java | 24 ++-- .../netty/WebsocketDuplexConnection.java | 25 +++- .../netty/client/TcpClientTransport.java | 2 +- .../client/WebsocketClientTransport.java | 2 +- .../netty/server/TcpServerTransport.java | 2 +- .../netty/server/WebsocketRouteTransport.java | 4 +- .../server/WebsocketServerTransport.java | 2 +- 30 files changed, 445 insertions(+), 88 deletions(-) create mode 100644 rsocket-core/src/jcstress/java/io/rsocket/resume/InMemoryResumableFramesStoreStressTest.java diff --git a/rsocket-core/src/jcstress/java/io/rsocket/resume/InMemoryResumableFramesStoreStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/resume/InMemoryResumableFramesStoreStressTest.java new file mode 100644 index 000000000..f0b209552 --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/resume/InMemoryResumableFramesStoreStressTest.java @@ -0,0 +1,118 @@ +package io.rsocket.resume; + +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.rsocket.exceptions.ConnectionErrorException; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.internal.UnboundedProcessor; +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.LL_Result; +import reactor.core.Disposable; + +public class InMemoryResumableFramesStoreStressTest { + boolean storeClosed; + + InMemoryResumableFramesStore store = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 128); + boolean processorClosed; + UnboundedProcessor processor = new UnboundedProcessor(() -> processorClosed = true); + + void subscribe() { + store.saveFrames(processor).subscribe(); + store.onClose().subscribe(null, t -> storeClosed = true, () -> storeClosed = true); + } + + @JCStressTest + @Outcome( + id = {"true, true"}, + expect = ACCEPTABLE) + @State + public static class TwoSubscribesRaceStressTest extends InMemoryResumableFramesStoreStressTest { + + Disposable d1; + + final ByteBuf b1 = + PayloadFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + true, + false, + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello1"), + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello2")); + final ByteBuf b2 = + PayloadFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 3, + false, + true, + false, + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello3"), + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello4")); + final ByteBuf b3 = + PayloadFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 5, + false, + true, + false, + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello5"), + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello6")); + + final ByteBuf c1 = + ErrorFrameCodec.encode(ByteBufAllocator.DEFAULT, 0, new ConnectionErrorException("closed")); + + { + subscribe(); + d1 = store.doOnDiscard(ByteBuf.class, ByteBuf::release).subscribe(ByteBuf::release, t -> {}); + } + + @Actor + public void producer1() { + processor.tryEmitNormal(b1); + processor.tryEmitNormal(b2); + processor.tryEmitNormal(b3); + } + + @Actor + public void producer2() { + processor.tryEmitFinal(c1); + } + + @Actor + public void producer3() { + d1.dispose(); + store + .doOnDiscard(ByteBuf.class, ByteBuf::release) + .subscribe(ByteBuf::release, t -> {}) + .dispose(); + store + .doOnDiscard(ByteBuf.class, ByteBuf::release) + .subscribe(ByteBuf::release, t -> {}) + .dispose(); + store.doOnDiscard(ByteBuf.class, ByteBuf::release).subscribe(ByteBuf::release, t -> {}); + } + + @Actor + public void producer4() { + store.releaseFrames(0); + store.releaseFrames(0); + store.releaseFrames(0); + } + + @Arbiter + public void arbiter(LL_Result r) { + r.r1 = storeClosed; + r.r2 = processorClosed; + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java index d6cb46d98..e19d31924 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java @@ -67,8 +67,8 @@ public ClientServerInputMultiplexer( this.source = source; this.isClient = isClient; - this.serverReceiver = new InternalDuplexConnection(this, source); - this.clientReceiver = new InternalDuplexConnection(this, source); + this.serverReceiver = new InternalDuplexConnection(Type.SERVER, this, source); + this.clientReceiver = new InternalDuplexConnection(Type.CLIENT, this, source); this.serverConnection = registry.initConnection(Type.SERVER, serverReceiver); this.clientConnection = registry.initConnection(Type.CLIENT, clientReceiver); } @@ -195,8 +195,33 @@ int incrementAndGetCheckingState() { } } + @Override + public String toString() { + return "ClientServerInputMultiplexer{" + + "serverReceiver=" + + serverReceiver + + ", clientReceiver=" + + clientReceiver + + ", serverConnection=" + + serverConnection + + ", clientConnection=" + + clientConnection + + ", source=" + + source + + ", isClient=" + + isClient + + ", s=" + + s + + ", t=" + + t + + ", state=" + + state + + '}'; + } + private static class InternalDuplexConnection extends Flux implements Subscription, DuplexConnection { + private final Type type; private final ClientServerInputMultiplexer clientServerInputMultiplexer; private final DuplexConnection source; @@ -207,7 +232,10 @@ private static class InternalDuplexConnection extends Flux CoreSubscriber actual; public InternalDuplexConnection( - ClientServerInputMultiplexer clientServerInputMultiplexer, DuplexConnection source) { + Type type, + ClientServerInputMultiplexer clientServerInputMultiplexer, + DuplexConnection source) { + this.type = type; this.clientServerInputMultiplexer = clientServerInputMultiplexer; this.source = source; } @@ -304,5 +332,17 @@ public Mono onClose() { public double availability() { return source.availability(); } + + @Override + public String toString() { + return "InternalDuplexConnection{" + + "type=" + + type + + ", source=" + + source + + ", state=" + + state + + '}'; + } } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index 432c0f0f5..de494c4e3 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -47,6 +47,7 @@ import java.util.function.Supplier; import reactor.core.Disposable; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; import reactor.util.annotation.Nullable; import reactor.util.function.Tuples; import reactor.util.retry.Retry; @@ -633,8 +634,7 @@ public Mono connect(Supplier transportSupplier) { wrappedConnection = resumableDuplexConnection; } else { keepAliveHandler = - new KeepAliveHandler.DefaultKeepAliveHandler( - clientServerConnection); + new KeepAliveHandler.DefaultKeepAliveHandler(); wrappedConnection = clientServerConnection; } @@ -655,6 +655,11 @@ public Mono connect(Supplier transportSupplier) { requesterLeaseTracker = null; } + final Sinks.Empty requesterOnAllClosedSink = + Sinks.unsafe().empty(); + final Sinks.Empty responderOnAllClosedSink = + Sinks.unsafe().empty(); + RSocket rSocketRequester = new RSocketRequester( multiplexer.asClientConnection(), @@ -667,7 +672,11 @@ public Mono connect(Supplier transportSupplier) { (int) keepAliveMaxLifeTime.toMillis(), keepAliveHandler, interceptors::initRequesterRequestInterceptor, - requesterLeaseTracker); + requesterLeaseTracker, + requesterOnAllClosedSink, + Mono.whenDelayError( + responderOnAllClosedSink.asMono(), + requesterOnAllClosedSink.asMono())); RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); @@ -715,7 +724,8 @@ public Mono connect(Supplier transportSupplier) { (RequestInterceptor) leases.sender) : interceptors - ::initResponderRequestInterceptor); + ::initResponderRequestInterceptor, + responderOnAllClosedSink); return wrappedRSocketRequester; }) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index bf298706a..9e8d349bf 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -66,8 +66,10 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { RSocketRequester.class, Throwable.class, "terminationError"); @Nullable private final RequesterLeaseTracker requesterLeaseTracker; + + private final Sinks.Empty onThisSideClosedSink; + private final Mono onAllClosed; private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; - private final Sinks.Empty onClose; RSocketRequester( DuplexConnection connection, @@ -80,7 +82,9 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { int keepAliveAckTimeout, @Nullable KeepAliveHandler keepAliveHandler, Function requestInterceptorFunction, - @Nullable RequesterLeaseTracker requesterLeaseTracker) { + @Nullable RequesterLeaseTracker requesterLeaseTracker, + Sinks.Empty onThisSideClosedSink, + Mono onAllClosed) { super( mtu, maxFrameLength, @@ -91,10 +95,11 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { requestInterceptorFunction); this.requesterLeaseTracker = requesterLeaseTracker; - this.onClose = Sinks.empty(); + this.onThisSideClosedSink = onThisSideClosedSink; + this.onAllClosed = onAllClosed; // DO NOT Change the order here. The Send processor must be subscribed to before receiving - connection.onClose().subscribe(null, this::tryTerminateOnConnectionError, this::tryShutdown); + connection.onClose().subscribe(null, this::tryShutdown, this::tryShutdown); connection.receive().subscribe(this::handleIncomingFrames, e -> {}); @@ -188,7 +193,11 @@ public double availability() { @Override public void dispose() { - tryShutdown(); + if (terminationError != null) { + return; + } + + getDuplexConnection().sendErrorAndClose(new ConnectionErrorException("Disposed")); } @Override @@ -198,7 +207,7 @@ public boolean isDisposed() { @Override public Mono onClose() { - return onClose.asMono(); + return onAllClosed; } private void handleIncomingFrames(ByteBuf frame) { @@ -305,8 +314,31 @@ private void tryTerminateOnKeepAlive(KeepAliveSupport.KeepAlive keepAlive) { String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()))); } - private void tryTerminateOnConnectionError(Throwable e) { - tryTerminate(() -> e); + private void tryShutdown(Throwable e) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("trying to close requester " + getDuplexConnection()); + } + if (terminationError == null) { + if (TERMINATION_ERROR.compareAndSet(this, null, e)) { + terminate(CLOSED_CHANNEL_EXCEPTION); + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); + } + } + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.info( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); + } + } } private void tryTerminateOnZeroError(ByteBuf errorFrame) { @@ -314,27 +346,67 @@ private void tryTerminateOnZeroError(ByteBuf errorFrame) { } private void tryTerminate(Supplier errorSupplier) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("trying to close requester " + getDuplexConnection()); + } if (terminationError == null) { Throwable e = errorSupplier.get(); if (TERMINATION_ERROR.compareAndSet(this, null, e)) { terminate(e); + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); + } + } + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); } } } private void tryShutdown() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("trying to close requester " + getDuplexConnection()); + } if (terminationError == null) { if (TERMINATION_ERROR.compareAndSet(this, null, CLOSED_CHANNEL_EXCEPTION)) { terminate(CLOSED_CHANNEL_EXCEPTION); + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); + } + } + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); } } } private void terminate(Throwable e) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("closing requester " + getDuplexConnection() + " due to " + e); + } if (keepAliveFramesAcceptor != null) { keepAliveFramesAcceptor.dispose(); } - getDuplexConnection().dispose(); final RequestInterceptor requestInterceptor = getRequestInterceptor(); if (requestInterceptor != null) { requestInterceptor.dispose(); @@ -361,9 +433,12 @@ private void terminate(Throwable e) { } if (e == CLOSED_CHANNEL_EXCEPTION) { - onClose.tryEmitEmpty(); + onThisSideClosedSink.tryEmitEmpty(); } else { - onClose.tryEmitError(e); + onThisSideClosedSink.tryEmitError(e); + } + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("requester closed " + getDuplexConnection()); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index ce4fe70a3..50c5ba54c 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -44,6 +44,7 @@ import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; import reactor.util.annotation.Nullable; /** Responder side of RSocket. Receives {@link ByteBuf}s from a peer's {@link RSocketRequester} */ @@ -54,6 +55,7 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { private static final Exception CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException(); private final RSocket requestHandler; + private final Sinks.Empty onThisSideClosedSink; @Nullable private final ResponderLeaseTracker leaseHandler; @@ -70,7 +72,8 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { int mtu, int maxFrameLength, int maxInboundPayloadSize, - Function requestInterceptorFunction) { + Function requestInterceptorFunction, + Sinks.Empty onThisSideClosedSink) { super( mtu, maxFrameLength, @@ -83,19 +86,27 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { this.requestHandler = requestHandler; this.leaseHandler = leaseHandler; - - connection.receive().subscribe(this::handleFrame, e -> {}); + this.onThisSideClosedSink = onThisSideClosedSink; connection .onClose() .subscribe(null, this::tryTerminateOnConnectionError, this::tryTerminateOnConnectionClose); + + connection.receive().subscribe(this::handleFrame, e -> {}); } private void tryTerminateOnConnectionError(Throwable e) { + if (LOGGER.isDebugEnabled()) { + + LOGGER.debug("Try terminate connection on responder side"); + } tryTerminate(() -> e); } private void tryTerminateOnConnectionClose() { + if (LOGGER.isDebugEnabled()) { + LOGGER.info("Try terminate connection on responder side"); + } tryTerminate(() -> CLOSED_CHANNEL_EXCEPTION); } @@ -169,6 +180,9 @@ public Mono onClose() { } final void doOnDispose() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("closing responder " + getDuplexConnection()); + } cleanUpSendingSubscriptions(); getDuplexConnection().dispose(); @@ -183,6 +197,10 @@ final void doOnDispose() { } requestHandler.dispose(); + onThisSideClosedSink.tryEmitEmpty(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("responder closed " + getDuplexConnection()); + } } private void cleanUpSendingSubscriptions() { diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index 3208bb4fd..0c68db6df 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -46,6 +46,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; /** * The main class for starting an RSocket server. @@ -437,6 +438,9 @@ private Mono acceptSetup( requesterLeaseTracker = null; } + final Sinks.Empty requesterOnAllClosedSink = Sinks.unsafe().empty(); + final Sinks.Empty responderOnAllClosedSink = Sinks.unsafe().empty(); + RSocket rSocketRequester = new RSocketRequester( multiplexer.asServerConnection(), @@ -449,7 +453,10 @@ private Mono acceptSetup( setupPayload.keepAliveMaxLifetime(), keepAliveHandler, interceptors::initRequesterRequestInterceptor, - requesterLeaseTracker); + requesterLeaseTracker, + requesterOnAllClosedSink, + Mono.whenDelayError( + responderOnAllClosedSink.asMono(), requesterOnAllClosedSink.asMono())); RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); @@ -481,7 +488,8 @@ private Mono acceptSetup( ? rSocket -> interceptors.initResponderRequestInterceptor( rSocket, (RequestInterceptor) leases.sender) - : interceptors::initResponderRequestInterceptor); + : interceptors::initResponderRequestInterceptor, + responderOnAllClosedSink); }) .doFinally(signalType -> setupPayload.release()) .then(); diff --git a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java index ddad96047..5aae22e89 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java @@ -79,7 +79,7 @@ public Mono acceptRSocketSetup( sendError(duplexConnection, new UnsupportedSetupException("resume not supported")); return duplexConnection.onClose(); } else { - return then.apply(new DefaultKeepAliveHandler(duplexConnection), duplexConnection); + return then.apply(new DefaultKeepAliveHandler(), duplexConnection); } } @@ -141,7 +141,7 @@ public Mono acceptRSocketSetup( resumableDuplexConnection, serverRSocketSession, serverRSocketSession), resumableDuplexConnection); } else { - return then.apply(new DefaultKeepAliveHandler(duplexConnection), duplexConnection); + return then.apply(new DefaultKeepAliveHandler(), duplexConnection); } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java index 2da572de3..3beedf97f 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java @@ -168,4 +168,9 @@ public void sendErrorAndClose(RSocketErrorException e) { public ByteBufAllocator alloc() { return source.alloc(); } + + @Override + public String toString() { + return "SetupHandlingDuplexConnection{" + "source=" + source + ", done=" + done + '}'; + } } diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java index 95bc210fe..23ada95fe 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -619,13 +619,8 @@ public ByteBuf poll() { t = this.last; if (t != null) { - try { - this.last = null; - return t; - } finally { - - clearAndFinalize(this); - } + this.last = null; + return t; } return null; diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java index b92c25f46..4fd7a772d 100644 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java @@ -1,7 +1,6 @@ package io.rsocket.keepalive; import io.netty.buffer.ByteBuf; -import io.rsocket.Closeable; import io.rsocket.keepalive.KeepAliveSupport.KeepAlive; import io.rsocket.resume.RSocketSession; import io.rsocket.resume.ResumableDuplexConnection; @@ -16,18 +15,11 @@ KeepAliveFramesAcceptor start( Consumer onTimeout); class DefaultKeepAliveHandler implements KeepAliveHandler { - private final Closeable duplexConnection; - - public DefaultKeepAliveHandler(Closeable duplexConnection) { - this.duplexConnection = duplexConnection; - } - @Override public KeepAliveFramesAcceptor start( KeepAliveSupport keepAliveSupport, Consumer onSendKeepAliveFrame, Consumer onTimeout) { - duplexConnection.onClose().doFinally(s -> keepAliveSupport.stop()).subscribe(); return keepAliveSupport .onSendKeepAliveFrame(onSendKeepAliveFrame) .onTimeout(onTimeout) diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java index d6a0c9292..ca4f5dcb4 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -110,17 +110,9 @@ public ClientRSocketSession( position); } - return connectionTransformer - .apply(dc) - .doOnDiscard( - Tuple2.class, - tuple2 -> { - if (logger.isDebugEnabled()) { - logger.debug("try to reestablish from discard"); - } - tryReestablishSession(tuple2); - }); - }); + return connectionTransformer.apply(dc); + }) + .doOnDiscard(Tuple2.class, this::tryReestablishSession); this.resumableFramesStore = resumableFramesStore; this.allocator = resumableDuplexConnection.alloc(); this.resumeSessionDuration = resumeSessionDuration; diff --git a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java index b71693f0d..e23bc154b 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java @@ -118,7 +118,7 @@ public class InMemoryResumableFramesStore extends Flux * the {@link InMemoryResumableFramesStore#drain(long)} method. */ static final long MAX_WORK_IN_PROGRESS = - 0b0000_0000_0000_0000_0000_0000_0000_0000_1111_1111_1111_1111_1111_1111_1111_1111L; + 0b0000_0000_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111L; public InMemoryResumableFramesStore(String side, ByteBuf session, int cacheSizeBytes) { this.side = side; @@ -374,7 +374,7 @@ public void dispose() { return; } - drain(previousState | DISPOSED_FLAG); + drain((previousState + 1) | DISPOSED_FLAG); } void clearCache() { @@ -557,12 +557,13 @@ public void onNext(ByteBuf byteBuf) { return; } - if (isWorkInProgress(previousState) - || (!isConnected(previousState) && !hasPendingConnection(previousState))) { + if (isWorkInProgress(previousState)) { return; } - parent.drain(previousState + 1); + if (isConnected(previousState) || hasPendingConnection(previousState)) { + parent.drain((previousState + 1) | HAS_FRAME_FLAG); + } } @Override @@ -587,7 +588,7 @@ public void onError(Throwable t) { return; } - parent.drain(previousState | TERMINATED_FLAG); + parent.drain((previousState + 1) | TERMINATED_FLAG); } @Override @@ -609,7 +610,7 @@ public void onComplete() { return; } - parent.drain(previousState | TERMINATED_FLAG); + parent.drain((previousState + 1) | TERMINATED_FLAG); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java index 9704d9aba..c8811b9b3 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -322,6 +322,26 @@ static boolean isResumableFrame(ByteBuf frame) { return FrameHeaderCodec.streamId(frame) != 0; } + @Override + public String toString() { + return "ResumableDuplexConnection{" + + "side='" + + side + + '\'' + + ", session='" + + session + + '\'' + + ", remoteAddress=" + + remoteAddress + + ", state=" + + state + + ", activeConnection=" + + activeConnection + + ", connectionIndex=" + + connectionIndex + + '}'; + } + private static final class DisposedConnection implements DuplexConnection { static final DisposedConnection INSTANCE = new DisposedConnection(); diff --git a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java index 3c95fd65c..fa208269b 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -643,6 +643,8 @@ public static class ClientSocketRule extends AbstractSocketRule { protected Runnable delayer; protected Sinks.One producer; + protected Sinks.Empty thisClosedSink; + @Override protected void doInit() { super.doInit(); @@ -660,6 +662,7 @@ protected void doInit() { @Override protected RSocket newRSocket() { + this.thisClosedSink = Sinks.empty(); return new RSocketRequester( connection, PayloadDecoder.ZERO_COPY, @@ -671,7 +674,9 @@ protected RSocket newRSocket() { Integer.MAX_VALUE, null, __ -> null, - null); + null, + thisClosedSink, + thisClosedSink.asMono()); } } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java index a9c9ed9a5..aad0aaaca 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -91,6 +91,8 @@ class RSocketLeaseTest { private Sinks.Many leaseSender = Sinks.many().multicast().onBackpressureBuffer(); private RequesterLeaseTracker requesterLeaseTracker; + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; @BeforeEach void setUp() { @@ -100,6 +102,8 @@ void setUp() { connection = new TestDuplexConnection(byteBufAllocator); requesterLeaseTracker = new RequesterLeaseTracker(TAG, 0); responderLeaseTracker = new ResponderLeaseTracker(TAG, connection, () -> leaseSender.asFlux()); + this.thisClosedSink = Sinks.empty(); + this.otherClosedSink = Sinks.empty(); ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(connection, new InitializingInterceptorRegistry(), true); @@ -115,7 +119,9 @@ void setUp() { 0, null, __ -> null, - requesterLeaseTracker); + requesterLeaseTracker, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); mockRSocketHandler = mock(RSocket.class); when(mockRSocketHandler.metadataPush(any())) @@ -182,7 +188,8 @@ protected void hookOnError(Throwable throwable) { 0, FRAME_LENGTH_MASK, Integer.MAX_VALUE, - __ -> null); + __ -> null, + otherClosedSink); } @Test diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java index 25d91b25b..d736ae190 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -44,6 +44,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; import reactor.test.util.RaceTestUtils; class RSocketRequesterSubscribersTest { @@ -60,11 +61,15 @@ class RSocketRequesterSubscribersTest { private LeaksTrackingByteBufAllocator allocator; private RSocket rSocketRequester; private TestDuplexConnection connection; + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; @BeforeEach void setUp() { allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); connection = new TestDuplexConnection(allocator); + this.thisClosedSink = Sinks.empty(); + this.otherClosedSink = Sinks.empty(); rSocketRequester = new RSocketRequester( connection, @@ -77,7 +82,9 @@ void setUp() { 0, null, __ -> null, - null); + null, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); } @ParameterizedTest diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index 183785d2f..5861a459a 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -1453,8 +1453,14 @@ public void testWorkaround959(String type) { } public static class ClientSocketRule extends AbstractSocketRule { + + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; + @Override protected RSocketRequester newRSocket() { + this.thisClosedSink = Sinks.empty(); + this.otherClosedSink = Sinks.empty(); return new RSocketRequester( connection, PayloadDecoder.ZERO_COPY, @@ -1466,7 +1472,9 @@ protected RSocketRequester newRSocket() { Integer.MAX_VALUE, null, (__) -> null, - null); + null, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); } public int getStreamIdForRequestType(FrameType expectedFrameType) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index c0f64469c..cbfc05ea3 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -1184,6 +1184,7 @@ public static class ServerSocketRule extends AbstractSocketRule onCloseSink; @Override protected void doInit() { @@ -1220,6 +1221,7 @@ public void setAcceptingSocket(RSocket acceptingSocket, int prefetch) { @Override protected RSocketResponder newRSocket() { + onCloseSink = Sinks.empty(); return new RSocketResponder( connection, acceptingSocket, @@ -1228,7 +1230,8 @@ protected RSocketResponder newRSocket() { 0, maxFrameLength, maxInboundPayloadSize, - __ -> requestInterceptor); + __ -> requestInterceptor, + onCloseSink); } private void sendRequest(int streamId, FrameType frameType) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index c9904d583..98cc94087 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -509,6 +509,8 @@ public static class SocketRule { private RSocket requestAcceptor; private LeaksTrackingByteBufAllocator allocator; + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; public LeaksTrackingByteBufAllocator alloc() { return allocator; @@ -519,6 +521,9 @@ public void init() { serverProcessor = Sinks.many().multicast().directBestEffort(); clientProcessor = Sinks.many().multicast().directBestEffort(); + this.thisClosedSink = Sinks.empty(); + this.otherClosedSink = Sinks.empty(); + LocalDuplexConnection serverConnection = new LocalDuplexConnection("server", allocator, clientProcessor, serverProcessor); LocalDuplexConnection clientConnection = @@ -566,7 +571,8 @@ public Flux requestChannel(Publisher payloads) { 0, FRAME_LENGTH_MASK, Integer.MAX_VALUE, - __ -> null); + __ -> null, + otherClosedSink); crs = new RSocketRequester( @@ -580,7 +586,9 @@ public Flux requestChannel(Publisher payloads) { 0, null, __ -> null, - null); + null, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); } public void setRequestAcceptor(RSocket requestAcceptor) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java index 44ff78a64..e85c5856e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -69,6 +69,8 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); TestDuplexConnection conn = new TestDuplexConnection(allocator); + Sinks.Empty onThisSideClosedSink = Sinks.empty(); + RSocketRequester rSocket = new RSocketRequester( conn, @@ -81,7 +83,9 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { 0, null, __ -> null, - null); + null, + onThisSideClosedSink, + onThisSideClosedSink.asMono()); String errorMsg = "error"; @@ -107,6 +111,7 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); TestDuplexConnection conn = new TestDuplexConnection(allocator); + Sinks.Empty onThisSideClosedSink = Sinks.empty(); RSocketRequester rSocket = new RSocketRequester( conn, @@ -119,7 +124,9 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { 0, null, __ -> null, - null); + null, + onThisSideClosedSink, + onThisSideClosedSink.asMono()); conn.addToReceivedBuffer( ErrorFrameCodec.encode(ByteBufAllocator.DEFAULT, 0, new RejectedSetupException("error"))); diff --git a/rsocket-examples/build.gradle b/rsocket-examples/build.gradle index 423acec79..a49570776 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -28,7 +28,6 @@ dependencies { implementation "io.micrometer:micrometer-core" implementation "io.micrometer:micrometer-tracing" implementation project(":rsocket-micrometer") - testImplementation 'org.awaitility:awaitility' runtimeOnly 'ch.qos.logback:logback-classic' @@ -37,6 +36,7 @@ dependencies { testImplementation 'org.mockito:mockito-core' testImplementation 'org.assertj:assertj-core' testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.awaitility:awaitility' testImplementation "io.micrometer:micrometer-test" testImplementation "io.micrometer:micrometer-tracing-integration-test" diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java index 113b7a2f8..1b3779e85 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java @@ -79,10 +79,10 @@ public Mono connect() { Sinks.One inSink = Sinks.one(); Sinks.One outSink = Sinks.one(); - UnboundedProcessor in = new UnboundedProcessor(() -> inSink.tryEmitValue(inSink)); - UnboundedProcessor out = new UnboundedProcessor(() -> outSink.tryEmitValue(outSink)); + UnboundedProcessor in = new UnboundedProcessor(inSink::tryEmitEmpty); + UnboundedProcessor out = new UnboundedProcessor(outSink::tryEmitEmpty); - Mono onClose = inSink.asMono().zipWith(outSink.asMono()).then(); + Mono onClose = inSink.asMono().and(outSink.asMono()); server.apply(new LocalDuplexConnection(name, allocator, out, in, onClose)).subscribe(); diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java index 08fd780dc..c1d0fd2a3 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java @@ -36,7 +36,7 @@ final class LocalDuplexConnection implements DuplexConnection { private final LocalSocketAddress address; private final ByteBufAllocator allocator; - private final Flux in; + private final UnboundedProcessor in; private final Mono onClose; @@ -54,7 +54,7 @@ final class LocalDuplexConnection implements DuplexConnection { LocalDuplexConnection( String name, ByteBufAllocator allocator, - Flux in, + UnboundedProcessor in, UnboundedProcessor out, Mono onClose) { this.address = new LocalSocketAddress(name); @@ -111,6 +111,11 @@ public SocketAddress remoteAddress() { return address; } + @Override + public String toString() { + return "LocalDuplexConnection{" + "address=" + address + "hash=" + hashCode() + '}'; + } + static class ByteBufReleaserOperator implements CoreSubscriber, Subscription, Fuseable.QueueSubscription { diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java index 0445f5c02..f5d36269c 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java @@ -31,7 +31,7 @@ /** An implementation of {@link DuplexConnection} that connects via TCP. */ public final class TcpDuplexConnection extends BaseDuplexConnection { - + private final String side; private final Connection connection; /** @@ -40,14 +40,19 @@ public final class TcpDuplexConnection extends BaseDuplexConnection { * @param connection the {@link Connection} for managing the server */ public TcpDuplexConnection(Connection connection) { + this("unknown", connection); + } + + /** + * Creates a new instance + * + * @param connection the {@link Connection} for managing the server + */ + public TcpDuplexConnection(String side, Connection connection) { this.connection = Objects.requireNonNull(connection, "connection must not be null"); + this.side = side; - connection - .outbound() - .send(sender.hide()) - .then() - .doFinally(__ -> connection.dispose()) - .subscribe(); + connection.outbound().send(sender).then().doFinally(__ -> connection.dispose()).subscribe(); } @Override @@ -85,4 +90,9 @@ public Flux receive() { public void sendFrame(int streamId, ByteBuf frame) { super.sendFrame(streamId, FrameLengthCodec.encode(alloc(), frame.readableBytes(), frame)); } + + @Override + public String toString() { + return "TcpDuplexConnection{" + "side='" + side + '\'' + ", connection=" + connection + '}'; + } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java index 9deef6030..8f1170c5b 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java @@ -36,7 +36,7 @@ * stitched back on for frames received. */ public final class WebsocketDuplexConnection extends BaseDuplexConnection { - + private final String side; private final Connection connection; /** @@ -45,11 +45,21 @@ public final class WebsocketDuplexConnection extends BaseDuplexConnection { * @param connection the {@link Connection} to for managing the server */ public WebsocketDuplexConnection(Connection connection) { + this("unknown", connection); + } + + /** + * Creates a new instance + * + * @param connection the {@link Connection} to for managing the server + */ + public WebsocketDuplexConnection(String side, Connection connection) { this.connection = Objects.requireNonNull(connection, "connection must not be null"); + this.side = side; connection .outbound() - .sendObject(sender.map(BinaryWebSocketFrame::new).hide()) + .sendObject(sender.map(BinaryWebSocketFrame::new)) .then() .doFinally(__ -> connection.dispose()) .subscribe(); @@ -85,4 +95,15 @@ public void sendErrorAndClose(RSocketErrorException e) { final ByteBuf errorFrame = ErrorFrameCodec.encode(alloc(), 0, e); sender.tryEmitFinal(errorFrame); } + + @Override + public String toString() { + return "WebsocketDuplexConnection{" + + "side='" + + side + + '\'' + + ", connection=" + + connection + + '}'; + } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java index f64c6063c..84214b98c 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java @@ -116,6 +116,6 @@ public Mono connect() { return client .doOnConnected(c -> c.addHandlerLast(new RSocketLengthCodec(maxFrameLength))) .connect() - .map(TcpDuplexConnection::new); + .map(connection -> new TcpDuplexConnection("client", connection)); } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/WebsocketClientTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/WebsocketClientTransport.java index fe66da50a..86be47893 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/WebsocketClientTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/WebsocketClientTransport.java @@ -172,6 +172,6 @@ public Mono connect() { .websocket(specBuilder.build()) .uri(path) .connect() - .map(WebsocketDuplexConnection::new); + .map(connection -> new WebsocketDuplexConnection("client", connection)); } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java index effc7bed5..32562c4a4 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java @@ -114,7 +114,7 @@ public Mono start(ConnectionAcceptor acceptor) { c -> { c.addHandlerLast(new RSocketLengthCodec(maxFrameLength)); acceptor - .apply(new TcpDuplexConnection(c)) + .apply(new TcpDuplexConnection("server", c)) .then(Mono.never()) .subscribe(c.disposeSubscriber()); }) diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java index 38344c472..db13720e7 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java @@ -80,6 +80,8 @@ public Mono start(ConnectionAcceptor acceptor) { public static BiFunction> newHandler( ConnectionAcceptor acceptor) { return (in, out) -> - acceptor.apply(new WebsocketDuplexConnection((Connection) in)).then(out.neverComplete()); + acceptor + .apply(new WebsocketDuplexConnection("server", (Connection) in)) + .then(out.neverComplete()); } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java index 81ac8dcb6..4fe736fad 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java @@ -117,7 +117,7 @@ public Mono start(ConnectionAcceptor acceptor) { return response.sendWebsocket( (in, out) -> acceptor - .apply(new WebsocketDuplexConnection((Connection) in)) + .apply(new WebsocketDuplexConnection("server", (Connection) in)) .then(out.neverComplete()), specBuilder.build()); }) From 47e4e3b561a7ea0c52b8df77a520ebe32eccb673 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 14 Apr 2023 12:22:19 +0300 Subject: [PATCH 168/183] update version Signed-off-by: Oleh Dokuka --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7f8f4ca23..b234fff5b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.1.4 +version=1.2.0 perfBaselineVersion=1.1.3 From 5547cb1843c361e0f2a36bb70f5c6ceb80c81ccb Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Fri, 21 Apr 2023 22:33:10 +0300 Subject: [PATCH 169/183] improves tests & ensures thereare no leaks of bufs at execution (#1090) --- .../io/rsocket/core/DefaultRSocketClient.java | 2 +- .../ClientServerInputMultiplexerTest.java | 24 +- .../core/DefaultRSocketClientTests.java | 56 ++ .../java/io/rsocket/core/KeepAliveTest.java | 796 +++++++++--------- .../io/rsocket/core/RSocketConnectorTest.java | 6 + .../io/rsocket/core/RSocketLeaseTest.java | 54 +- .../io/rsocket/core/RSocketReconnectTest.java | 63 +- .../core/RSocketRequesterSubscribersTest.java | 14 +- .../core/RSocketRequesterTerminationTest.java | 95 ++- .../io/rsocket/core/RSocketRequesterTest.java | 47 +- .../io/rsocket/core/RSocketResponderTest.java | 12 +- .../core/RSocketServerFragmentationTest.java | 29 +- .../io/rsocket/core/RSocketServerTest.java | 28 +- .../java/io/rsocket/core/RSocketTest.java | 6 + .../io/rsocket/core/SetupRejectionTest.java | 4 + .../io/rsocket/exceptions/ExceptionsTest.java | 218 +++-- .../LoadbalanceRSocketClientTest.java | 5 +- .../rsocket/loadbalance/LoadbalanceTest.java | 10 +- .../metadata/CompositeMetadataCodecTest.java | 103 +-- .../metadata/MimeTypeMetadataCodecTest.java | 26 +- .../plugins/RequestInterceptorTest.java | 46 +- .../resume/ClientRSocketSessionTest.java | 5 +- .../resume/ServerRSocketSessionTest.java | 310 +++---- .../test/util/TestClientTransport.java | 4 +- .../rsocket/integration/TestingStreaming.java | 2 - 25 files changed, 1180 insertions(+), 785 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java index 5119814cd..9cd89c0b1 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java @@ -450,8 +450,8 @@ public void accept(RSocket rSocket, Throwable t) { @Override public void request(long n) { - this.main.request(n); super.request(n); + this.main.request(n); } public void cancel() { diff --git a/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java b/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java index c9ecb6eb6..195df9434 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java @@ -56,12 +56,20 @@ public void clientSplits() { clientMultiplexer .asClientConnection() .receive() - .doOnNext(f -> clientFrames.incrementAndGet()) + .doOnNext( + f -> { + clientFrames.incrementAndGet(); + f.release(); + }) .subscribe(); clientMultiplexer .asServerConnection() .receive() - .doOnNext(f -> serverFrames.incrementAndGet()) + .doOnNext( + f -> { + serverFrames.incrementAndGet(); + f.release(); + }) .subscribe(); source.addToReceivedBuffer(errorFrame(1).retain()); @@ -101,12 +109,20 @@ public void serverSplits() { serverMultiplexer .asClientConnection() .receive() - .doOnNext(f -> clientFrames.incrementAndGet()) + .doOnNext( + f -> { + clientFrames.incrementAndGet(); + f.release(); + }) .subscribe(); serverMultiplexer .asServerConnection() .receive() - .doOnNext(f -> serverFrames.incrementAndGet()) + .doOnNext( + f -> { + serverFrames.incrementAndGet(); + f.release(); + }) .subscribe(); source.addToReceivedBuffer(errorFrame(1).retain()); diff --git a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java index fa208269b..a8a5f2e58 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -19,6 +19,7 @@ import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; +import io.rsocket.FrameAssert; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.RaceTestConstants; @@ -434,6 +435,8 @@ public void shouldBeAbleToResolveOriginalSource() { assertSubscriber1.assertTerminated().assertValueCount(1); Assertions.assertThat(assertSubscriber1.values()).isEqualTo(assertSubscriber.values()); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -457,6 +460,13 @@ public void shouldDisposeOriginalSource() { .assertErrorMessage("Disposed"); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -494,6 +504,13 @@ public Mono onClose() { onCloseSubscriber.assertTerminated().assertComplete(); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -515,6 +532,13 @@ public void shouldResolveOnStartSource() { assertSubscriber1.assertTerminated().assertComplete(); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -536,6 +560,13 @@ public void shouldNotStartIfAlreadyDisposed() { assertSubscriber1.assertTerminated().assertComplete(); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -553,6 +584,11 @@ public void shouldBeRestartedIfSourceWasClosed() { rule.socket.dispose(); + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + terminateSubscriber.assertNotTerminated(); Assertions.assertThat(rule.client.isDisposed()).isFalse(); @@ -576,6 +612,13 @@ public void shouldBeRestartedIfSourceWasClosed() { Assertions.assertThat(rule.client.connect()).isFalse(); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -603,6 +646,13 @@ public void shouldDisposeOriginalSourceIfRacing() { .assertTerminated() .assertError(CancellationException.class) .assertErrorMessage("Disposed"); + + ByteBuf buf; + while ((buf = rule.connection.pollFrame()) != null) { + FrameAssert.assertThat(buf).hasStreamIdZero().hasData("Disposed").hasNoLeaks(); + } + + rule.allocator.assertHasNoLeaks(); } } @@ -632,8 +682,14 @@ public void shouldStartOriginalSourceOnceIfRacing() { AssertSubscriber assertSubscriber1 = AssertSubscriber.create(); rule.client.onClose().subscribe(assertSubscriber1); + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); assertSubscriber1.assertTerminated().assertComplete(); + + rule.allocator.assertHasNoLeaks(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java index 5bd5f9999..5be59235c 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java @@ -1,376 +1,420 @@ -/// * -// * Copyright 2015-2019 the original author or authors. -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ -// -// package io.rsocket.core; -// -// import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; -// import static io.rsocket.keepalive.KeepAliveHandler.DefaultKeepAliveHandler; -// import static io.rsocket.keepalive.KeepAliveHandler.ResumableKeepAliveHandler; -// -// import io.netty.buffer.ByteBuf; -// import io.netty.buffer.ByteBufAllocator; -// import io.netty.buffer.Unpooled; -// import io.rsocket.RSocket; -// import io.rsocket.buffer.LeaksTrackingByteBufAllocator; -// import io.rsocket.exceptions.ConnectionErrorException; -// import io.rsocket.frame.FrameHeaderCodec; -// import io.rsocket.frame.FrameType; -// import io.rsocket.frame.KeepAliveFrameCodec; -// import io.rsocket.core.RequesterLeaseHandler; -// import io.rsocket.resume.InMemoryResumableFramesStore; -//// import io.rsocket.resume.ResumableDuplexConnection; -// import io.rsocket.test.util.TestDuplexConnection; -// import io.rsocket.util.DefaultPayload; -// import java.time.Duration; -// import org.assertj.core.api.Assertions; -// import org.junit.jupiter.api.AfterEach; -// import org.junit.jupiter.api.BeforeEach; -// import org.junit.jupiter.api.Test; -// import reactor.core.Disposable; -// import reactor.core.publisher.Flux; -// import reactor.core.publisher.Mono; -// import reactor.test.StepVerifier; -// import reactor.test.scheduler.VirtualTimeScheduler; -// -// public class KeepAliveTest { -// private static final int KEEP_ALIVE_INTERVAL = 100; -// private static final int KEEP_ALIVE_TIMEOUT = 1000; -// private static final int RESUMABLE_KEEP_ALIVE_TIMEOUT = 200; -// -// VirtualTimeScheduler virtualTimeScheduler; -// -// @BeforeEach -// public void setUp() { -// virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); -// } -// -// @AfterEach -// public void tearDown() { -// VirtualTimeScheduler.reset(); -// } -// -// static RSocketState requester(int tickPeriod, int timeout) { -// LeaksTrackingByteBufAllocator allocator = -// LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); -// TestDuplexConnection connection = new TestDuplexConnection(allocator); -// RSocketRequester rSocket = -// new RSocketRequester( -// connection, -// DefaultPayload::create, -// StreamIdSupplier.clientSupplier(), -// 0, -// FRAME_LENGTH_MASK, -// Integer.MAX_VALUE, -// tickPeriod, -// timeout, -// new DefaultKeepAliveHandler(connection), -// RequesterLeaseHandler.None); -// return new RSocketState(rSocket, allocator, connection); -// } -// -// static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { -// LeaksTrackingByteBufAllocator allocator = -// LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); -// TestDuplexConnection connection = new TestDuplexConnection(allocator); -//// ResumableDuplexConnection resumableConnection = -//// new ResumableDuplexConnection( -//// "test", -//// connection, -//// new InMemoryResumableFramesStore("test", 10_000), -//// Duration.ofSeconds(10), -//// false); -// -// RSocketRequester rSocket = -// new RSocketRequester( -// resumableConnection, -// DefaultPayload::create, -// StreamIdSupplier.clientSupplier(), -// 0, -// FRAME_LENGTH_MASK, -// Integer.MAX_VALUE, -// tickPeriod, -// timeout, -// new ResumableKeepAliveHandler(resumableConnection), -// RequesterLeaseHandler.None); -// return new ResumableRSocketState(rSocket, connection, resumableConnection, allocator); -// } -// -// @Test -// void rSocketNotDisposedOnPresentKeepAlives() { -// RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// -// TestDuplexConnection connection = requesterState.connection(); -// -// Disposable disposable = -// Flux.interval(Duration.ofMillis(KEEP_ALIVE_INTERVAL)) -// .subscribe( -// n -> -// connection.addToReceivedBuffer( -// KeepAliveFrameCodec.encode( -// requesterState.allocator, true, 0, Unpooled.EMPTY_BUFFER))); -// -// virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_TIMEOUT * 2)); -// -// RSocket rSocket = requesterState.rSocket(); -// -// Assertions.assertThat(rSocket.isDisposed()).isFalse(); -// -// disposable.dispose(); -// -// requesterState.connection.dispose(); -// requesterState.rSocket.dispose(); -// -// Assertions.assertThat(requesterState.connection.getSent()).allMatch(ByteBuf::release); -// -// requesterState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void noKeepAlivesSentAfterRSocketDispose() { -// RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// -// requesterState.rSocket().dispose(); -// -// Duration duration = Duration.ofMillis(500); -// -// StepVerifier.create(Flux.from(requesterState.connection().getSentAsPublisher()).take(duration)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) -// .expectComplete() -// .verify(Duration.ofSeconds(1)); -// -// requesterState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void rSocketDisposedOnMissingKeepAlives() { -// RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// -// RSocket rSocket = requesterState.rSocket(); -// -// virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_TIMEOUT * 2)); -// -// Assertions.assertThat(rSocket.isDisposed()).isTrue(); -// rSocket -// .onClose() -// .as(StepVerifier::create) -// .expectError(ConnectionErrorException.class) -// .verify(Duration.ofMillis(100)); -// -// Assertions.assertThat(requesterState.connection.getSent()).allMatch(ByteBuf::release); -// -// requesterState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void clientRequesterSendsKeepAlives() { -// RSocketState RSocketState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// TestDuplexConnection connection = RSocketState.connection(); -// -// StepVerifier.create(Flux.from(connection.getSentAsPublisher()).take(3)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) -// .expectNextMatches(this::keepAliveFrameWithRespondFlag) -// .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) -// .expectNextMatches(this::keepAliveFrameWithRespondFlag) -// .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) -// .expectNextMatches(this::keepAliveFrameWithRespondFlag) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// RSocketState.rSocket.dispose(); -// RSocketState.connection.dispose(); -// -// RSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void requesterRespondsToKeepAlives() { -// RSocketState rSocketState = requester(100_000, 100_000); -// TestDuplexConnection connection = rSocketState.connection(); -// Duration duration = Duration.ofMillis(100); -// Mono.delay(duration) -// .subscribe( -// l -> -// connection.addToReceivedBuffer( -// KeepAliveFrameCodec.encode( -// rSocketState.allocator, true, 0, Unpooled.EMPTY_BUFFER))); -// -// StepVerifier.create(Flux.from(connection.getSentAsPublisher()).take(1)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) -// .expectNextMatches(this::keepAliveFrameWithoutRespondFlag) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// rSocketState.rSocket.dispose(); -// rSocketState.connection.dispose(); -// -// rSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void resumableRequesterNoKeepAlivesAfterDisconnect() { -// ResumableRSocketState rSocketState = -// resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// TestDuplexConnection testConnection = rSocketState.connection(); -// ResumableDuplexConnection resumableDuplexConnection = -// rSocketState.resumableDuplexConnection(); -// -// resumableDuplexConnection.disconnect(); -// -// Duration duration = Duration.ofMillis(500); -// StepVerifier.create(Flux.from(testConnection.getSentAsPublisher()).take(duration)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// rSocketState.rSocket.dispose(); -// rSocketState.connection.dispose(); -// -// rSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void resumableRequesterKeepAlivesAfterReconnect() { -// ResumableRSocketState rSocketState = -// resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// ResumableDuplexConnection resumableDuplexConnection = -// rSocketState.resumableDuplexConnection(); -// resumableDuplexConnection.disconnect(); -// TestDuplexConnection newTestConnection = new TestDuplexConnection(rSocketState.alloc()); -// resumableDuplexConnection.reconnect(newTestConnection); -// resumableDuplexConnection.resume(0, 0, ignored -> Mono.empty()); -// -// StepVerifier.create(Flux.from(newTestConnection.getSentAsPublisher()).take(1)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) -// .expectNextMatches(frame -> keepAliveFrame(frame) && frame.release()) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// rSocketState.rSocket.dispose(); -// rSocketState.connection.dispose(); -// -// rSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void resumableRequesterNoKeepAlivesAfterDispose() { -// ResumableRSocketState rSocketState = -// resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// rSocketState.rSocket().dispose(); -// Duration duration = Duration.ofMillis(500); -// StepVerifier.create(Flux.from(rSocketState.connection().getSentAsPublisher()).take(duration)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// rSocketState.rSocket.dispose(); -// rSocketState.connection.dispose(); -// -// rSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void resumableRSocketsNotDisposedOnMissingKeepAlives() throws InterruptedException { -// ResumableRSocketState resumableRequesterState = -// resumableRequester(KEEP_ALIVE_INTERVAL, RESUMABLE_KEEP_ALIVE_TIMEOUT); -// RSocket rSocket = resumableRequesterState.rSocket(); -// TestDuplexConnection connection = resumableRequesterState.connection(); -// -// virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(500)); -// -// Assertions.assertThat(rSocket.isDisposed()).isFalse(); -// Assertions.assertThat(connection.isDisposed()).isTrue(); -// -// -// Assertions.assertThat(resumableRequesterState.connection.getSent()).allMatch(ByteBuf::release); -// -// resumableRequesterState.connection.dispose(); -// resumableRequesterState.rSocket.dispose(); -// -// resumableRequesterState.allocator.assertHasNoLeaks(); -// } -// -// private boolean keepAliveFrame(ByteBuf frame) { -// return FrameHeaderCodec.frameType(frame) == FrameType.KEEPALIVE; -// } -// -// private boolean keepAliveFrameWithRespondFlag(ByteBuf frame) { -// return keepAliveFrame(frame) && KeepAliveFrameCodec.respondFlag(frame) && frame.release(); -// } -// -// private boolean keepAliveFrameWithoutRespondFlag(ByteBuf frame) { -// return keepAliveFrame(frame) && !KeepAliveFrameCodec.respondFlag(frame) && frame.release(); -// } -// -// static class RSocketState { -// private final RSocket rSocket; -// private final TestDuplexConnection connection; -// private final LeaksTrackingByteBufAllocator allocator; -// -// public RSocketState( -// RSocket rSocket, LeaksTrackingByteBufAllocator allocator, TestDuplexConnection connection) -// { -// this.rSocket = rSocket; -// this.connection = connection; -// this.allocator = allocator; -// } -// -// public TestDuplexConnection connection() { -// return connection; -// } -// -// public RSocket rSocket() { -// return rSocket; -// } -// -// public LeaksTrackingByteBufAllocator alloc() { -// return allocator; -// } -// } -// -// static class ResumableRSocketState { -// private final RSocket rSocket; -// private final TestDuplexConnection connection; -// private final ResumableDuplexConnection resumableDuplexConnection; -// private final LeaksTrackingByteBufAllocator allocator; -// -// public ResumableRSocketState( -// RSocket rSocket, -// TestDuplexConnection connection, -// ResumableDuplexConnection resumableDuplexConnection, -// LeaksTrackingByteBufAllocator allocator) { -// this.rSocket = rSocket; -// this.connection = connection; -// this.resumableDuplexConnection = resumableDuplexConnection; -// this.allocator = allocator; -// } -// -// public TestDuplexConnection connection() { -// return connection; -// } -// -// public ResumableDuplexConnection resumableDuplexConnection() { -// return resumableDuplexConnection; -// } -// -// public RSocket rSocket() { -// return rSocket; -// } -// -// public LeaksTrackingByteBufAllocator alloc() { -// return allocator; -// } -// } -// } +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; +import static io.rsocket.keepalive.KeepAliveHandler.DefaultKeepAliveHandler; +import static io.rsocket.keepalive.KeepAliveHandler.ResumableKeepAliveHandler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.FrameAssert; +import io.rsocket.RSocket; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.exceptions.ConnectionErrorException; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.KeepAliveFrameCodec; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.resume.InMemoryResumableFramesStore; +import io.rsocket.resume.RSocketSession; +import io.rsocket.resume.ResumableDuplexConnection; +import io.rsocket.resume.ResumeStateHolder; +import io.rsocket.test.util.TestDuplexConnection; +import java.time.Duration; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; +import reactor.test.StepVerifier; +import reactor.test.scheduler.VirtualTimeScheduler; + +public class KeepAliveTest { + private static final int KEEP_ALIVE_INTERVAL = 100; + private static final int KEEP_ALIVE_TIMEOUT = 1000; + private static final int RESUMABLE_KEEP_ALIVE_TIMEOUT = 200; + + VirtualTimeScheduler virtualTimeScheduler; + + @BeforeEach + public void setUp() { + virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); + } + + @AfterEach + public void tearDown() { + VirtualTimeScheduler.reset(); + } + + static RSocketState requester(int tickPeriod, int timeout) { + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + TestDuplexConnection connection = new TestDuplexConnection(allocator); + Sinks.Empty empty = Sinks.empty(); + RSocketRequester rSocket = + new RSocketRequester( + connection, + PayloadDecoder.ZERO_COPY, + StreamIdSupplier.clientSupplier(), + 0, + FRAME_LENGTH_MASK, + Integer.MAX_VALUE, + tickPeriod, + timeout, + new DefaultKeepAliveHandler(), + r -> null, + null, + empty, + empty.asMono()); + return new RSocketState(rSocket, allocator, connection, empty); + } + + static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + TestDuplexConnection connection = new TestDuplexConnection(allocator); + ResumableDuplexConnection resumableConnection = + new ResumableDuplexConnection( + "test", + Unpooled.EMPTY_BUFFER, + connection, + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 10_000)); + Sinks.Empty onClose = Sinks.empty(); + + RSocketRequester rSocket = + new RSocketRequester( + resumableConnection, + PayloadDecoder.ZERO_COPY, + StreamIdSupplier.clientSupplier(), + 0, + FRAME_LENGTH_MASK, + Integer.MAX_VALUE, + tickPeriod, + timeout, + new ResumableKeepAliveHandler( + resumableConnection, + Mockito.mock(RSocketSession.class), + Mockito.mock(ResumeStateHolder.class)), + __ -> null, + null, + onClose, + onClose.asMono()); + return new ResumableRSocketState(rSocket, connection, resumableConnection, onClose, allocator); + } + + @Test + void rSocketNotDisposedOnPresentKeepAlives() { + RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + + TestDuplexConnection connection = requesterState.connection(); + + Disposable disposable = + Flux.interval(Duration.ofMillis(KEEP_ALIVE_INTERVAL)) + .subscribe( + n -> + connection.addToReceivedBuffer( + KeepAliveFrameCodec.encode( + requesterState.allocator, true, 0, Unpooled.EMPTY_BUFFER))); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_TIMEOUT * 2)); + + RSocket rSocket = requesterState.rSocket(); + + Assertions.assertThat(rSocket.isDisposed()).isFalse(); + + disposable.dispose(); + + requesterState.connection.dispose(); + requesterState.rSocket.dispose(); + + Assertions.assertThat(requesterState.connection.getSent()).allMatch(ByteBuf::release); + + requesterState.allocator.assertHasNoLeaks(); + } + + @Test + void noKeepAlivesSentAfterRSocketDispose() { + RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + + requesterState.rSocket().dispose(); + + Duration duration = Duration.ofMillis(500); + + virtualTimeScheduler.advanceTimeBy(duration); + + FrameAssert.assertThat(requesterState.connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasData("Disposed") + .hasNoLeaks(); + FrameAssert.assertThat(requesterState.connection.pollFrame()).isNull(); + requesterState.allocator.assertHasNoLeaks(); + } + + @Test + void rSocketDisposedOnMissingKeepAlives() { + RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + + RSocket rSocket = requesterState.rSocket(); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_TIMEOUT * 2)); + + Assertions.assertThat(rSocket.isDisposed()).isTrue(); + rSocket + .onClose() + .as(StepVerifier::create) + .expectError(ConnectionErrorException.class) + .verify(Duration.ofMillis(100)); + + Assertions.assertThat(requesterState.connection.getSent()).allMatch(ByteBuf::release); + + requesterState.allocator.assertHasNoLeaks(); + } + + @Test + void clientRequesterSendsKeepAlives() { + RSocketState RSocketState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + TestDuplexConnection connection = RSocketState.connection(); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL)); + this.keepAliveFrameWithRespondFlag(connection.pollFrame()); + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL)); + this.keepAliveFrameWithRespondFlag(connection.pollFrame()); + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL)); + this.keepAliveFrameWithRespondFlag(connection.pollFrame()); + + RSocketState.rSocket.dispose(); + FrameAssert.assertThat(connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasData("Disposed") + .hasNoLeaks(); + RSocketState.connection.dispose(); + + RSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void requesterRespondsToKeepAlives() { + RSocketState rSocketState = requester(100_000, 100_000); + TestDuplexConnection connection = rSocketState.connection(); + Duration duration = Duration.ofMillis(100); + Mono.delay(duration) + .subscribe( + l -> + connection.addToReceivedBuffer( + KeepAliveFrameCodec.encode( + rSocketState.allocator, true, 0, Unpooled.EMPTY_BUFFER))); + + virtualTimeScheduler.advanceTimeBy(duration); + FrameAssert.assertThat(connection.awaitFrame()) + .typeOf(FrameType.KEEPALIVE) + .matches(this::keepAliveFrameWithoutRespondFlag); + + rSocketState.rSocket.dispose(); + FrameAssert.assertThat(rSocketState.connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + rSocketState.connection.dispose(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRequesterNoKeepAlivesAfterDisconnect() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + TestDuplexConnection testConnection = rSocketState.connection(); + ResumableDuplexConnection resumableDuplexConnection = rSocketState.resumableDuplexConnection(); + + resumableDuplexConnection.disconnect(); + + Duration duration = Duration.ofMillis(KEEP_ALIVE_INTERVAL * 5); + virtualTimeScheduler.advanceTimeBy(duration); + Assertions.assertThat(testConnection.pollFrame()).isNull(); + + rSocketState.rSocket.dispose(); + rSocketState.connection.dispose(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRequesterKeepAlivesAfterReconnect() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + ResumableDuplexConnection resumableDuplexConnection = rSocketState.resumableDuplexConnection(); + resumableDuplexConnection.disconnect(); + TestDuplexConnection newTestConnection = new TestDuplexConnection(rSocketState.alloc()); + resumableDuplexConnection.connect(newTestConnection); + // resumableDuplexConnection.(0, 0, ignored -> Mono.empty()); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL)); + + FrameAssert.assertThat(newTestConnection.awaitFrame()) + .typeOf(FrameType.KEEPALIVE) + .hasStreamIdZero() + .hasNoLeaks(); + + rSocketState.rSocket.dispose(); + FrameAssert.assertThat(newTestConnection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + FrameAssert.assertThat(newTestConnection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Connection Closed Unexpectedly") // API limitations + .hasNoLeaks(); + newTestConnection.dispose(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRequesterNoKeepAlivesAfterDispose() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + rSocketState.rSocket().dispose(); + Duration duration = Duration.ofMillis(500); + StepVerifier.create(Flux.from(rSocketState.connection().getSentAsPublisher()).take(duration)) + .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + rSocketState.rSocket.dispose(); + FrameAssert.assertThat(rSocketState.connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + rSocketState.connection.dispose(); + FrameAssert.assertThat(rSocketState.connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Connection Closed Unexpectedly") + .hasNoLeaks(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRSocketsNotDisposedOnMissingKeepAlives() throws InterruptedException { + ResumableRSocketState resumableRequesterState = + resumableRequester(KEEP_ALIVE_INTERVAL, RESUMABLE_KEEP_ALIVE_TIMEOUT); + RSocket rSocket = resumableRequesterState.rSocket(); + TestDuplexConnection connection = resumableRequesterState.connection(); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(500)); + + Assertions.assertThat(rSocket.isDisposed()).isFalse(); + Assertions.assertThat(connection.isDisposed()).isTrue(); + + Assertions.assertThat(resumableRequesterState.connection.getSent()).allMatch(ByteBuf::release); + + resumableRequesterState.connection.dispose(); + resumableRequesterState.rSocket.dispose(); + + resumableRequesterState.allocator.assertHasNoLeaks(); + } + + private boolean keepAliveFrame(ByteBuf frame) { + return FrameHeaderCodec.frameType(frame) == FrameType.KEEPALIVE; + } + + private boolean keepAliveFrameWithRespondFlag(ByteBuf frame) { + return keepAliveFrame(frame) && KeepAliveFrameCodec.respondFlag(frame) && frame.release(); + } + + private boolean keepAliveFrameWithoutRespondFlag(ByteBuf frame) { + return keepAliveFrame(frame) && !KeepAliveFrameCodec.respondFlag(frame) && frame.release(); + } + + static class RSocketState { + private final RSocket rSocket; + private final TestDuplexConnection connection; + private final LeaksTrackingByteBufAllocator allocator; + private final Sinks.Empty onClose; + + public RSocketState( + RSocket rSocket, + LeaksTrackingByteBufAllocator allocator, + TestDuplexConnection connection, + Sinks.Empty onClose) { + this.rSocket = rSocket; + this.connection = connection; + this.allocator = allocator; + this.onClose = onClose; + } + + public TestDuplexConnection connection() { + return connection; + } + + public RSocket rSocket() { + return rSocket; + } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } + } + + static class ResumableRSocketState { + private final RSocket rSocket; + private final TestDuplexConnection connection; + private final ResumableDuplexConnection resumableDuplexConnection; + private final LeaksTrackingByteBufAllocator allocator; + private final Sinks.Empty onClose; + + public ResumableRSocketState( + RSocket rSocket, + TestDuplexConnection connection, + ResumableDuplexConnection resumableDuplexConnection, + Sinks.Empty onClose, + LeaksTrackingByteBufAllocator allocator) { + this.rSocket = rSocket; + this.connection = connection; + this.resumableDuplexConnection = resumableDuplexConnection; + this.onClose = onClose; + this.allocator = allocator; + } + + public TestDuplexConnection connection() { + return connection; + } + + public ResumableDuplexConnection resumableDuplexConnection() { + return resumableDuplexConnection; + } + + public RSocket rSocket() { + return rSocket; + } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java index 40487bec1..7cf12a81e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java @@ -100,6 +100,8 @@ public void unexpectedFramesBeforeResumeOKFrame(String frameType) { .hasData("RESUME_OK frame must be received before any others") .hasStreamIdZero() .hasNoLeaks(); + + transport.alloc().assertHasNoLeaks(); } @Test @@ -204,6 +206,8 @@ public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions return byteBuf.release(); }); assertThat(setupPayload.refCnt()).isZero(); + + testClientTransport.alloc().assertHasNoLeaks(); } @Test @@ -263,6 +267,8 @@ public void ensuresThatSetupPayloadProvidedAsMonoIsReleased() { payload.refCnt() == 1 && payload.data().refCnt() == 0 && payload.metadata().refCnt() == 0); + + testClientTransport.alloc().assertHasNoLeaks(); } @Test diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java index aad0aaaca..a461833d3 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -64,6 +64,7 @@ import java.util.function.BiFunction; import java.util.stream.Stream; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -192,6 +193,11 @@ protected void hookOnError(Throwable throwable) { otherClosedSink); } + @AfterEach + void tearDownAndCheckForLeaks() { + byteBufAllocator.assertHasNoLeaks(); + } + @Test public void serverRSocketFactoryRejectsUnsupportedLease() { Payload payload = DefaultPayload.create(DefaultPayload.EMPTY_BUFFER); @@ -217,18 +223,26 @@ public void serverRSocketFactoryRejectsUnsupportedLease() { Assertions.assertThat(FrameHeaderCodec.frameType(error)).isEqualTo(ERROR); Assertions.assertThat(Exceptions.from(0, error).getMessage()) .isEqualTo("lease is not supported"); + error.release(); + connection.dispose(); + transport.alloc().assertHasNoLeaks(); } @Test public void clientRSocketFactorySetsLeaseFlag() { TestClientTransport clientTransport = new TestClientTransport(); - RSocketConnector.create().lease().connect(clientTransport).block(); - - Collection sent = clientTransport.testConnection().getSent(); - Assertions.assertThat(sent).hasSize(1); - ByteBuf setup = sent.iterator().next(); - Assertions.assertThat(FrameHeaderCodec.frameType(setup)).isEqualTo(SETUP); - Assertions.assertThat(SetupFrameCodec.honorLease(setup)).isTrue(); + try { + RSocketConnector.create().lease().connect(clientTransport).block(); + Collection sent = clientTransport.testConnection().getSent(); + Assertions.assertThat(sent).hasSize(1); + ByteBuf setup = sent.iterator().next(); + Assertions.assertThat(FrameHeaderCodec.frameType(setup)).isEqualTo(SETUP); + Assertions.assertThat(SetupFrameCodec.honorLease(setup)).isTrue(); + setup.release(); + } finally { + clientTransport.testConnection().dispose(); + clientTransport.alloc().assertHasNoLeaks(); + } } @ParameterizedTest @@ -382,10 +396,16 @@ void requesterExpiredLeaseRequestsAreRejected( @Test void requesterAvailabilityRespectsTransport() { - requesterLeaseTracker.handleLeaseFrame(leaseFrame(5_000, 1, Unpooled.EMPTY_BUFFER)); - double unavailable = 0.0; - connection.setAvailability(unavailable); - Assertions.assertThat(rSocketRequester.availability()).isCloseTo(unavailable, offset(1e-2)); + ByteBuf frame = leaseFrame(5_000, 1, Unpooled.EMPTY_BUFFER); + try { + + requesterLeaseTracker.handleLeaseFrame(frame); + double unavailable = 0.0; + connection.setAvailability(unavailable); + Assertions.assertThat(rSocketRequester.availability()).isCloseTo(unavailable, offset(1e-2)); + } finally { + frame.release(); + } } @ParameterizedTest @@ -637,10 +657,14 @@ void sendLease() { .findFirst() .orElseThrow(() -> new IllegalStateException("Lease frame not sent")); - Assertions.assertThat(LeaseFrameCodec.ttl(leaseFrame)).isEqualTo(ttl); - Assertions.assertThat(LeaseFrameCodec.numRequests(leaseFrame)).isEqualTo(numberOfRequests); - Assertions.assertThat(LeaseFrameCodec.metadata(leaseFrame).toString(utf8)) - .isEqualTo(metadataContent); + try { + Assertions.assertThat(LeaseFrameCodec.ttl(leaseFrame)).isEqualTo(ttl); + Assertions.assertThat(LeaseFrameCodec.numRequests(leaseFrame)).isEqualTo(numberOfRequests); + Assertions.assertThat(LeaseFrameCodec.metadata(leaseFrame).toString(utf8)) + .isEqualTo(metadataContent); + } finally { + leaseFrame.release(); + } } // @Test diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java index 8c662d67d..966fd65f2 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java @@ -17,8 +17,11 @@ import static org.assertj.core.api.Assertions.assertThat; +import io.rsocket.FrameAssert; import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; import io.rsocket.test.util.TestClientTransport; +import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.transport.ClientTransport; import java.io.UncheckedIOException; import java.time.Duration; @@ -49,27 +52,44 @@ public void shouldBeASharedReconnectableInstanceOfRSocketMono() throws Interrupt RSocket rSocket1 = rSocketMono.block(); RSocket rSocket2 = rSocketMono.block(); + FrameAssert.assertThat(testClientTransport[0].testConnection().awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + assertThat(rSocket1).isEqualTo(rSocket2); testClientTransport[0].testConnection().dispose(); + rSocket1.onClose().block(Duration.ofSeconds(1)); + testClientTransport[0].alloc().assertHasNoLeaks(); testClientTransport[0] = new TestClientTransport(); RSocket rSocket3 = rSocketMono.block(); RSocket rSocket4 = rSocketMono.block(); + FrameAssert.assertThat(testClientTransport[0].testConnection().awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + assertThat(rSocket3).isEqualTo(rSocket4).isNotEqualTo(rSocket2); + + testClientTransport[0].testConnection().dispose(); + rSocket3.onClose().block(Duration.ofSeconds(1)); + testClientTransport[0].alloc().assertHasNoLeaks(); } @Test - @SuppressWarnings({"rawtype", "unchecked"}) + @SuppressWarnings({"rawtype"}) public void shouldBeRetrieableConnectionSharedReconnectableInstanceOfRSocketMono() { ClientTransport transport = Mockito.mock(ClientTransport.class); + TestClientTransport transport1 = new TestClientTransport(); Mockito.when(transport.connect()) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) - .thenReturn(new TestClientTransport().connect()); + .thenReturn(transport1.connect()); Mono rSocketMono = RSocketConnector.create() .reconnect( @@ -87,19 +107,29 @@ public void shouldBeRetrieableConnectionSharedReconnectableInstanceOfRSocketMono UncheckedIOException.class, UncheckedIOException.class, UncheckedIOException.class); + + FrameAssert.assertThat(transport1.testConnection().awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + + transport1.testConnection().dispose(); + rSocket1.onClose().block(Duration.ofSeconds(1)); + transport1.alloc().assertHasNoLeaks(); } @Test - @SuppressWarnings({"rawtype", "unchecked"}) + @SuppressWarnings({"rawtype"}) public void shouldBeExaustedRetrieableConnectionSharedReconnectableInstanceOfRSocketMono() { ClientTransport transport = Mockito.mock(ClientTransport.class); + TestClientTransport transport1 = new TestClientTransport(); Mockito.when(transport.connect()) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) - .thenReturn(new TestClientTransport().connect()); + .thenReturn(transport1.connect()); Mono rSocketMono = RSocketConnector.create() .reconnect( @@ -121,17 +151,38 @@ public void shouldBeExaustedRetrieableConnectionSharedReconnectableInstanceOfRSo UncheckedIOException.class, UncheckedIOException.class, UncheckedIOException.class); + + transport1.alloc().assertHasNoLeaks(); } @Test public void shouldBeNotBeASharedReconnectableInstanceOfRSocketMono() { - - Mono rSocketMono = RSocketConnector.connectWith(new TestClientTransport()); + TestClientTransport transport = new TestClientTransport(); + Mono rSocketMono = RSocketConnector.connectWith(transport); RSocket rSocket1 = rSocketMono.block(); + TestDuplexConnection connection1 = transport.testConnection(); + + FrameAssert.assertThat(connection1.awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + RSocket rSocket2 = rSocketMono.block(); + TestDuplexConnection connection2 = transport.testConnection(); assertThat(rSocket1).isNotEqualTo(rSocket2); + + FrameAssert.assertThat(connection2.awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + + connection1.dispose(); + connection2.dispose(); + rSocket1.onClose().block(Duration.ofSeconds(1)); + rSocket2.onClose().block(Duration.ofSeconds(1)); + transport.alloc().assertHasNoLeaks(); } @SafeVarargs diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java index d736ae190..01eb998c7 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -21,6 +21,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.CharsetUtil; +import io.rsocket.FrameAssert; import io.rsocket.RSocket; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.FrameHeaderCodec; @@ -37,6 +38,7 @@ import java.util.function.Function; import java.util.stream.Stream; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -64,6 +66,11 @@ class RSocketRequesterSubscribersTest { protected Sinks.Empty thisClosedSink; protected Sinks.Empty otherClosedSink; + @AfterEach + void tearDownAndCheckNoLeaks() { + allocator.assertHasNoLeaks(); + } + @BeforeEach void setUp() { allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); @@ -89,6 +96,7 @@ void setUp() { @ParameterizedTest @MethodSource("allInteractions") + @SuppressWarnings({"rawtypes", "unchecked"}) void singleSubscriber(Function> interaction, FrameType requestType) { Flux response = Flux.from(interaction.apply(rSocketRequester)); @@ -105,7 +113,11 @@ void singleSubscriber(Function> interaction, FrameType req assertSubscriberA.assertTerminated(); assertSubscriberB.assertTerminated(); - Assertions.assertThat(requestFramesCount(connection.getSent())).isEqualTo(1); + FrameAssert.assertThat(connection.pollFrame()).typeOf(requestType).hasNoLeaks(); + + if (requestType == FrameType.REQUEST_CHANNEL) { + FrameAssert.assertThat(connection.pollFrame()).typeOf(FrameType.COMPLETE).hasNoLeaks(); + } } @ParameterizedTest diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java index 6ccff3701..5cfa76a1c 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java @@ -1,15 +1,19 @@ package io.rsocket.core; +import io.rsocket.FrameAssert; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.core.RSocketRequesterTest.ClientSocketRule; +import io.rsocket.frame.FrameType; import io.rsocket.util.EmptyPayload; import java.nio.channels.ClosedChannelException; import java.time.Duration; import java.util.Arrays; import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -25,15 +29,23 @@ public void setup() { rule.init(); } + @AfterEach + public void tearDownAndCheckNoLeaks() { + rule.assertHasNoLeaks(); + } + @ParameterizedTest @MethodSource("rsocketInteractions") public void testCurrentStreamIsTerminatedOnConnectionClose( - Function> interaction) { + FrameType requestType, Function> interaction) { RSocketRequester rSocket = rule.socket; - Mono.delay(Duration.ofSeconds(1)).doOnNext(v -> rule.connection.dispose()).subscribe(); - StepVerifier.create(interaction.apply(rSocket)) + .then( + () -> { + FrameAssert.assertThat(rule.connection.pollFrame()).typeOf(requestType).hasNoLeaks(); + }) + .then(() -> rule.connection.dispose()) .expectError(ClosedChannelException.class) .verify(Duration.ofSeconds(5)); } @@ -41,7 +53,7 @@ public void testCurrentStreamIsTerminatedOnConnectionClose( @ParameterizedTest @MethodSource("rsocketInteractions") public void testSubsequentStreamIsTerminatedAfterConnectionClose( - Function> interaction) { + FrameType requestType, Function> interaction) { RSocketRequester rSocket = rule.socket; rule.connection.dispose(); @@ -50,46 +62,51 @@ public void testSubsequentStreamIsTerminatedAfterConnectionClose( .verify(Duration.ofSeconds(5)); } - public static Iterable>> rsocketInteractions() { + public static Iterable rsocketInteractions() { EmptyPayload payload = EmptyPayload.INSTANCE; - Publisher payloadStream = Flux.just(payload); - Function> resp = - new Function>() { - @Override - public Mono apply(RSocket rSocket) { - return rSocket.requestResponse(payload); - } + Arguments resp = + Arguments.of( + FrameType.REQUEST_RESPONSE, + new Function>() { + @Override + public Mono apply(RSocket rSocket) { + return rSocket.requestResponse(payload); + } - @Override - public String toString() { - return "Request Response"; - } - }; - Function> stream = - new Function>() { - @Override - public Flux apply(RSocket rSocket) { - return rSocket.requestStream(payload); - } + @Override + public String toString() { + return "Request Response"; + } + }); + Arguments stream = + Arguments.of( + FrameType.REQUEST_STREAM, + new Function>() { + @Override + public Flux apply(RSocket rSocket) { + return rSocket.requestStream(payload); + } - @Override - public String toString() { - return "Request Stream"; - } - }; - Function> channel = - new Function>() { - @Override - public Flux apply(RSocket rSocket) { - return rSocket.requestChannel(payloadStream); - } + @Override + public String toString() { + return "Request Stream"; + } + }); + Arguments channel = + Arguments.of( + FrameType.REQUEST_CHANNEL, + new Function>() { + @Override + public Flux apply(RSocket rSocket) { + return rSocket.requestChannel(Flux.never().startWith(payload)); + } - @Override - public String toString() { - return "Request Channel"; - } - }; + @Override + public String toString() { + return "Request Channel"; + } + }); return Arrays.asList(resp, stream, channel); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index 5861a459a..a1199f698 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -122,6 +122,7 @@ public void setUp() throws Throwable { public void tearDown() { Hooks.resetOnErrorDropped(); Hooks.resetOnNextDropped(); + rule.assertHasNoLeaks(); } @Test @@ -765,16 +766,21 @@ private static Stream racingCases() { @Test public void simpleOnDiscardRequestChannelTest() { AssertSubscriber assertSubscriber = AssertSubscriber.create(1); - TestPublisher testPublisher = TestPublisher.create(); + Sinks.Many testPublisher = Sinks.many().unicast().onBackpressureBuffer(); - Flux payloadFlux = rule.socket.requestChannel(testPublisher); + Flux payloadFlux = rule.socket.requestChannel(testPublisher.asFlux()); payloadFlux.subscribe(assertSubscriber); - testPublisher.next( - ByteBufPayload.create("d", "m"), - ByteBufPayload.create("d1", "m1"), - ByteBufPayload.create("d2", "m2")); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d"), ByteBufUtil.writeUtf8(rule.alloc(), "m"))); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d1"), ByteBufUtil.writeUtf8(rule.alloc(), "m1"))); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d2"), ByteBufUtil.writeUtf8(rule.alloc(), "m2"))); assertSubscriber.cancel(); @@ -787,16 +793,23 @@ public void simpleOnDiscardRequestChannelTest() { public void simpleOnDiscardRequestChannelTest2() { ByteBufAllocator allocator = rule.alloc(); AssertSubscriber assertSubscriber = AssertSubscriber.create(1); - TestPublisher testPublisher = TestPublisher.create(); + Sinks.Many testPublisher = Sinks.many().unicast().onBackpressureBuffer(); - Flux payloadFlux = rule.socket.requestChannel(testPublisher); + Flux payloadFlux = rule.socket.requestChannel(testPublisher.asFlux()); payloadFlux.subscribe(assertSubscriber); - testPublisher.next(ByteBufPayload.create("d", "m")); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d"), ByteBufUtil.writeUtf8(rule.alloc(), "m"))); int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); - testPublisher.next(ByteBufPayload.create("d1", "m1"), ByteBufPayload.create("d2", "m2")); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d1"), ByteBufUtil.writeUtf8(rule.alloc(), "m1"))); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d2"), ByteBufUtil.writeUtf8(rule.alloc(), "m2"))); rule.connection.addToReceivedBuffer( ErrorFrameCodec.encode( @@ -820,7 +833,7 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( switch (frameType) { case REQUEST_FNF: response = - testPublisher.mono().flatMap(p -> rule.socket.fireAndForget(p).then(Mono.empty())); + testPublisher.mono().flatMap(p -> rule.socket.fireAndForget(p)).then(Mono.empty()); break; case REQUEST_RESPONSE: response = testPublisher.mono().flatMap(p -> rule.socket.requestResponse(p)); @@ -836,7 +849,7 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( } response.subscribe(assertSubscriber); - testPublisher.next(ByteBufPayload.create("d")); + testPublisher.next(ByteBufPayload.create(ByteBufUtil.writeUtf8(rule.alloc(), "d"))); int streamId = rule.getStreamIdForRequestType(frameType); @@ -870,7 +883,7 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( } for (int i = 1; i < framesCnt; i++) { - testPublisher.next(ByteBufPayload.create("d" + i)); + testPublisher.next(ByteBufPayload.create(ByteBufUtil.writeUtf8(rule.alloc(), "d" + i))); } assertThat(rule.connection.getSent()) @@ -908,7 +921,10 @@ static Stream encodeDecodePayloadCases() { @MethodSource("refCntCases") public void ensureSendsErrorOnIllegalRefCntPayload( BiFunction> sourceProducer) { - Payload invalidPayload = ByteBufPayload.create("test", "test"); + Payload invalidPayload = + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "test"), + ByteBufUtil.writeUtf8(rule.alloc(), "test")); invalidPayload.release(); Publisher source = sourceProducer.apply(invalidPayload, rule); @@ -926,7 +942,8 @@ private static Stream>> refCn (p, clientSocketRule) -> clientSocketRule.socket.requestChannel(Mono.just(p)), (p, clientSocketRule) -> { Flux.from(clientSocketRule.connection.getSentAsPublisher()) - .filter(bb -> FrameHeaderCodec.frameType(bb) == REQUEST_CHANNEL) + .filter(bb -> frameType(bb) == REQUEST_CHANNEL) + .doOnDiscard(ByteBuf.class, ReferenceCounted::release) .subscribe( bb -> { clientSocketRule.connection.addToReceivedBuffer( diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index cbfc05ea3..4f689e396 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -113,6 +113,7 @@ public void setUp() { public void tearDown() { Hooks.resetOnErrorDropped(); Hooks.resetOnNextDropped(); + rule.assertHasNoLeaks(); } @Test @@ -146,9 +147,7 @@ public Mono requestResponse(Payload payload) { }); rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE); testPublisher.complete(); - assertThat(frameType(rule.connection.awaitFrame())) - .describedAs("Unexpected frame sent.") - .isIn(FrameType.COMPLETE, FrameType.NEXT_COMPLETE); + FrameAssert.assertThat(rule.connection.awaitFrame()).typeOf(FrameType.COMPLETE).hasNoLeaks(); testPublisher.assertWasNotCancelled(); } @@ -158,9 +157,10 @@ public void testHandlerEmitsError() { final int streamId = 4; rule.prefetch = 1; rule.sendRequest(streamId, FrameType.REQUEST_STREAM); - assertThat(frameType(rule.connection.awaitFrame())) - .describedAs("Unexpected frame sent.") - .isEqualTo(FrameType.ERROR); + FrameAssert.assertThat(rule.connection.awaitFrame()) + .typeOf(FrameType.ERROR) + .hasData("Request-Stream not implemented.") + .hasNoLeaks(); } @Test diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java index fd588cda3..90e881257 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java @@ -1,5 +1,8 @@ package io.rsocket.core; +import io.rsocket.Closeable; +import io.rsocket.FrameAssert; +import io.rsocket.frame.FrameType; import io.rsocket.test.util.TestClientTransport; import io.rsocket.test.util.TestServerTransport; import org.assertj.core.api.Assertions; @@ -16,12 +19,18 @@ public void serverErrorsWithEnabledFragmentationOnInsufficientMtu() { @Test public void serverSucceedsWithEnabledFragmentationOnSufficientMtu() { - RSocketServer.create().fragment(100).bind(new TestServerTransport()).block(); + TestServerTransport transport = new TestServerTransport(); + Closeable closeable = RSocketServer.create().fragment(100).bind(transport).block(); + closeable.dispose(); + transport.alloc().assertHasNoLeaks(); } @Test public void serverSucceedsWithDisabledFragmentation() { - RSocketServer.create().bind(new TestServerTransport()).block(); + TestServerTransport transport = new TestServerTransport(); + Closeable closeable = RSocketServer.create().bind(transport).block(); + closeable.dispose(); + transport.alloc().assertHasNoLeaks(); } @Test @@ -33,11 +42,23 @@ public void clientErrorsWithEnabledFragmentationOnInsufficientMtu() { @Test public void clientSucceedsWithEnabledFragmentationOnSufficientMtu() { - RSocketConnector.create().fragment(100).connect(new TestClientTransport()).block(); + TestClientTransport transport = new TestClientTransport(); + RSocketConnector.create().fragment(100).connect(transport).block(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .typeOf(FrameType.SETUP) + .hasNoLeaks(); + transport.testConnection().dispose(); + transport.alloc().assertHasNoLeaks(); } @Test public void clientSucceedsWithDisabledFragmentation() { - RSocketConnector.connectWith(new TestClientTransport()).block(); + TestClientTransport transport = new TestClientTransport(); + RSocketConnector.connectWith(transport).block(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .typeOf(FrameType.SETUP) + .hasNoLeaks(); + transport.testConnection().dispose(); + transport.alloc().assertHasNoLeaks(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java index 24bf95215..0b5ca38f7 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java @@ -20,6 +20,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.rsocket.Closeable; import io.rsocket.FrameAssert; import io.rsocket.RSocket; import io.rsocket.frame.FrameType; @@ -60,13 +61,14 @@ public void unexpectedFramesBeforeSetupFrame() { .hasData("SETUP or RESUME frame must be received before any others") .hasStreamIdZero() .hasNoLeaks(); + duplexConnection.alloc().assertHasNoLeaks(); } @Test public void timeoutOnNoFirstFrame() { final VirtualTimeScheduler scheduler = VirtualTimeScheduler.getOrSet(); + TestServerTransport transport = new TestServerTransport(); try { - TestServerTransport transport = new TestServerTransport(); RSocketServer.create().maxTimeToFirstFrame(Duration.ofMinutes(2)).bind(transport).block(); final TestDuplexConnection duplexConnection = transport.connect(); @@ -84,6 +86,7 @@ public void timeoutOnNoFirstFrame() { FrameAssert.assertThat(duplexConnection.pollFrame()).isNull(); } finally { + transport.alloc().assertHasNoLeaks(); VirtualTimeScheduler.reset(); } } @@ -128,14 +131,15 @@ public void unexpectedFramesBeforeSetup() { Sinks.Empty connectedSink = Sinks.empty(); TestServerTransport transport = new TestServerTransport(); - RSocketServer.create() - .acceptor( - (setup, sendingSocket) -> { - connectedSink.tryEmitEmpty(); - return Mono.just(new RSocket() {}); - }) - .bind(transport) - .block(); + Closeable server = + RSocketServer.create() + .acceptor( + (setup, sendingSocket) -> { + connectedSink.tryEmitEmpty(); + return Mono.just(new RSocket() {}); + }) + .bind(transport) + .block(); byte[] bytes = new byte[16_000_000]; new Random().nextBytes(bytes); @@ -153,5 +157,11 @@ public void unexpectedFramesBeforeSetup() { assertThat(connectedSink.scan(Scannable.Attr.TERMINATED)) .as("Connection should not succeed") .isFalse(); + FrameAssert.assertThat(connection.pollFrame()) + .hasStreamIdZero() + .hasData("SETUP or RESUME frame must be received before any others") + .hasNoLeaks(); + server.dispose(); + transport.alloc().assertHasNoLeaks(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index 98cc94087..e01e6ebdc 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -35,6 +35,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicReference; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -56,6 +57,11 @@ public void setup() { rule.init(); } + @AfterEach + public void tearDownAndCheckOnLeaks() { + rule.alloc().assertHasNoLeaks(); + } + @Test public void rsocketDisposalShouldEndupWithNoErrorsOnClose() { RSocket requestHandlingRSocket = diff --git a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java index e85c5856e..87c3a865f 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -58,10 +58,12 @@ void responderRejectSetup() { ByteBuf sentFrame = transport.awaitSent(); assertThat(FrameHeaderCodec.frameType(sentFrame)).isEqualTo(FrameType.ERROR); RuntimeException error = Exceptions.from(0, sentFrame); + sentFrame.release(); assertThat(errorMsg).isEqualTo(error.getMessage()); assertThat(error).isInstanceOf(RejectedSetupException.class); RSocket acceptorSender = acceptor.senderRSocket().block(); assertThat(acceptorSender.isDisposed()).isTrue(); + transport.allocator.assertHasNoLeaks(); } @Test @@ -104,6 +106,7 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { .verify(Duration.ofSeconds(5)); assertThat(rSocket.isDisposed()).isTrue(); + allocator.assertHasNoLeaks(); } @Test @@ -138,6 +141,7 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { .expectErrorMatches( err -> err instanceof RejectedSetupException && "error".equals(err.getMessage())) .verify(Duration.ofSeconds(5)); + allocator.assertHasNoLeaks(); } private static class RejectingAcceptor implements SocketAcceptor { diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java index ff7e4ff17..a316aed8b 100644 --- a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java @@ -43,14 +43,18 @@ final class ExceptionsTest { void fromApplicationException() { ByteBuf byteBuf = createErrorFrame(1, APPLICATION_ERROR, "test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(ApplicationErrorException.class) - .hasMessage("test-message"); - - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage( - "Invalid Error frame in Stream ID 0: 0x%08X '%s'", APPLICATION_ERROR, "test-message"); + try { + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(ApplicationErrorException.class) + .hasMessage("test-message"); + + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Invalid Error frame in Stream ID 0: 0x%08X '%s'", APPLICATION_ERROR, "test-message"); + } finally { + byteBuf.release(); + } } @DisplayName("from returns CanceledException") @@ -58,28 +62,37 @@ void fromApplicationException() { void fromCanceledException() { ByteBuf byteBuf = createErrorFrame(1, CANCELED, "test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(CanceledException.class) - .hasMessage("test-message"); + try { - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", CANCELED, "test-message"); + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(CanceledException.class) + .hasMessage("test-message"); + + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", CANCELED, "test-message"); + } finally { + byteBuf.release(); + } } @DisplayName("from returns ConnectionCloseException") @Test void fromConnectionCloseException() { ByteBuf byteBuf = createErrorFrame(0, CONNECTION_CLOSE, "test-message"); + try { - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(ConnectionCloseException.class) - .hasMessage("test-message"); + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(ConnectionCloseException.class) + .hasMessage("test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage( - "Invalid Error frame in Stream ID 1: 0x%08X '%s'", CONNECTION_CLOSE, "test-message"); + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", CONNECTION_CLOSE, "test-message"); + } finally { + byteBuf.release(); + } } @DisplayName("from returns ConnectionErrorException") @@ -87,116 +100,146 @@ void fromConnectionCloseException() { void fromConnectionErrorException() { ByteBuf byteBuf = createErrorFrame(0, CONNECTION_ERROR, "test-message"); - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(ConnectionErrorException.class) - .hasMessage("test-message"); + try { - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage( - "Invalid Error frame in Stream ID 1: 0x%08X '%s'", CONNECTION_ERROR, "test-message"); + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(ConnectionErrorException.class) + .hasMessage("test-message"); + + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", CONNECTION_ERROR, "test-message"); + } finally { + byteBuf.release(); + } } @DisplayName("from returns IllegalArgumentException if error frame has illegal error code") @Test void fromIllegalErrorFrame() { ByteBuf byteBuf = createErrorFrame(0, 0x00000000, "test-message"); + try { - assertThat(Exceptions.from(0, byteBuf)) - .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", 0, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", 0, "test-message") + .isInstanceOf(IllegalArgumentException.class); - assertThat(Exceptions.from(1, byteBuf)) - .hasMessage("Invalid Error frame in Stream ID 1: 0x%08X '%s'", 0x00000000, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 1: 0x%08X '%s'", 0x00000000, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns InvalidException") @Test void fromInvalidException() { ByteBuf byteBuf = createErrorFrame(1, INVALID, "test-message"); + try { + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(InvalidException.class) + .hasMessage("test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(InvalidException.class) - .hasMessage("test-message"); - - assertThat(Exceptions.from(0, byteBuf)) - .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", INVALID, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", INVALID, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns InvalidSetupException") @Test void fromInvalidSetupException() { ByteBuf byteBuf = createErrorFrame(0, INVALID_SETUP, "test-message"); + try { + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(InvalidSetupException.class) + .hasMessage("test-message"); - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(InvalidSetupException.class) - .hasMessage("test-message"); - - assertThat(Exceptions.from(1, byteBuf)) - .hasMessage( - "Invalid Error frame in Stream ID 1: 0x%08X '%s'", INVALID_SETUP, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", INVALID_SETUP, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns RejectedException") @Test void fromRejectedException() { ByteBuf byteBuf = createErrorFrame(1, REJECTED, "test-message"); + try { - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(RejectedException.class) - .withFailMessage("test-message"); + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(RejectedException.class) + .withFailMessage("test-message"); - assertThat(Exceptions.from(0, byteBuf)) - .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", REJECTED, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", REJECTED, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns RejectedResumeException") @Test void fromRejectedResumeException() { ByteBuf byteBuf = createErrorFrame(0, REJECTED_RESUME, "test-message"); + try { - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(RejectedResumeException.class) - .hasMessage("test-message"); + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(RejectedResumeException.class) + .hasMessage("test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .hasMessage( - "Invalid Error frame in Stream ID 1: 0x%08X '%s'", REJECTED_RESUME, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", REJECTED_RESUME, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns RejectedSetupException") @Test void fromRejectedSetupException() { ByteBuf byteBuf = createErrorFrame(0, REJECTED_SETUP, "test-message"); + try { - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(RejectedSetupException.class) - .withFailMessage("test-message"); + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(RejectedSetupException.class) + .withFailMessage("test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .hasMessage( - "Invalid Error frame in Stream ID 1: 0x%08X '%s'", REJECTED_SETUP, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", REJECTED_SETUP, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns UnsupportedSetupException") @Test void fromUnsupportedSetupException() { ByteBuf byteBuf = createErrorFrame(0, UNSUPPORTED_SETUP, "test-message"); + try { + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(UnsupportedSetupException.class) + .hasMessage("test-message"); - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(UnsupportedSetupException.class) - .hasMessage("test-message"); - - assertThat(Exceptions.from(1, byteBuf)) - .hasMessage( - "Invalid Error frame in Stream ID 1: 0x%08X '%s'", UNSUPPORTED_SETUP, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", UNSUPPORTED_SETUP, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns CustomRSocketException") @@ -210,15 +253,18 @@ void fromCustomRSocketException() { : ThreadLocalRandom.current() .nextInt(ErrorFrameCodec.MIN_USER_ALLOWED_ERROR_CODE, Integer.MAX_VALUE); ByteBuf byteBuf = createErrorFrame(0, randomCode, "test-message"); - - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(CustomRSocketException.class) - .hasMessage("test-message"); - - assertThat(Exceptions.from(0, byteBuf)) - .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", randomCode, "test-message") - .isInstanceOf(IllegalArgumentException.class); - byteBuf.release(); + try { + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(CustomRSocketException.class) + .hasMessage("test-message"); + + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 0: 0x%08X '%s'", randomCode, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } } diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java index c838d704c..a35e89391 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java @@ -33,7 +33,10 @@ class LoadbalanceRSocketClientTest { public static final Duration LONG_DURATION = Duration.ofMillis(75); private static final Publisher SOURCE = - Flux.interval(SHORT_DURATION).map(String::valueOf).map(DefaultPayload::create); + Flux.interval(SHORT_DURATION) + .onBackpressureBuffer() + .map(String::valueOf) + .map(DefaultPayload::create); private static final Mono PROGRESSING_HANDLER = Mono.just( diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java index fcd3ae4a9..c1b509297 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java @@ -20,7 +20,6 @@ import io.rsocket.RaceTestConstants; import io.rsocket.core.RSocketConnector; import io.rsocket.internal.subscriber.AssertSubscriber; -import io.rsocket.plugins.RSocketInterceptor; import io.rsocket.test.util.TestClientTransport; import io.rsocket.transport.ClientTransport; import io.rsocket.util.EmptyPayload; @@ -71,10 +70,11 @@ public Mono fireAndForget(Payload payload) { return Mono.empty(); } }; - final RSocketConnector rSocketConnectorMock = - RSocketConnector.create() - .interceptors( - ir -> ir.forRequester((RSocketInterceptor) socket -> new TestRSocket(rSocket))); + + final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + final ClientTransport mockTransport1 = Mockito.mock(ClientTransport.class); + Mockito.when(rSocketConnectorMock.connect(Mockito.any(ClientTransport.class))) + .then(im -> Mono.just(new TestRSocket(rSocket))); final List collectionOfDestination1 = Collections.singletonList(LoadbalanceTarget.from("1", mockTransport)); diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataCodecTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataCodecTest.java index 3ce07729d..a4e8fb2d8 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataCodecTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataCodecTest.java @@ -23,12 +23,22 @@ import io.netty.buffer.*; import io.netty.util.CharsetUtil; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.test.util.ByteBufUtils; import io.rsocket.util.NumberUtils; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; class CompositeMetadataCodecTest { + final LeaksTrackingByteBufAllocator testAllocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + + @AfterEach + void tearDownAndCheckForLeaks() { + testAllocator.assertHasNoLeaks(); + } + static String byteToBitsString(byte b) { return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'); } @@ -48,17 +58,14 @@ void customMimeHeaderLatin1_encodingFails() { assertThatIllegalArgumentException() .isThrownBy( - () -> - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + () -> CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeNotAscii, 0)) .withMessage("custom mime type must be US_ASCII characters only"); } @Test void customMimeHeaderLength0_encodingFails() { assertThatIllegalArgumentException() - .isThrownBy( - () -> CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "", 0)) + .isThrownBy(() -> CompositeMetadataCodec.encodeMetadataHeader(testAllocator, "", 0)) .withMessage( "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); } @@ -70,8 +77,7 @@ void customMimeHeaderLength127() { builder.append('a'); } String mimeString = builder.toString(); - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeString, 0); // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111110"); @@ -99,6 +105,7 @@ void customMimeHeaderLength127() { .hasToString(mimeString); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -108,8 +115,7 @@ void customMimeHeaderLength128() { builder.append('a'); } String mimeString = builder.toString(); - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeString, 0); // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111111"); @@ -137,6 +143,7 @@ void customMimeHeaderLength128() { .hasToString(mimeString); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -148,9 +155,7 @@ void customMimeHeaderLength129_encodingFails() { assertThatIllegalArgumentException() .isThrownBy( - () -> - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, builder.toString(), 0)) + () -> CompositeMetadataCodec.encodeMetadataHeader(testAllocator, builder.toString(), 0)) .withMessage( "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); } @@ -158,8 +163,7 @@ void customMimeHeaderLength129_encodingFails() { @Test void customMimeHeaderLengthOne() { String mimeString = "w"; - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeString, 0); // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000000"); @@ -185,13 +189,13 @@ void customMimeHeaderLengthOne() { .hasToString(mimeString); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test void customMimeHeaderLengthTwo() { String mimeString = "ww"; - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeString, 0); // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000001"); @@ -219,6 +223,7 @@ void customMimeHeaderLengthTwo() { .hasToString(mimeString); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -227,9 +232,7 @@ void customMimeHeaderUtf8_encodingFails() { "mime/tyࠒe"; // this is the SAMARITAN LETTER QUF u+0812 represented on 3 bytes assertThatIllegalArgumentException() .isThrownBy( - () -> - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + () -> CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeNotAscii, 0)) .withMessage("custom mime type must be US_ASCII characters only"); } @@ -317,72 +320,73 @@ void decodeTypeSkipsFirstByte() { @Test void encodeMetadataCustomTypeDelegates() { - ByteBuf expected = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo", 2); + ByteBuf expected = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, "foo", 2); - CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf test = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadata( - test, ByteBufAllocator.DEFAULT, "foo", ByteBufUtils.getRandomByteBuf(2)); + test, testAllocator, "foo", ByteBufUtils.getRandomByteBuf(2)); assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + test.release(); + expected.release(); } @Test void encodeMetadataKnownTypeDelegates() { ByteBuf expected = CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, - WellKnownMimeType.APPLICATION_OCTET_STREAM.getIdentifier(), - 2); + testAllocator, WellKnownMimeType.APPLICATION_OCTET_STREAM.getIdentifier(), 2); - CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf test = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadata( test, - ByteBufAllocator.DEFAULT, + testAllocator, WellKnownMimeType.APPLICATION_OCTET_STREAM, ByteBufUtils.getRandomByteBuf(2)); assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + test.release(); + expected.release(); } @Test void encodeMetadataReservedTypeDelegates() { - ByteBuf expected = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, (byte) 120, 2); + ByteBuf expected = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, (byte) 120, 2); - CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf test = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadata( - test, ByteBufAllocator.DEFAULT, (byte) 120, ByteBufUtils.getRandomByteBuf(2)); + test, testAllocator, (byte) 120, ByteBufUtils.getRandomByteBuf(2)); assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + test.release(); + expected.release(); } @Test void encodeTryCompressWithCompressableType() { ByteBuf metadata = ByteBufUtils.getRandomByteBuf(2); - CompositeByteBuf target = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf target = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadataWithCompression( - target, - UnpooledByteBufAllocator.DEFAULT, - WellKnownMimeType.APPLICATION_AVRO.getString(), - metadata); + target, testAllocator, WellKnownMimeType.APPLICATION_AVRO.getString(), metadata); assertThat(target.readableBytes()).as("readableBytes 1 + 3 + 2").isEqualTo(6); + target.release(); } @Test void encodeTryCompressWithCustomType() { ByteBuf metadata = ByteBufUtils.getRandomByteBuf(2); - CompositeByteBuf target = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf target = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadataWithCompression( - target, UnpooledByteBufAllocator.DEFAULT, "custom/example", metadata); + target, testAllocator, "custom/example", metadata); assertThat(target.readableBytes()).as("readableBytes 1 + 14 + 3 + 2").isEqualTo(20); + target.release(); } @Test @@ -390,19 +394,20 @@ void hasEntry() { WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; CompositeByteBuf buffer = - Unpooled.compositeBuffer() + testAllocator + .compositeBuffer() .addComponent( true, - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0)) + CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mime.getIdentifier(), 0)) .addComponent( true, CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0)); + testAllocator, mime.getIdentifier(), 0)); assertThat(CompositeMetadataCodec.hasEntry(buffer, 0)).isTrue(); assertThat(CompositeMetadataCodec.hasEntry(buffer, 4)).isTrue(); assertThat(CompositeMetadataCodec.hasEntry(buffer, 8)).isFalse(); + buffer.release(); } @Test @@ -417,8 +422,7 @@ void isWellKnownMimeType() { @Test void knownMimeHeader120_reserved() { byte mime = (byte) 120; - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mime, 0); assertThat(mime) .as("smoke test RESERVED_120 unsigned 7 bits representation") @@ -443,6 +447,7 @@ void knownMimeHeader120_reserved() { assertThat(decodeMimeIdFromMimeBuffer(header)).as("decoded mime id").isEqualTo(mime); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -453,8 +458,7 @@ void knownMimeHeader127_compositeMetadata() { .isEqualTo((byte) 127) .isEqualTo((byte) 0b01111111); ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); + CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mime.getIdentifier(), 0); assertThat(toHeaderBits(encoded)) .startsWith("1") @@ -480,6 +484,7 @@ void knownMimeHeader127_compositeMetadata() { .isEqualTo(mime.getIdentifier()); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -490,8 +495,7 @@ void knownMimeHeaderZero_avro() { .isEqualTo((byte) 0) .isEqualTo((byte) 0b00000000); ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); + CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mime.getIdentifier(), 0); assertThat(toHeaderBits(encoded)) .startsWith("1") @@ -517,6 +521,7 @@ void knownMimeHeaderZero_avro() { .isEqualTo(mime.getIdentifier()); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java index 9227bcaca..5c8d40306 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java @@ -30,20 +30,28 @@ public class MimeTypeMetadataCodecTest { public void wellKnownMimeType() { WellKnownMimeType mimeType = WellKnownMimeType.APPLICATION_HESSIAN; ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, mimeType); - List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); + try { + List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); - assertThat(mimeTypes.size()).isEqualTo(1); - assertThat(WellKnownMimeType.fromString(mimeTypes.get(0))).isEqualTo(mimeType); + assertThat(mimeTypes.size()).isEqualTo(1); + assertThat(WellKnownMimeType.fromString(mimeTypes.get(0))).isEqualTo(mimeType); + } finally { + byteBuf.release(); + } } @Test public void customMimeType() { String mimeType = "aaa/bb"; ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, mimeType); - List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); + try { + List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); - assertThat(mimeTypes.size()).isEqualTo(1); - assertThat(mimeTypes.get(0)).isEqualTo(mimeType); + assertThat(mimeTypes.size()).isEqualTo(1); + assertThat(mimeTypes.get(0)).isEqualTo(mimeType); + } finally { + byteBuf.release(); + } } @Test @@ -51,6 +59,10 @@ public void multipleMimeTypes() { List mimeTypes = Lists.newArrayList("aaa/bbb", "application/x-hessian"); ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, mimeTypes); - assertThat(MimeTypeMetadataCodec.decode(byteBuf)).isEqualTo(mimeTypes); + try { + assertThat(MimeTypeMetadataCodec.decode(byteBuf)).isEqualTo(mimeTypes); + } finally { + byteBuf.release(); + } } } diff --git a/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java b/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java index 2bb718ef7..9a19050f9 100644 --- a/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java @@ -1,16 +1,19 @@ package io.rsocket.plugins; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.Closeable; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.frame.FrameType; import io.rsocket.transport.local.LocalClientTransport; import io.rsocket.transport.local.LocalServerTransport; import io.rsocket.util.DefaultPayload; +import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.Function; @@ -29,6 +32,9 @@ public class RequestInterceptorTest { @ParameterizedTest @ValueSource(booleans = {true, false}) void interceptorShouldBeInstalledProperlyOnTheClientRequesterSide(boolean errorOutcome) { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); final Closeable closeable = RSocketServer.create( SocketAcceptor.with( @@ -69,7 +75,7 @@ public Flux requestChannel(Publisher payloads) { ir.forRequestsInRequester( (Function) (__) -> testRequestInterceptor)) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -130,6 +136,7 @@ public Flux requestChannel(Publisher payloads) { } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @@ -137,6 +144,10 @@ public Flux requestChannel(Publisher payloads) { @ValueSource(booleans = {true, false}) void interceptorShouldBeInstalledProperlyOnTheClientResponderSide(boolean errorOutcome) throws InterruptedException { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + CountDownLatch latch = new CountDownLatch(1); final Closeable closeable = RSocketServer.create( @@ -209,7 +220,7 @@ public Flux requestChannel(Publisher payloads) { ir.forRequestsInResponder( (Function) (__) -> testRequestInterceptor)) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -253,12 +264,17 @@ public Flux requestChannel(Publisher payloads) { } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @ParameterizedTest @ValueSource(booleans = {true, false}) void interceptorShouldBeInstalledProperlyOnTheServerRequesterSide(boolean errorOutcome) { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); final Closeable closeable = RSocketServer.create( @@ -297,7 +313,9 @@ public Flux requestChannel(Publisher payloads) { (__) -> testRequestInterceptor)) .bindNow(LocalServerTransport.create("test")); final RSocket rSocket = - RSocketConnector.create().connect(LocalClientTransport.create("test")).block(); + RSocketConnector.create() + .connect(LocalClientTransport.create("test", byteBufAllocator)) + .block(); try { rSocket @@ -357,6 +375,7 @@ public Flux requestChannel(Publisher payloads) { } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @@ -364,6 +383,10 @@ public Flux requestChannel(Publisher payloads) { @ValueSource(booleans = {true, false}) void interceptorShouldBeInstalledProperlyOnTheServerResponderSide(boolean errorOutcome) throws InterruptedException { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + CountDownLatch latch = new CountDownLatch(1); final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); final Closeable closeable = @@ -437,7 +460,7 @@ public Flux requestChannel(Publisher payloads) { : Flux.from(payloads); } })) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -481,11 +504,16 @@ public Flux requestChannel(Publisher payloads) { } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @Test void ensuresExceptionInTheInterceptorIsHandledProperly() { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + final Closeable closeable = RSocketServer.create( SocketAcceptor.with( @@ -546,7 +574,7 @@ public void dispose() {} ir.forRequestsInRequester( (Function) (__) -> testRequestInterceptor)) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -575,12 +603,17 @@ public void dispose() {} } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @ParameterizedTest @ValueSource(booleans = {true, false}) void shouldSupportMultipleInterceptors(boolean errorOutcome) { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + final Closeable closeable = RSocketServer.create( SocketAcceptor.with( @@ -655,7 +688,7 @@ public void dispose() {} .forRequestsInRequester( (Function) (__) -> testRequestInterceptor2)) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -751,6 +784,7 @@ public void dispose() {} } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } } diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java index bdd46f8c6..8229bf42b 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java @@ -159,6 +159,7 @@ void sessionTimeoutSmokeTest() { assertThat(session.isDisposed()).isTrue(); resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); + keepAliveSupport.dispose(); transport.alloc().assertHasNoLeaks(); } finally { VirtualTimeScheduler.reset(); @@ -291,6 +292,7 @@ void sessionTerminationOnWrongFrameTest() { .as(StepVerifier::create) .expectErrorMessage("RESUME_OK frame must be received before any others") .verify(); + keepAliveSupport.dispose(); transport.alloc().assertHasNoLeaks(); } finally { VirtualTimeScheduler.reset(); @@ -386,6 +388,7 @@ void shouldErrorWithNoRetriesOnErrorFrameTest() { .as(StepVerifier::create) .expectError(RejectedResumeException.class) .verify(); + keepAliveSupport.dispose(); transport.alloc().assertHasNoLeaks(); } finally { VirtualTimeScheduler.reset(); @@ -458,7 +461,7 @@ void shouldTerminateConnectionOnIllegalStateInKeepAliveFrame() { .matches(ReferenceCounted::release); resumableDuplexConnection.onClose().as(StepVerifier::create).expectError().verify(); - + keepAliveSupport.dispose(); transport.alloc().assertHasNoLeaks(); } finally { VirtualTimeScheduler.reset(); diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java index eff65f587..b5625bf8e 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java @@ -22,161 +22,169 @@ public class ServerRSocketSessionTest { @Test void sessionTimeoutSmokeTest() { final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); - final TestClientTransport transport = new TestClientTransport(); - final InMemoryResumableFramesStore framesStore = - new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); - - transport.connect().subscribe(); - - final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection( - "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); - - resumableDuplexConnection.receive().subscribe(); - - final ServerRSocketSession session = - new ServerRSocketSession( - Unpooled.EMPTY_BUFFER, - resumableDuplexConnection, - transport.testConnection(), - framesStore, - Duration.ofMinutes(1), - true); - - final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = - new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); - session.setKeepAliveSupport(keepAliveSupport); - - // connection is active. just advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - // deactivate connection - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // resubscribe so a new connection is generated - transport.connect().subscribe(); - - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); - // timeout should not terminate current connection - assertThat(transport.testConnection().isDisposed()).isFalse(); - - // send RESUME frame - final ByteBuf resumeFrame = - ResumeFrameCodec.encode(transport.alloc(), Unpooled.EMPTY_BUFFER, 0, 0); - session.resumeWith(resumeFrame, transport.testConnection()); - resumeFrame.release(); - - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be terminated - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME_OK) - .matches(ReferenceCounted::release); - - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); - - // disconnects for the second time - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - transport.connect().subscribe(); - - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(61)); - - final ByteBuf resumeFrame1 = - ResumeFrameCodec.encode(transport.alloc(), Unpooled.EMPTY_BUFFER, 0, 0); - session.resumeWith(resumeFrame1, transport.testConnection()); - resumeFrame1.release(); - - // should obtain new connection - assertThat(transport.testConnection().isDisposed()).isTrue(); - // timeout should be still active since no RESUME_OK frame has been received yet - assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); - assertThat(session.isDisposed()).isTrue(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.ERROR) - .matches(ReferenceCounted::release); - - resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); - transport.alloc().assertHasNoLeaks(); + try { + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ServerRSocketSession session = + new ServerRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.testConnection(), + framesStore, + Duration.ofMinutes(1), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + // deactivate connection + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // resubscribe so a new connection is generated + transport.connect().subscribe(); + + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); + // timeout should not terminate current connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + + // send RESUME frame + final ByteBuf resumeFrame = + ResumeFrameCodec.encode(transport.alloc(), Unpooled.EMPTY_BUFFER, 0, 0); + session.resumeWith(resumeFrame, transport.testConnection()); + resumeFrame.release(); + + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be terminated + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME_OK) + .matches(ReferenceCounted::release); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); + + // disconnects for the second time + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + transport.connect().subscribe(); + + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(61)); + + final ByteBuf resumeFrame1 = + ResumeFrameCodec.encode(transport.alloc(), Unpooled.EMPTY_BUFFER, 0, 0); + session.resumeWith(resumeFrame1, transport.testConnection()); + resumeFrame1.release(); + + // should obtain new connection + assertThat(transport.testConnection().isDisposed()).isTrue(); + // timeout should be still active since no RESUME_OK frame has been received yet + assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); + assertThat(session.isDisposed()).isTrue(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.ERROR) + .matches(ReferenceCounted::release); + + resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } } @Test void shouldTerminateConnectionOnIllegalStateInKeepAliveFrame() { final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); - final TestClientTransport transport = new TestClientTransport(); - final InMemoryResumableFramesStore framesStore = - new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); - - transport.connect().subscribe(); - - final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection( - "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); - - resumableDuplexConnection.receive().subscribe(); - - final ServerRSocketSession session = - new ServerRSocketSession( - Unpooled.EMPTY_BUFFER, - resumableDuplexConnection, - transport.testConnection(), - framesStore, - Duration.ofMinutes(1), - true); - - final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = - new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); - keepAliveSupport.resumeState(session); - session.setKeepAliveSupport(keepAliveSupport); - - // connection is active. just advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - final ByteBuf keepAliveFrame = - KeepAliveFrameCodec.encode(transport.alloc(), false, 1529, Unpooled.EMPTY_BUFFER); - keepAliveSupport.receive(keepAliveFrame); - keepAliveFrame.release(); - - assertThat(transport.testConnection().isDisposed()).isTrue(); - // timeout should be terminated - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isTrue(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.ERROR) - .matches(ReferenceCounted::release); - - resumableDuplexConnection.onClose().as(StepVerifier::create).expectError().verify(); - - transport.alloc().assertHasNoLeaks(); + try { + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ServerRSocketSession session = + new ServerRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.testConnection(), + framesStore, + Duration.ofMinutes(1), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + keepAliveSupport.resumeState(session); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + final ByteBuf keepAliveFrame = + KeepAliveFrameCodec.encode(transport.alloc(), false, 1529, Unpooled.EMPTY_BUFFER); + keepAliveSupport.receive(keepAliveFrame); + keepAliveFrame.release(); + + assertThat(transport.testConnection().isDisposed()).isTrue(); + // timeout should be terminated + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isTrue(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.ERROR) + .matches(ReferenceCounted::release); + + resumableDuplexConnection.onClose().as(StepVerifier::create).expectError().verify(); + keepAliveSupport.dispose(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } } } diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java index e307627ff..f02bc99a4 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java @@ -6,11 +6,13 @@ import io.rsocket.DuplexConnection; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.transport.ClientTransport; +import java.time.Duration; import reactor.core.publisher.Mono; public class TestClientTransport implements ClientTransport { private final LeaksTrackingByteBufAllocator allocator = - LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "client"); private volatile TestDuplexConnection testDuplexConnection; diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java index 625f8fcb1..cd96584ed 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java @@ -78,8 +78,6 @@ public void testRangeOfConsumers() { .block(); Flux.range(1, 6).flatMap(i -> consumer("connection number -> " + i)).blockLast(); - System.out.println("here"); - } finally { server.dispose(); } From 6d0738974fc3451508112dfdaeee2e174666e1cc Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Mon, 24 Apr 2023 12:27:07 +0300 Subject: [PATCH 170/183] adds class check for discarded values (#1091) --- .../io/rsocket/core/DefaultRSocketClient.java | 17 +++++----- .../main/java/io/rsocket/core/SendUtils.java | 12 ++++--- .../core/DefaultRSocketClientTests.java | 22 +++++++++++++ .../java/io/rsocket/core/SendUtilsTest.java | 31 +++++++++++++++++++ 4 files changed, 70 insertions(+), 12 deletions(-) create mode 100644 rsocket-core/src/test/java/io/rsocket/core/SendUtilsTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java index 9cd89c0b1..82a02268d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java @@ -46,13 +46,16 @@ */ class DefaultRSocketClient extends ResolvingOperator implements CoreSubscriber, CorePublisher, RSocketClient { - static final Consumer DISCARD_ELEMENTS_CONSUMER = - referenceCounted -> { - if (referenceCounted.refCnt() > 0) { - try { - referenceCounted.release(); - } catch (IllegalReferenceCountException e) { - // ignored + static final Consumer DISCARD_ELEMENTS_CONSUMER = + data -> { + if (data instanceof ReferenceCounted) { + ReferenceCounted referenceCounted = ((ReferenceCounted) data); + if (referenceCounted.refCnt() > 0) { + try { + referenceCounted.release(); + } catch (IllegalReferenceCountException e) { + // ignored + } } } }; diff --git a/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java b/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java index 53d222605..568dada2e 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java +++ b/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java @@ -40,11 +40,13 @@ final class SendUtils { private static final Consumer DROPPED_ELEMENTS_CONSUMER = data -> { - try { - ReferenceCounted referenceCounted = (ReferenceCounted) data; - referenceCounted.release(); - } catch (Throwable e) { - // ignored + if (data instanceof ReferenceCounted) { + try { + ReferenceCounted referenceCounted = (ReferenceCounted) data; + referenceCounted.release(); + } catch (Throwable e) { + // ignored + } } }; diff --git a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java index a8a5f2e58..84576e6ce 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -39,6 +39,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import org.assertj.core.api.Assertions; @@ -49,6 +50,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; import org.reactivestreams.Publisher; import reactor.core.Disposable; import reactor.core.publisher.Flux; @@ -79,6 +81,26 @@ public void setUp() throws Throwable { public void tearDown() { Hooks.resetOnErrorDropped(); Hooks.resetOnNextDropped(); + rule.allocator.assertHasNoLeaks(); + } + + @Test + @SuppressWarnings("unchecked") + void discardElementsConsumerShouldAcceptOtherTypesThanReferenceCounted() { + Consumer discardElementsConsumer = DefaultRSocketClient.DISCARD_ELEMENTS_CONSUMER; + discardElementsConsumer.accept(new Object()); + } + + @Test + void droppedElementsConsumerReleaseReference() { + ReferenceCounted referenceCounted = Mockito.mock(ReferenceCounted.class); + Mockito.when(referenceCounted.release()).thenReturn(true); + Mockito.when(referenceCounted.refCnt()).thenReturn(1); + + Consumer discardElementsConsumer = DefaultRSocketClient.DISCARD_ELEMENTS_CONSUMER; + discardElementsConsumer.accept(referenceCounted); + + Mockito.verify(referenceCounted).release(); } static Stream interactions() { diff --git a/rsocket-core/src/test/java/io/rsocket/core/SendUtilsTest.java b/rsocket-core/src/test/java/io/rsocket/core/SendUtilsTest.java new file mode 100644 index 000000000..9a51b9419 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/SendUtilsTest.java @@ -0,0 +1,31 @@ +package io.rsocket.core; + +import static org.mockito.Mockito.*; + +import io.netty.util.ReferenceCounted; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; + +public class SendUtilsTest { + + @Test + void droppedElementsConsumerShouldAcceptOtherTypesThanReferenceCounted() { + Consumer value = extractDroppedElementConsumer(); + value.accept(new Object()); + } + + @Test + void droppedElementsConsumerReleaseReference() { + ReferenceCounted referenceCounted = mock(ReferenceCounted.class); + when(referenceCounted.release()).thenReturn(true); + + Consumer value = extractDroppedElementConsumer(); + value.accept(referenceCounted); + + verify(referenceCounted).release(); + } + + private static Consumer extractDroppedElementConsumer() { + return (Consumer) SendUtils.DISCARD_CONTEXT.stream().findAny().get().getValue(); + } +} From 6bba66288c42aaf6f348c8cab12e77325a888036 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 6 Jun 2023 20:43:42 +0300 Subject: [PATCH 171/183] bumps lib versions Signed-off-by: Oleh Dokuka --- build.gradle | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 079a0e1d9..62ee687da 100644 --- a/build.gradle +++ b/build.gradle @@ -16,11 +16,11 @@ plugins { id 'com.github.sherter.google-java-format' version '0.9' apply false - id 'me.champeau.jmh' version '0.6.7' apply false - id 'io.spring.dependency-management' version '1.0.15.RELEASE' apply false + id 'me.champeau.jmh' version '0.7.1' apply false + id 'io.spring.dependency-management' version '1.1.0' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false - id 'io.github.reyerizo.gradle.jcstress' version '0.8.13' apply false - id 'com.github.vlsi.gradle-extensions' version '1.76' apply false + id 'io.github.reyerizo.gradle.jcstress' version '0.8.15' apply false + id 'com.github.vlsi.gradle-extensions' version '1.89' apply false } boolean isCiServer = ["CI", "CONTINUOUS_INTEGRATION", "TRAVIS", "CIRCLECI", "bamboo_planKey", "GITHUB_ACTION"].with { @@ -33,21 +33,21 @@ subprojects { apply plugin: 'com.github.sherter.google-java-format' apply plugin: 'com.github.vlsi.gradle-extensions' - ext['reactor-bom.version'] = '2020.0.31-SNAPSHOT' + ext['reactor-bom.version'] = '2020.0.32' ext['logback.version'] = '1.2.10' - ext['netty-bom.version'] = '4.1.90.Final' - ext['netty-boringssl.version'] = '2.0.59.Final' + ext['netty-bom.version'] = '4.1.93.Final' + ext['netty-boringssl.version'] = '2.0.61.Final' ext['hdrhistogram.version'] = '2.1.12' - ext['mockito.version'] = '4.4.0' + ext['mockito.version'] = '4.11.0' ext['slf4j.version'] = '1.7.36' - ext['jmh.version'] = '1.35' - ext['junit.version'] = '5.8.1' - ext['micrometer.version'] = '1.10.0' - ext['micrometer-tracing.version'] = '1.0.0' - ext['assertj.version'] = '3.22.0' + ext['jmh.version'] = '1.36' + ext['junit.version'] = '5.9.3' + ext['micrometer.version'] = '1.11.0' + ext['micrometer-tracing.version'] = '1.1.1' + ext['assertj.version'] = '3.24.2' ext['netflix.limits.version'] = '0.3.6' ext['bouncycastle-bcpkix.version'] = '1.70' - ext['awaitility.version'] = '4.1.1' + ext['awaitility.version'] = '4.2.0' group = "io.rsocket" From cb811cf6c77c59cc4b62d7caf356f203c92c22c5 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Fri, 9 Jun 2023 20:57:59 +0300 Subject: [PATCH 172/183] increment version Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 7f8f4ca23..ce5421125 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.1.4 -perfBaselineVersion=1.1.3 +version=1.1.5 +perfBaselineVersion=1.1.4 From f591f9d8313b79ea674ee7786000bb1bfd48e1d8 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 27 Feb 2024 13:08:04 +0200 Subject: [PATCH 173/183] update versions and fixes memleak in UnboundedProcessor (#1106) This PR updates dependencies and makes minor modifications to UnboundedProcessor due to repeating failures of UnboundedProcessorJCStreassTest, which started reproducing some unspotted issues. Motivation: UnboundedProcessor is a critical component in the RSocket-Java ecosystem and must work properly. After analysis of its internal state machine, it was spotted that sometimes: The request may not be delivered due to natural concurrency The terminal signal may not be delivered since it checks for demand which might be consumed already (due to natural concurrency) The final value could be delivered violating reactive-streams spec Modifications: This PR adds a minimal set of changes, preserving old implementation but eliminating the mentioned bugs --------- Signed-off-by: Oleh Dokuka --- build.gradle | 8 +- rsocket-core/build.gradle | 5 +- .../UnboundedProcessorStressTest.java | 52 ++++- .../java/io/rsocket/utils/FastLogger.java | 137 +++++++++++ .../rsocket/internal/UnboundedProcessor.java | 220 ++++++++++++++---- 5 files changed, 370 insertions(+), 52 deletions(-) create mode 100644 rsocket-core/src/jcstress/java/io/rsocket/utils/FastLogger.java diff --git a/build.gradle b/build.gradle index 62ee687da..ae8f05d06 100644 --- a/build.gradle +++ b/build.gradle @@ -33,10 +33,10 @@ subprojects { apply plugin: 'com.github.sherter.google-java-format' apply plugin: 'com.github.vlsi.gradle-extensions' - ext['reactor-bom.version'] = '2020.0.32' - ext['logback.version'] = '1.2.10' - ext['netty-bom.version'] = '4.1.93.Final' - ext['netty-boringssl.version'] = '2.0.61.Final' + ext['reactor-bom.version'] = '2020.0.39' + ext['logback.version'] = '1.3.14' + ext['netty-bom.version'] = '4.1.106.Final' + ext['netty-boringssl.version'] = '2.0.62.Final' ext['hdrhistogram.version'] = '2.1.12' ext['mockito.version'] = '4.11.0' ext['slf4j.version'] = '1.7.36' diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index 6f2056da0..da5b69b14 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -41,13 +41,14 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' jcstressImplementation(project(":rsocket-test")) + jcstressImplementation 'org.slf4j:slf4j-api' jcstressImplementation "ch.qos.logback:logback-classic" jcstressImplementation 'io.projectreactor:reactor-test' } jcstress { - mode = 'quick' //quick, default, tough - jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.15" + mode = 'sanity' //sanity, quick, default, tough + jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.16" } jar { diff --git a/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java index 2f5e51f0e..a2d9fcf4d 100644 --- a/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java +++ b/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java @@ -3,6 +3,9 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.UnpooledByteBufAllocator; import io.rsocket.core.StressSubscriber; +import io.rsocket.utils.FastLogger; +import java.util.Arrays; +import java.util.ConcurrentModificationException; import org.openjdk.jcstress.annotations.Actor; import org.openjdk.jcstress.annotations.Arbiter; import org.openjdk.jcstress.annotations.Expect; @@ -14,6 +17,7 @@ import org.openjdk.jcstress.infra.results.L_Result; import reactor.core.Fuseable; import reactor.core.publisher.Hooks; +import reactor.util.Logger; public abstract class UnboundedProcessorStressTest { @@ -21,7 +25,9 @@ public abstract class UnboundedProcessorStressTest { Hooks.onErrorDropped(t -> {}); } - final UnboundedProcessor unboundedProcessor = new UnboundedProcessor(); + final Logger logger = new FastLogger(getClass().getName()); + + final UnboundedProcessor unboundedProcessor = new UnboundedProcessor(logger); @JCStressTest @Outcome( @@ -145,6 +151,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -270,6 +278,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -375,6 +385,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -476,6 +488,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -578,6 +592,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -701,6 +717,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -781,6 +799,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -837,9 +857,15 @@ public void arbiter(LLL_Result r) { + stressSubscriber.onErrorCalls * 2 + stressSubscriber.droppedErrors.size() * 3; + if (stressSubscriber.concurrentOnNext || stressSubscriber.concurrentOnComplete) { + throw new ConcurrentModificationException("boo"); + } + stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -892,6 +918,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1107,6 +1135,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1238,6 +1268,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1390,6 +1422,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1522,6 +1556,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1587,6 +1623,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1652,6 +1690,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1678,6 +1718,16 @@ public void subscribe2() { @Arbiter public void arbiter(L_Result r) { r.r1 = stressSubscriber1.onErrorCalls + stressSubscriber2.onErrorCalls; + + checkOutcomes(this, r.toString(), logger); + } + } + + static void checkOutcomes(Object instance, String result, Logger logger) { + if (Arrays.stream(instance.getClass().getDeclaredAnnotationsByType(Outcome.class)) + .flatMap(o -> Arrays.stream(o.id())) + .noneMatch(s -> s.equalsIgnoreCase(result))) { + throw new RuntimeException(result + " " + logger); } } } diff --git a/rsocket-core/src/jcstress/java/io/rsocket/utils/FastLogger.java b/rsocket-core/src/jcstress/java/io/rsocket/utils/FastLogger.java new file mode 100644 index 000000000..c301d87cf --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/utils/FastLogger.java @@ -0,0 +1,137 @@ +package io.rsocket.utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import reactor.util.Logger; + +/** + * Implementation of {@link Logger} which is based on the {@link ThreadLocal} based queue which + * collects all the events on the per-thread basis.
    Such logger is designed to have all events + * stored during the stress-test run and then sorted and printed out once all the Threads completed + * execution (inside the {@link org.openjdk.jcstress.annotations.Arbiter} annotated method.
    + * Note, this implementation only supports trace-level logs and ignores all others, it is intended + * to be used by {@link reactor.core.publisher.StateLogger}. + */ +public class FastLogger implements Logger { + + final Map> queues = new ConcurrentHashMap<>(); + + final ThreadLocal> logsQueueLocal = + ThreadLocal.withInitial( + () -> { + final ArrayList logs = new ArrayList<>(100); + queues.put(Thread.currentThread(), logs); + return logs; + }); + + private final String name; + + public FastLogger(String name) { + this.name = name; + } + + @Override + public String toString() { + return queues + .values() + .stream() + .flatMap(List::stream) + .sorted( + Comparator.comparingLong( + s -> { + Pattern pattern = Pattern.compile("\\[(.*?)]"); + Matcher matcher = pattern.matcher(s); + matcher.find(); + return Long.parseLong(matcher.group(1)); + })) + .collect(Collectors.joining("\n")); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public boolean isTraceEnabled() { + return true; + } + + @Override + public void trace(String msg) { + logsQueueLocal.get().add(String.format("[%s] %s", System.nanoTime(), msg)); + } + + @Override + public void trace(String format, Object... arguments) { + trace(String.format(format, arguments)); + } + + @Override + public void trace(String msg, Throwable t) { + trace(String.format("%s, %s", msg, Arrays.toString(t.getStackTrace()))); + } + + @Override + public boolean isDebugEnabled() { + return false; + } + + @Override + public void debug(String msg) {} + + @Override + public void debug(String format, Object... arguments) {} + + @Override + public void debug(String msg, Throwable t) {} + + @Override + public boolean isInfoEnabled() { + return false; + } + + @Override + public void info(String msg) {} + + @Override + public void info(String format, Object... arguments) {} + + @Override + public void info(String msg, Throwable t) {} + + @Override + public boolean isWarnEnabled() { + return false; + } + + @Override + public void warn(String msg) {} + + @Override + public void warn(String format, Object... arguments) {} + + @Override + public void warn(String msg, Throwable t) {} + + @Override + public boolean isErrorEnabled() { + return false; + } + + @Override + public void error(String msg) {} + + @Override + public void error(String format, Object... arguments) {} + + @Override + public void error(String msg, Throwable t) {} +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java index 23ada95fe..c96a7aed2 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -32,6 +32,7 @@ import reactor.core.Scannable; import reactor.core.publisher.Flux; import reactor.core.publisher.Operators; +import reactor.util.Logger; import reactor.util.annotation.Nullable; import reactor.util.concurrent.Queues; import reactor.util.context.Context; @@ -51,6 +52,7 @@ public final class UnboundedProcessor extends Flux final Queue queue; final Queue priorityQueue; final Runnable onFinalizedHook; + @Nullable final Logger logger; boolean cancelled; boolean done; @@ -99,10 +101,19 @@ public UnboundedProcessor() { this(() -> {}); } + UnboundedProcessor(Logger logger) { + this(() -> {}, logger); + } + public UnboundedProcessor(Runnable onFinalizedHook) { + this(onFinalizedHook, null); + } + + UnboundedProcessor(Runnable onFinalizedHook, @Nullable Logger logger) { this.onFinalizedHook = onFinalizedHook; this.queue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); this.priorityQueue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); + this.logger = logger; } @Override @@ -153,7 +164,7 @@ public boolean tryEmitPrioritized(ByteBuf t) { } if (hasRequest(previousState)) { - drainRegular(previousState); + drainRegular((previousState | FLAG_HAS_VALUE) + 1); } } return true; @@ -189,7 +200,7 @@ public boolean tryEmitNormal(ByteBuf t) { } if (hasRequest(previousState)) { - drainRegular(previousState); + drainRegular((previousState | FLAG_HAS_VALUE) + 1); } } @@ -223,9 +234,7 @@ public boolean tryEmitFinal(ByteBuf t) { return true; } - if (hasRequest(previousState)) { - drainRegular(previousState); - } + drainRegular((previousState | FLAG_TERMINATED | FLAG_HAS_VALUE) + 1); } return true; @@ -279,9 +288,7 @@ public void onError(Throwable t) { return; } - if (hasRequest(previousState)) { - drainRegular(previousState); - } + drainRegular((previousState | FLAG_TERMINATED) + 1); } } @@ -318,18 +325,15 @@ public void onComplete() { return; } - if (hasRequest(previousState)) { - drainRegular(previousState); - } + drainRegular((previousState | FLAG_TERMINATED) + 1); } } - void drainRegular(long previousState) { + void drainRegular(long expectedState) { final CoreSubscriber a = this.actual; final Queue q = this.queue; final Queue pq = this.priorityQueue; - long expectedState = previousState + 1; for (; ; ) { long r = this.requested; @@ -351,7 +355,7 @@ void drainRegular(long previousState) { empty = t == null; } - if (checkTerminated(done, empty, a)) { + if (checkTerminated(done, empty, true, a)) { if (!empty) { release(t); } @@ -374,7 +378,7 @@ void drainRegular(long previousState) { done = this.done; empty = q.isEmpty() && pq.isEmpty(); - if (checkTerminated(done, empty, a)) { + if (checkTerminated(done, empty, false, a)) { return; } } @@ -401,7 +405,8 @@ void drainRegular(long previousState) { } } - boolean checkTerminated(boolean done, boolean empty, CoreSubscriber a) { + boolean checkTerminated( + boolean done, boolean empty, boolean hasDemand, CoreSubscriber a) { final long state = this.state; if (isCancelled(state)) { clearAndFinalize(this); @@ -415,8 +420,15 @@ boolean checkTerminated(boolean done, boolean empty, CoreSubscriber actual) { previousState = markSubscriberReady(this); + if (isSubscriberReady(previousState)) { + return; + } + if (this.outputFused) { if (isCancelled(previousState)) { return; @@ -523,7 +539,7 @@ public void subscribe(CoreSubscriber actual) { } if (hasRequest(previousState)) { - drainRegular(previousState); + drainRegular((previousState | FLAG_SUBSCRIBER_READY) + 1); } } @@ -549,7 +565,7 @@ public void request(long n) { } if (isSubscriberReady(previousState) && hasValue(previousState)) { - drainRegular(previousState); + drainRegular((previousState | FLAG_HAS_REQUEST) + 1); } } } @@ -727,7 +743,9 @@ static long markSubscribedOnce(UnboundedProcessor instance) { return state; } - if (STATE.compareAndSet(instance, state, state | FLAG_SUBSCRIBED_ONCE)) { + final long nextState = state | FLAG_SUBSCRIBED_ONCE; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mso", state, nextState); return state; } } @@ -743,7 +761,10 @@ static long markSubscriberReady(UnboundedProcessor instance) { for (; ; ) { long state = instance.state; - if (isFinalized(state) || isCancelled(state) || isDisposed(state)) { + if (isFinalized(state) + || isCancelled(state) + || isDisposed(state) + || isSubscriberReady(state)) { return state; } @@ -754,7 +775,9 @@ static long markSubscriberReady(UnboundedProcessor instance) { } } - if (STATE.compareAndSet(instance, state, nextState | FLAG_SUBSCRIBER_READY)) { + nextState = nextState | FLAG_SUBSCRIBER_READY; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " msr", state, nextState); return state; } } @@ -776,11 +799,13 @@ static long markRequestAdded(UnboundedProcessor instance) { } long nextState = state; - if (isSubscriberReady(state) && hasValue(state)) { + if (isWorkInProgress(state) || (isSubscriberReady(state) && hasValue(state))) { nextState = addWork(state); } - if (STATE.compareAndSet(instance, state, nextState | FLAG_HAS_REQUEST)) { + nextState = nextState | FLAG_HAS_REQUEST; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mra", state, nextState); return state; } } @@ -815,7 +840,9 @@ static long markValueAdded(UnboundedProcessor instance) { } } - if (STATE.compareAndSet(instance, state, nextState | FLAG_HAS_VALUE)) { + nextState = nextState | FLAG_HAS_VALUE; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mva", state, nextState); return state; } } @@ -840,12 +867,12 @@ static long markValueAddedAndTerminated(UnboundedProcessor instance) { if (isWorkInProgress(state)) { nextState = addWork(state); } else if (isSubscriberReady(state) && !instance.outputFused) { - if (hasRequest(state)) { - nextState = addWork(state); - } + nextState = addWork(state); } - if (STATE.compareAndSet(instance, state, nextState | FLAG_HAS_VALUE | FLAG_TERMINATED)) { + nextState = nextState | FLAG_HAS_VALUE | FLAG_TERMINATED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, "mva&t", state, nextState); return state; } } @@ -867,16 +894,20 @@ static long markTerminatedOrFinalized(UnboundedProcessor instance) { } long nextState = state; - if (isSubscriberReady(state) && !instance.outputFused) { + if (isWorkInProgress(state)) { + nextState = addWork(state); + } else if (isSubscriberReady(state) && !instance.outputFused) { if (!hasValue(state)) { // fast path for no values and no work in progress nextState = FLAG_FINALIZED; - } else if (hasRequest(state)) { + } else { nextState = addWork(state); } } - if (STATE.compareAndSet(instance, state, nextState | FLAG_TERMINATED)) { + nextState = nextState | FLAG_TERMINATED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mt|f", state, nextState); if (isFinalized(nextState)) { instance.onFinalizedHook.run(); } @@ -899,8 +930,9 @@ static long markCancelled(UnboundedProcessor instance) { return state; } - final long nextState = addWork(state); - if (STATE.compareAndSet(instance, state, nextState | FLAG_CANCELLED)) { + final long nextState = addWork(state) | FLAG_CANCELLED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mc", state, nextState); return state; } } @@ -921,8 +953,9 @@ static long markDisposed(UnboundedProcessor instance) { return state; } - final long nextState = addWork(state); - if (STATE.compareAndSet(instance, state, nextState | FLAG_DISPOSED)) { + final long nextState = addWork(state) | FLAG_DISPOSED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " md", state, nextState); return state; } } @@ -945,12 +978,10 @@ static long addWork(long state) { */ static long markWorkDone( UnboundedProcessor instance, long expectedState, boolean hasRequest, boolean hasValue) { - final long expectedMissed = expectedState & MAX_WIP_VALUE; for (; ; ) { final long state = instance.state; - final long missed = state & MAX_WIP_VALUE; - if (missed != expectedMissed) { + if (state != expectedState) { return state; } @@ -958,11 +989,12 @@ static long markWorkDone( return state; } - final long nextState = state - expectedMissed; - if (STATE.compareAndSet( - instance, - state, - nextState ^ (hasRequest ? 0 : FLAG_HAS_REQUEST) ^ (hasValue ? 0 : FLAG_HAS_VALUE))) { + final long nextState = + (state - (expectedState & MAX_WIP_VALUE)) + ^ (hasRequest ? 0 : FLAG_HAS_REQUEST) + ^ (hasValue ? 0 : FLAG_HAS_VALUE); + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mwd", state, nextState); return nextState; } } @@ -991,8 +1023,9 @@ static void clearAndFinalize(UnboundedProcessor instance) { instance.clearUnsafely(); } - if (STATE.compareAndSet( - instance, state, (state & ~MAX_WIP_VALUE & ~FLAG_HAS_VALUE) | FLAG_FINALIZED)) { + long nextState = (state & ~MAX_WIP_VALUE & ~FLAG_HAS_VALUE) | FLAG_FINALIZED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " c&f", state, nextState); instance.onFinalizedHook.run(); break; } @@ -1034,4 +1067,101 @@ static boolean isSubscriberReady(long state) { static boolean isSubscribedOnce(long state) { return (state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE; } + + static void log( + UnboundedProcessor instance, String action, long initialState, long committedState) { + log(instance, action, initialState, committedState, false); + } + + static void log( + UnboundedProcessor instance, + String action, + long initialState, + long committedState, + boolean logStackTrace) { + Logger logger = instance.logger; + if (logger == null || !logger.isTraceEnabled()) { + return; + } + + if (logStackTrace) { + logger.trace( + String.format( + "[%s][%s][%s][%s-%s]", + instance, + action, + action, + Thread.currentThread().getId(), + formatState(initialState, 64), + formatState(committedState, 64)), + new RuntimeException()); + } else { + logger.trace( + String.format( + "[%s][%s][%s][%s-%s]", + instance, + action, + Thread.currentThread().getId(), + formatState(initialState, 64), + formatState(committedState, 64))); + } + } + + static void log( + UnboundedProcessor instance, String action, int initialState, int committedState) { + log(instance, action, initialState, committedState, false); + } + + static void log( + UnboundedProcessor instance, + String action, + int initialState, + int committedState, + boolean logStackTrace) { + Logger logger = instance.logger; + if (logger == null || !logger.isTraceEnabled()) { + return; + } + + if (logStackTrace) { + logger.trace( + String.format( + "[%s][%s][%s][%s-%s]", + instance, + action, + action, + Thread.currentThread().getId(), + formatState(initialState, 32), + formatState(committedState, 32)), + new RuntimeException()); + } else { + logger.trace( + String.format( + "[%s][%s][%s][%s-%s]", + instance, + action, + Thread.currentThread().getId(), + formatState(initialState, 32), + formatState(committedState, 32))); + } + } + + static String formatState(long state, int size) { + final String defaultFormat = Long.toBinaryString(state); + final StringBuilder formatted = new StringBuilder(); + final int toPrepend = size - defaultFormat.length(); + for (int i = 0; i < size; i++) { + if (i != 0 && i % 4 == 0) { + formatted.append("_"); + } + if (i < toPrepend) { + formatted.append("0"); + } else { + formatted.append(defaultFormat.charAt(i - toPrepend)); + } + } + + formatted.insert(0, "0b"); + return formatted.toString(); + } } From 7abe35ee592843de7cf11f4675615765c690d8a4 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 24 Jan 2025 21:31:24 +0000 Subject: [PATCH 174/183] Fix handling of rejected setup errors (#1117) Closes gh-1092 --- .../java/io/rsocket/core/RSocketServer.java | 12 +++++-- .../rsocket/resume/ServerRSocketSession.java | 3 ++ .../io/rsocket/core/RSocketServerTest.java | 36 ++++++++++++++++++- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index 0c68db6df..e969c39d2 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -463,8 +463,14 @@ private Mono acceptSetup( return interceptors .initSocketAcceptor(acceptor) .accept(setupPayload, wrappedRSocketRequester) - .doOnError( - err -> serverSetup.sendError(wrappedDuplexConnection, rejectedSetupError(err))) + .onErrorResume( + err -> + Mono.fromRunnable( + () -> + serverSetup.sendError( + wrappedDuplexConnection, rejectedSetupError(err))) + .then(wrappedDuplexConnection.onClose()) + .then(Mono.error(err))) .doOnNext( rSocketHandler -> { RSocket wrappedRSocketHandler = interceptors.initResponder(rSocketHandler); diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java index c4dc4d837..ad1b38375 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -287,6 +287,9 @@ public void setKeepAliveSupport(KeepAliveSupport keepAliveSupport) { @Override public void dispose() { + if (logger.isDebugEnabled()) { + logger.debug("Side[server]|Session[{}]. Disposing session", session); + } Operators.terminate(S, this); resumableConnection.dispose(); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java index 0b5ca38f7..a335ac1f3 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2021 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,11 +23,14 @@ import io.rsocket.Closeable; import io.rsocket.FrameAssert; import io.rsocket.RSocket; +import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.frame.FrameType; import io.rsocket.frame.KeepAliveFrameCodec; import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.SetupFrameCodec; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestServerTransport; +import io.rsocket.util.EmptyPayload; import java.time.Duration; import java.util.Random; import org.assertj.core.api.Assertions; @@ -164,4 +167,35 @@ public void unexpectedFramesBeforeSetup() { server.dispose(); transport.alloc().assertHasNoLeaks(); } + + @Test + public void ensuresErrorFrameDeliveredPriorConnectionDisposal() { + TestServerTransport transport = new TestServerTransport(); + Closeable server = + RSocketServer.create() + .acceptor( + (setup, sendingSocket) -> Mono.error(new RejectedSetupException("ACCESS_DENIED"))) + .bind(transport) + .block(); + + TestDuplexConnection connection = transport.connect(); + connection.addToReceivedBuffer( + SetupFrameCodec.encode( + ByteBufAllocator.DEFAULT, + false, + 0, + 1, + Unpooled.EMPTY_BUFFER, + "metadata_type", + "data_type", + EmptyPayload.INSTANCE)); + + StepVerifier.create(connection.onClose()).expectComplete().verify(Duration.ofSeconds(30)); + FrameAssert.assertThat(connection.pollFrame()) + .hasStreamIdZero() + .hasData("ACCESS_DENIED") + .hasNoLeaks(); + server.dispose(); + transport.alloc().assertHasNoLeaks(); + } } From b1dd3c54148daecc50695b6b2687e578fb641c0d Mon Sep 17 00:00:00 2001 From: sullis Date: Fri, 24 Jan 2025 13:31:58 -0800 Subject: [PATCH 175/183] support netty boringssl aarch_64 classifier (#1107) Signed-off-by: sullis --- rsocket-transport-netty/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 17756dbc6..39a5ceac5 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -22,7 +22,7 @@ plugins { } def os_suffix = "" -if (osdetector.classifier in ["linux-x86_64", "osx-x86_64", "windows-x86_64"]) { +if (osdetector.classifier in ["linux-x86_64", "linux-aarch_64", "osx-x86_64", "osx-aarch_64", "windows-x86_64"]) { os_suffix = "::" + osdetector.classifier } From 50c51c43c5963258af3fbc7562fb9bf2083782f2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 24 Jan 2025 21:31:24 +0000 Subject: [PATCH 176/183] Fix handling of rejected setup errors (#1117) Closes gh-1092 --- .../java/io/rsocket/core/RSocketServer.java | 12 +++++-- .../rsocket/resume/ServerRSocketSession.java | 3 ++ .../io/rsocket/core/RSocketServerTest.java | 36 ++++++++++++++++++- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index 0c68db6df..e969c39d2 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -463,8 +463,14 @@ private Mono acceptSetup( return interceptors .initSocketAcceptor(acceptor) .accept(setupPayload, wrappedRSocketRequester) - .doOnError( - err -> serverSetup.sendError(wrappedDuplexConnection, rejectedSetupError(err))) + .onErrorResume( + err -> + Mono.fromRunnable( + () -> + serverSetup.sendError( + wrappedDuplexConnection, rejectedSetupError(err))) + .then(wrappedDuplexConnection.onClose()) + .then(Mono.error(err))) .doOnNext( rSocketHandler -> { RSocket wrappedRSocketHandler = interceptors.initResponder(rSocketHandler); diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java index c4dc4d837..ad1b38375 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -287,6 +287,9 @@ public void setKeepAliveSupport(KeepAliveSupport keepAliveSupport) { @Override public void dispose() { + if (logger.isDebugEnabled()) { + logger.debug("Side[server]|Session[{}]. Disposing session", session); + } Operators.terminate(S, this); resumableConnection.dispose(); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java index 0b5ca38f7..a335ac1f3 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2021 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,11 +23,14 @@ import io.rsocket.Closeable; import io.rsocket.FrameAssert; import io.rsocket.RSocket; +import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.frame.FrameType; import io.rsocket.frame.KeepAliveFrameCodec; import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.SetupFrameCodec; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestServerTransport; +import io.rsocket.util.EmptyPayload; import java.time.Duration; import java.util.Random; import org.assertj.core.api.Assertions; @@ -164,4 +167,35 @@ public void unexpectedFramesBeforeSetup() { server.dispose(); transport.alloc().assertHasNoLeaks(); } + + @Test + public void ensuresErrorFrameDeliveredPriorConnectionDisposal() { + TestServerTransport transport = new TestServerTransport(); + Closeable server = + RSocketServer.create() + .acceptor( + (setup, sendingSocket) -> Mono.error(new RejectedSetupException("ACCESS_DENIED"))) + .bind(transport) + .block(); + + TestDuplexConnection connection = transport.connect(); + connection.addToReceivedBuffer( + SetupFrameCodec.encode( + ByteBufAllocator.DEFAULT, + false, + 0, + 1, + Unpooled.EMPTY_BUFFER, + "metadata_type", + "data_type", + EmptyPayload.INSTANCE)); + + StepVerifier.create(connection.onClose()).expectComplete().verify(Duration.ofSeconds(30)); + FrameAssert.assertThat(connection.pollFrame()) + .hasStreamIdZero() + .hasData("ACCESS_DENIED") + .hasNoLeaks(); + server.dispose(); + transport.alloc().assertHasNoLeaks(); + } } From a5fbd96a815b1ef07333df61256d4756227a3642 Mon Sep 17 00:00:00 2001 From: sullis Date: Fri, 24 Jan 2025 13:31:58 -0800 Subject: [PATCH 177/183] support netty boringssl aarch_64 classifier (#1107) Signed-off-by: sullis --- rsocket-transport-netty/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 17756dbc6..39a5ceac5 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -22,7 +22,7 @@ plugins { } def os_suffix = "" -if (osdetector.classifier in ["linux-x86_64", "osx-x86_64", "windows-x86_64"]) { +if (osdetector.classifier in ["linux-x86_64", "linux-aarch_64", "osx-x86_64", "osx-aarch_64", "windows-x86_64"]) { os_suffix = "::" + osdetector.classifier } From 9bc30c44d601fa1e5d937901df979e68a0227fc1 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 28 Jan 2025 11:29:28 +0000 Subject: [PATCH 178/183] Update Logback version to 1.2.13 It must stay < 1.3 to work with slf4j 1.7. Signed-off-by: rstoyanchev --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ae8f05d06..ccf03c4eb 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ subprojects { apply plugin: 'com.github.vlsi.gradle-extensions' ext['reactor-bom.version'] = '2020.0.39' - ext['logback.version'] = '1.3.14' + ext['logback.version'] = '1.2.13' ext['netty-bom.version'] = '4.1.106.Final' ext['netty-boringssl.version'] = '2.0.62.Final' ext['hdrhistogram.version'] = '2.1.12' From ccd67ba20d8a0c242901a180c8369a6315a6b626 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:41:38 +0200 Subject: [PATCH 179/183] ensures connection is closed on keepalive timeout (#1118) * ensures connection is close on keepalive timeout Signed-off-by: Oleh Dokuka * fix format Signed-off-by: Oleh Dokuka * improve KeepaliveTest Signed-off-by: Oleh Dokuka * fix format and failing test Signed-off-by: Oleh Dokuka * adds reference to the original GH issue Signed-off-by: Oleh Dokuka * fixes google format Signed-off-by: Oleh Dokuka --------- Signed-off-by: Oleh Dokuka --- .../io/rsocket/core/RSocketRequester.java | 1 + .../io/rsocket/integration/KeepaliveTest.java | 190 ++++++++++++++++++ .../src/test/resources/logback-test.xml | 1 - 3 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 9e8d349bf..b8a9c00ff 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -312,6 +312,7 @@ private void tryTerminateOnKeepAlive(KeepAliveSupport.KeepAlive keepAlive) { () -> new ConnectionErrorException( String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()))); + getDuplexConnection().dispose(); } private void tryShutdown(Throwable e) { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java new file mode 100644 index 000000000..f05713215 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java @@ -0,0 +1,190 @@ +package io.rsocket.integration; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketClient; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.tcp.TcpClient; +import reactor.netty.tcp.TcpServer; +import reactor.test.StepVerifier; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; + +/** + * Test case that reproduces the following GitHub Issue + */ +public class KeepaliveTest { + + private static final Logger LOG = LoggerFactory.getLogger(KeepaliveTest.class); + private static final int PORT = 23200; + + private CloseableChannel server; + + @BeforeEach + void setUp() { + server = createServer().block(); + } + + @AfterEach + void tearDown() { + server.dispose(); + server.onClose().block(); + } + + @Test + void keepAliveTest() { + RSocketClient rsocketClient = createClient(); + + int expectedCount = 4; + AtomicBoolean sleepOnce = new AtomicBoolean(true); + StepVerifier.create( + Flux.range(0, expectedCount) + .delayElements(Duration.ofMillis(2000)) + .concatMap( + i -> + rsocketClient + .requestResponse(Mono.just(DefaultPayload.create(""))) + .doOnNext( + __ -> { + if (sleepOnce.getAndSet(false)) { + try { + LOG.info("Sleeping..."); + Thread.sleep(1_000); + LOG.info("Waking up."); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }) + .log("id " + i) + .onErrorComplete())) + .expectSubscription() + .expectNextCount(expectedCount) + .verifyComplete(); + } + + @Test + void keepAliveTestLazy() { + Mono rsocketMono = createClientLazy(); + + int expectedCount = 4; + AtomicBoolean sleepOnce = new AtomicBoolean(true); + StepVerifier.create( + Flux.range(0, expectedCount) + .delayElements(Duration.ofMillis(2000)) + .concatMap( + i -> + rsocketMono.flatMap( + rsocket -> + rsocket + .requestResponse(DefaultPayload.create("")) + .doOnNext( + __ -> { + if (sleepOnce.getAndSet(false)) { + try { + LOG.info("Sleeping..."); + Thread.sleep(1_000); + LOG.info("Waking up."); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }) + .log("id " + i) + .onErrorComplete()))) + .expectSubscription() + .expectNextCount(expectedCount) + .verifyComplete(); + } + + private static Mono createServer() { + LOG.info("Starting server at port {}", PORT); + + TcpServer tcpServer = TcpServer.create().host("localhost").port(PORT); + + return RSocketServer.create( + (setupPayload, rSocket) -> { + rSocket + .onClose() + .doFirst(() -> LOG.info("Connected on server side.")) + .doOnTerminate(() -> LOG.info("Connection closed on server side.")) + .subscribe(); + + return Mono.just(new MyServerRsocket()); + }) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(TcpServerTransport.create(tcpServer)) + .doOnNext(closeableChannel -> LOG.info("RSocket server started.")); + } + + private static RSocketClient createClient() { + LOG.info("Connecting...."); + + Function reconnectSpec = + reason -> + Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(10L)) + .doBeforeRetry(retrySignal -> LOG.info("Reconnecting. Reason: {}", reason)); + + Mono rsocketMono = + RSocketConnector.create() + .fragment(16384) + .reconnect(reconnectSpec.apply("connector-close")) + .keepAlive(Duration.ofMillis(100L), Duration.ofMillis(900L)) + .connect(TcpClientTransport.create(TcpClient.create().host("localhost").port(PORT))); + + RSocketClient client = RSocketClient.from(rsocketMono); + + client + .source() + .doOnNext(r -> LOG.info("Got RSocket")) + .flatMap(RSocket::onClose) + .doOnError(err -> LOG.error("Error during onClose.", err)) + .retryWhen(reconnectSpec.apply("client-close")) + .doFirst(() -> LOG.info("Connected on client side.")) + .doOnTerminate(() -> LOG.info("Connection closed on client side.")) + .repeat() + .subscribe(); + + return client; + } + + private static Mono createClientLazy() { + LOG.info("Connecting...."); + + Function reconnectSpec = + reason -> + Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(10L)) + .doBeforeRetry(retrySignal -> LOG.info("Reconnecting. Reason: {}", reason)); + + return RSocketConnector.create() + .fragment(16384) + .reconnect(reconnectSpec.apply("connector-close")) + .keepAlive(Duration.ofMillis(100L), Duration.ofMillis(900L)) + .connect(TcpClientTransport.create(TcpClient.create().host("localhost").port(PORT))); + } + + public static class MyServerRsocket implements RSocket { + + @Override + public Mono requestResponse(Payload payload) { + return Mono.just("Pong").map(DefaultPayload::create); + } + } +} diff --git a/rsocket-transport-netty/src/test/resources/logback-test.xml b/rsocket-transport-netty/src/test/resources/logback-test.xml index b42db6df6..981d6d0b6 100644 --- a/rsocket-transport-netty/src/test/resources/logback-test.xml +++ b/rsocket-transport-netty/src/test/resources/logback-test.xml @@ -27,7 +27,6 @@ - From cff5cdbb16da6393efc04d8f0b80793e54f79026 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 31 Jan 2025 11:53:11 +0000 Subject: [PATCH 180/183] Log data in KEEPALIVE frame Fixes gh-1114 Signed-off-by: rstoyanchev --- .../main/java/io/rsocket/frame/FrameUtil.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java index 66d18c8a7..d581731a3 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.frame; import io.netty.buffer.ByteBuf; @@ -99,8 +114,9 @@ private static ByteBuf getData(ByteBuf frame, FrameType frameType) { case REQUEST_CHANNEL: data = RequestChannelFrameCodec.data(frame); break; - // Payload and synthetic types + // Payload, KeepAlive and synthetic types case PAYLOAD: + case KEEPALIVE: case NEXT: case NEXT_COMPLETE: case COMPLETE: From 838e8fbfa2c45d72fa323c7e82e53490cc0d3d8b Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:41:38 +0200 Subject: [PATCH 181/183] ensures connection is closed on keepalive timeout (#1118) * ensures connection is close on keepalive timeout Signed-off-by: Oleh Dokuka * fix format Signed-off-by: Oleh Dokuka * improve KeepaliveTest Signed-off-by: Oleh Dokuka * fix format and failing test Signed-off-by: Oleh Dokuka * adds reference to the original GH issue Signed-off-by: Oleh Dokuka * fixes google format Signed-off-by: Oleh Dokuka --------- Signed-off-by: Oleh Dokuka --- .../io/rsocket/core/RSocketRequester.java | 1 + .../io/rsocket/integration/KeepaliveTest.java | 190 ++++++++++++++++++ .../src/test/resources/logback-test.xml | 1 - 3 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 9e8d349bf..b8a9c00ff 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -312,6 +312,7 @@ private void tryTerminateOnKeepAlive(KeepAliveSupport.KeepAlive keepAlive) { () -> new ConnectionErrorException( String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()))); + getDuplexConnection().dispose(); } private void tryShutdown(Throwable e) { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java new file mode 100644 index 000000000..f05713215 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java @@ -0,0 +1,190 @@ +package io.rsocket.integration; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketClient; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.tcp.TcpClient; +import reactor.netty.tcp.TcpServer; +import reactor.test.StepVerifier; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; + +/** + * Test case that reproduces the following GitHub Issue + */ +public class KeepaliveTest { + + private static final Logger LOG = LoggerFactory.getLogger(KeepaliveTest.class); + private static final int PORT = 23200; + + private CloseableChannel server; + + @BeforeEach + void setUp() { + server = createServer().block(); + } + + @AfterEach + void tearDown() { + server.dispose(); + server.onClose().block(); + } + + @Test + void keepAliveTest() { + RSocketClient rsocketClient = createClient(); + + int expectedCount = 4; + AtomicBoolean sleepOnce = new AtomicBoolean(true); + StepVerifier.create( + Flux.range(0, expectedCount) + .delayElements(Duration.ofMillis(2000)) + .concatMap( + i -> + rsocketClient + .requestResponse(Mono.just(DefaultPayload.create(""))) + .doOnNext( + __ -> { + if (sleepOnce.getAndSet(false)) { + try { + LOG.info("Sleeping..."); + Thread.sleep(1_000); + LOG.info("Waking up."); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }) + .log("id " + i) + .onErrorComplete())) + .expectSubscription() + .expectNextCount(expectedCount) + .verifyComplete(); + } + + @Test + void keepAliveTestLazy() { + Mono rsocketMono = createClientLazy(); + + int expectedCount = 4; + AtomicBoolean sleepOnce = new AtomicBoolean(true); + StepVerifier.create( + Flux.range(0, expectedCount) + .delayElements(Duration.ofMillis(2000)) + .concatMap( + i -> + rsocketMono.flatMap( + rsocket -> + rsocket + .requestResponse(DefaultPayload.create("")) + .doOnNext( + __ -> { + if (sleepOnce.getAndSet(false)) { + try { + LOG.info("Sleeping..."); + Thread.sleep(1_000); + LOG.info("Waking up."); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }) + .log("id " + i) + .onErrorComplete()))) + .expectSubscription() + .expectNextCount(expectedCount) + .verifyComplete(); + } + + private static Mono createServer() { + LOG.info("Starting server at port {}", PORT); + + TcpServer tcpServer = TcpServer.create().host("localhost").port(PORT); + + return RSocketServer.create( + (setupPayload, rSocket) -> { + rSocket + .onClose() + .doFirst(() -> LOG.info("Connected on server side.")) + .doOnTerminate(() -> LOG.info("Connection closed on server side.")) + .subscribe(); + + return Mono.just(new MyServerRsocket()); + }) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(TcpServerTransport.create(tcpServer)) + .doOnNext(closeableChannel -> LOG.info("RSocket server started.")); + } + + private static RSocketClient createClient() { + LOG.info("Connecting...."); + + Function reconnectSpec = + reason -> + Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(10L)) + .doBeforeRetry(retrySignal -> LOG.info("Reconnecting. Reason: {}", reason)); + + Mono rsocketMono = + RSocketConnector.create() + .fragment(16384) + .reconnect(reconnectSpec.apply("connector-close")) + .keepAlive(Duration.ofMillis(100L), Duration.ofMillis(900L)) + .connect(TcpClientTransport.create(TcpClient.create().host("localhost").port(PORT))); + + RSocketClient client = RSocketClient.from(rsocketMono); + + client + .source() + .doOnNext(r -> LOG.info("Got RSocket")) + .flatMap(RSocket::onClose) + .doOnError(err -> LOG.error("Error during onClose.", err)) + .retryWhen(reconnectSpec.apply("client-close")) + .doFirst(() -> LOG.info("Connected on client side.")) + .doOnTerminate(() -> LOG.info("Connection closed on client side.")) + .repeat() + .subscribe(); + + return client; + } + + private static Mono createClientLazy() { + LOG.info("Connecting...."); + + Function reconnectSpec = + reason -> + Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(10L)) + .doBeforeRetry(retrySignal -> LOG.info("Reconnecting. Reason: {}", reason)); + + return RSocketConnector.create() + .fragment(16384) + .reconnect(reconnectSpec.apply("connector-close")) + .keepAlive(Duration.ofMillis(100L), Duration.ofMillis(900L)) + .connect(TcpClientTransport.create(TcpClient.create().host("localhost").port(PORT))); + } + + public static class MyServerRsocket implements RSocket { + + @Override + public Mono requestResponse(Payload payload) { + return Mono.just("Pong").map(DefaultPayload::create); + } + } +} diff --git a/rsocket-transport-netty/src/test/resources/logback-test.xml b/rsocket-transport-netty/src/test/resources/logback-test.xml index b42db6df6..981d6d0b6 100644 --- a/rsocket-transport-netty/src/test/resources/logback-test.xml +++ b/rsocket-transport-netty/src/test/resources/logback-test.xml @@ -27,7 +27,6 @@ - From d28d093aaef21bca01d8de12c2fb9be8492ed982 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 31 Jan 2025 11:53:11 +0000 Subject: [PATCH 182/183] Log data in KEEPALIVE frame Fixes gh-1114 Signed-off-by: rstoyanchev --- .../main/java/io/rsocket/frame/FrameUtil.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java index 66d18c8a7..d581731a3 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.frame; import io.netty.buffer.ByteBuf; @@ -99,8 +114,9 @@ private static ByteBuf getData(ByteBuf frame, FrameType frameType) { case REQUEST_CHANNEL: data = RequestChannelFrameCodec.data(frame); break; - // Payload and synthetic types + // Payload, KeepAlive and synthetic types case PAYLOAD: + case KEEPALIVE: case NEXT: case NEXT_COMPLETE: case COMPLETE: From 6e725d6d94f15de87ad7d67c8cc8e3e422e72acd Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 31 Jan 2025 13:58:46 +0000 Subject: [PATCH 183/183] Dependency upgrades Reactor 2020.0.39 -> 2020.0.47 Netty 4.1.106.Final -> 4.1.117.Final netty-tcnative-boringssl-static 2.0.62.Final -> 2.0.69.Final Micrometer 1.11.0 -> 1.11.12 Micrometer Tracing 1.1.1 -> 1.1.13 Signed-off-by: rstoyanchev --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index ccf03c4eb..b2122e4a5 100644 --- a/build.gradle +++ b/build.gradle @@ -33,17 +33,17 @@ subprojects { apply plugin: 'com.github.sherter.google-java-format' apply plugin: 'com.github.vlsi.gradle-extensions' - ext['reactor-bom.version'] = '2020.0.39' + ext['reactor-bom.version'] = '2020.0.47' ext['logback.version'] = '1.2.13' - ext['netty-bom.version'] = '4.1.106.Final' - ext['netty-boringssl.version'] = '2.0.62.Final' + ext['netty-bom.version'] = '4.1.117.Final' + ext['netty-boringssl.version'] = '2.0.69.Final' ext['hdrhistogram.version'] = '2.1.12' ext['mockito.version'] = '4.11.0' ext['slf4j.version'] = '1.7.36' ext['jmh.version'] = '1.36' ext['junit.version'] = '5.9.3' - ext['micrometer.version'] = '1.11.0' - ext['micrometer-tracing.version'] = '1.1.1' + ext['micrometer.version'] = '1.11.12' + ext['micrometer-tracing.version'] = '1.1.13' ext['assertj.version'] = '3.24.2' ext['netflix.limits.version'] = '0.3.6' ext['bouncycastle-bcpkix.version'] = '1.70'