diff --git a/.github/workflows/gradle-all.yml b/.github/workflows/gradle-all.yml index 8540539bb..abbd14106 100644 --- a/.github/workflows/gradle-all.yml +++ b/.github/workflows/gradle-all.yml @@ -5,18 +5,71 @@ on: # but only for the non master/1.0.x branches push: branches-ignore: - - 1.0.x + - 1.1.x - master jobs: build: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 17 ] + 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, 17 ] + 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, 17 ] fail-fast: false steps: @@ -34,12 +87,66 @@ 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, 17 ] + 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, 17 ] + 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: | + githubRef="${githubRef#refs/heads/}" + githubRef="${githubRef////-}" + ./gradlew -PversionSuffix="-${githubRef}-SNAPSHOT" -PbuildNumber="${buildNumber}" publishMavenPublicationToGitHubPackagesRepository --no-daemon --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..33bca8e72 100644 --- a/.github/workflows/gradle-main.yml +++ b/.github/workflows/gradle-main.yml @@ -2,21 +2,74 @@ 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: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 17 ] + 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, 17 ] + 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, 17 ] fail-fast: false steps: @@ -34,14 +87,69 @@ 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, 17 ] + 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, 17 ] + 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 -PversionSuffix="-SNAPSHOT" -PbuildNumber="${buildNumber}" publishMavenPublicationToSonatypeRepository --no-daemon --stacktrace env: - bintrayUser: ${{ secrets.bintrayUser }} - bintrayKey: ${{ secrets.bintrayKey }} + 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/.github/workflows/gradle-pr.yml b/.github/workflows/gradle-pr.yml index 994450faf..cecca085f 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, 17 ] + 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, 17 ] + 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, 17 ] + 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, 17 ] 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/.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/README.md b/README.md index 085a9bd5e..7ed3244b8 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Learn more at http://rsocket.io [![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. +⚠️ The `master` branch is now dedicated to development of the `1.2.x` line. Releases and milestones are available via Maven Central. @@ -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' - implementation 'io.rsocket:rsocket-transport-netty:1.1.0' + implementation 'io.rsocket:rsocket-core:1.2.0-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-netty:1.2.0-SNAPSHOT' } ``` @@ -40,12 +40,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.1.1-SNAPSHOT' - implementation 'io.rsocket:rsocket-transport-netty:1.1.1-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:1.2.0-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-netty:1.2.0-SNAPSHOT' } ``` 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') diff --git a/build.gradle b/build.gradle index f665350c3..2971a7767 100644 --- a/build.gradle +++ b/build.gradle @@ -15,12 +15,12 @@ */ 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 'me.champeau.gradle.jmh' version '0.5.0' apply false - id 'io.spring.dependency-management' version '1.0.9.RELEASE' apply false + id 'com.github.sherter.google-java-format' version '0.9' apply false + id 'me.champeau.jmh' version '0.7.1' apply false + id 'io.spring.dependency-management' version '1.1.0' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false + id 'io.github.reyerizo.gradle.jcstress' version '0.8.15' apply false + id 'com.github.vlsi.gradle-extensions' version '1.89' apply false } boolean isCiServer = ["CI", "CONTINUOUS_INTEGRATION", "TRAVIS", "CIRCLECI", "bamboo_planKey", "GITHUB_ACTION"].with { @@ -31,20 +31,23 @@ boolean isCiServer = ["CI", "CONTINUOUS_INTEGRATION", "TRAVIS", "CIRCLECI", "bam subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - - 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' - 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['hamcrest.version'] = '1.3' - ext['micrometer.version'] = '1.0.6' - ext['assertj.version'] = '3.11.1' + apply plugin: 'com.github.vlsi.gradle-extensions' + + ext['reactor-bom.version'] = '2022.0.7-SNAPSHOT' + ext['logback.version'] = '1.2.13' + 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.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' + ext['awaitility.version'] = '4.2.0' group = "io.rsocket" @@ -67,23 +70,23 @@ 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 { 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']}" + dependency "org.bouncycastle:bcpkix-jdk15on:${ext['bouncycastle-bcpkix.version']}" 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' } - // 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' entry 'jmh-generator-annprocess' @@ -100,14 +103,17 @@ subprojects { maven { url 'https://repo.spring.io/milestone' content { + includeGroup "io.micrometer" includeGroup "io.projectreactor" includeGroup "io.projectreactor.netty" + includeGroup "io.micrometer" } } maven { url 'https://repo.spring.io/snapshot' content { + includeGroup "io.micrometer" includeGroup "io.projectreactor" includeGroup "io.projectreactor.netty" } @@ -116,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() } } @@ -124,6 +131,7 @@ subprojects { } plugins.withType(JavaPlugin) { + compileJava { sourceCompatibility = 1.8 @@ -152,8 +160,9 @@ subprojects { test { useJUnitPlatform() testLogging { - events "FAILED" + events "PASSED", "FAILED" showExceptions true + showCauses true exceptionFormat "FULL" stackTraceFilters "ENTRY_POINT" maxGranularity 3 @@ -196,7 +205,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") @@ -251,4 +260,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 e6cf7ec55..d138852c5 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 -perfBaselineVersion=1.1.0 +version=1.2.0 +perfBaselineVersion=1.1.4 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/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..f53413766 --- /dev/null +++ b/gradle/github-pkg.gradle @@ -0,0 +1,21 @@ +subprojects { + + 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" } + } + } +} \ No newline at end of file diff --git a/gradle/publications.gradle b/gradle/publications.gradle index 51c1d57fa..9e8dd6d88 100644 --- a/gradle/publications.gradle +++ b/gradle/publications.gradle @@ -1,5 +1,5 @@ -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) { @@ -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 new file mode 100644 index 000000000..f339079b0 --- /dev/null +++ b/gradle/sonotype.gradle @@ -0,0 +1,36 @@ +subprojects { + if (project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword')) { + 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 = 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") + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf01..249e5832f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8f6e03af5..774fae876 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.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 83f2acfdc..a69d9cb6c 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,78 +17,113 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - 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 + # Determine the Java command to use to start the JVM. 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 @@ -97,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 @@ -105,84 +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=$((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 -# 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" +# 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. +# -# 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 +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 24467a141..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,10 +25,13 @@ 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% +@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% equ 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,38 +64,26 @@ 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 -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-bom/build.gradle b/rsocket-bom/build.gradle index 2efc20a91..a75ab3bc8 100755 --- a/rsocket-bom/build.gradle +++ b/rsocket-bom/build.gradle @@ -16,8 +16,7 @@ plugins { id 'java-platform' id 'maven-publish' - id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' + id 'signing' } description = 'RSocket Java Bill of materials.' diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index 53a896aea..da5b69b14 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. @@ -17,10 +17,10 @@ plugins { id 'java-library' id 'maven-publish' - id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' + id 'signing' id 'io.morethan.jmhreport' - id 'me.champeau.gradle.jmh' + id 'me.champeau.jmh' + id 'io.github.reyerizo.gradle.jcstress' } dependencies { @@ -34,15 +34,27 @@ 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' 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(project(":rsocket-test")) + jcstressImplementation 'org.slf4j:slf4j-api' + jcstressImplementation "ch.qos.logback:logback-classic" + jcstressImplementation 'io.projectreactor:reactor-test' } -description = "Core functionality for the RSocket library" \ No newline at end of file +jcstress { + mode = 'sanity' //sanity, quick, default, tough + jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.16" +} + +jar { + manifest { + attributes("Automatic-Module-Name": "rsocket.core") + } +} + +description = "Core functionality for the RSocket library" 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/ReconnectMonoStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java new file mode 100644 index 000000000..ef79d344d --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/ReconnectMonoStressTest.java @@ -0,0 +1,604 @@ +/* + * 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 SubscribeBlockConnectRace 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); + } + + @Actor + void connect() { + reconnectMono.resolvingInner.connect(); + } + + @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/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(); + } + } +} 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/StressSubscriber.java b/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscriber.java new file mode 100644 index 000000000..883077f77 --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/core/StressSubscriber.java @@ -0,0 +1,472 @@ +/* + * 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 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; + +public class StressSubscriber implements CoreSubscriber { + + enum Operation { + ON_NEXT, + ON_ERROR, + ON_COMPLETE, + ON_SUBSCRIBE + } + + final Context context; + final int requestedFusionMode; + + 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") + 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; + + public BlockingQueue q = new LinkedBlockingDeque<>(); + + @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, 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) { + 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)), + (__) -> ON_NEXT_DISCARDED.incrementAndGet(this)); + REQUESTED.lazySet(this, initRequest | Long.MIN_VALUE); + } + + @Override + public Context currentContext() { + return this.context; + } + + @Override + public void onSubscribe(Subscription subscription) { + if (!GUARD.compareAndSet(this, null, Operation.ON_SUBSCRIBE)) { + concurrentOnSubscribe = true; + subscription.cancel(); + } else { + final boolean isValid = Operators.validate(this.subscription, subscription); + if (isValid) { + this.subscription = subscription; + } + GUARD.compareAndSet(this, Operation.ON_SUBSCRIBE, null); + + 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; + } + } + } + } + ON_SUBSCRIBE_CALLS.incrementAndGet(this); + } + + @Override + public void onNext(T value) { + if (fusionMode == Fuseable.ASYNC) { + drain(); + return; + } + + 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); + } + + if (done) { + throw new IllegalStateException("Already done"); + } + + error = throwable; + done = true; + q.offer(throwable); + ON_ERROR_CALLS.incrementAndGet(this); + + if (fusionMode == Fuseable.ASYNC) { + drain(); + } + } + + @Override + public void onComplete() { + if (!GUARD.compareAndSet(this, null, Operation.ON_COMPLETE)) { + concurrentOnComplete = true; + } else { + GUARD.compareAndSet(this, Operation.ON_COMPLETE, null); + } + if (done) { + throw new IllegalStateException("Already done"); + } + + done = true; + ON_COMPLETE_CALLS.incrementAndGet(this); + + if (fusionMode == Fuseable.ASYNC) { + drain(); + } + } + + public void request(long n) { + if (Operators.validate(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() { + 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; + } +} 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..3b51b8ef6 --- /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("rawtype s") + 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/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/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java new file mode 100644 index 000000000..a2d9fcf4d --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java @@ -0,0 +1,1733 @@ +package io.rsocket.internal; + +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; +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; +import reactor.core.publisher.Hooks; +import reactor.util.Logger; + +public abstract class UnboundedProcessorStressTest { + + static { + Hooks.onErrorDropped(t -> {}); + } + + final Logger logger = new FastLogger(getClass().getName()); + + final UnboundedProcessor unboundedProcessor = new UnboundedProcessor(logger); + + @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); + } + + @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(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @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); + } + + @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(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @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); + } + + @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(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @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); + } + + @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(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @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(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @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(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @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(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @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; + + 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); + } + } + + @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(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @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 = { + "-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(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @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(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @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); + } + + @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(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @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); + } + + @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(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @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(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @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(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @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; + + 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/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/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/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 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/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/DefaultRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java index 4dc250158..82a02268d 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; @@ -45,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 + } } } }; @@ -65,6 +69,8 @@ class DefaultRSocketClient extends ResolvingOperator final Mono source; + final Sinks.Empty onDisposeSink; + volatile Subscription s; static final AtomicReferenceFieldUpdater S = @@ -72,12 +78,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 +206,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 { @@ -435,8 +453,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/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/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; } } 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/main/java/io/rsocket/core/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketClient.java index 81392e661..32e3c229d 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,22 @@ * @since 1.1 * @see io.rsocket.loadbalance.LoadbalanceRSocketClient */ -public interface RSocketClient extends Disposable { +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()); + } /** 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..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,11 +41,21 @@ 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); } + @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/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index fe91cdb6f..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; @@ -590,7 +591,7 @@ public Mono connect(Supplier transportSupplier) { dataMimeType, setupPayload); - sourceConnection.sendFrame(0, setupFrame.retain()); + sourceConnection.sendFrame(0, setupFrame.retainedSlice()); return clientSetup .init(sourceConnection) @@ -612,6 +613,7 @@ public Mono connect(Supplier transportSupplier) { final ResumableDuplexConnection resumableDuplexConnection = new ResumableDuplexConnection( CLIENT_TAG, + resumeToken, clientServerConnection, resumableFramesStore); final ResumableClientSetup resumableClientSetup = @@ -632,8 +634,7 @@ public Mono connect(Supplier transportSupplier) { wrappedConnection = resumableDuplexConnection; } else { keepAliveHandler = - new KeepAliveHandler.DefaultKeepAliveHandler( - clientServerConnection); + new KeepAliveHandler.DefaultKeepAliveHandler(); wrappedConnection = clientServerConnection; } @@ -654,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(), @@ -666,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); @@ -714,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 89c1500bb..b8a9c00ff 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; @@ -34,6 +35,8 @@ import io.rsocket.keepalive.KeepAliveSupport; import io.rsocket.plugins.RequestInterceptor; import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.Collection; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Function; import java.util.function.Supplier; @@ -42,7 +45,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; /** @@ -63,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 MonoProcessor onClose; RSocketRequester( DuplexConnection connection, @@ -77,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, @@ -88,10 +95,11 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { requestInterceptorFunction); this.requesterLeaseTracker = requesterLeaseTracker; - this.onClose = MonoProcessor.create(); + 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 -> {}); @@ -185,7 +193,11 @@ public double availability() { @Override public void dispose() { - tryShutdown(); + if (terminationError != null) { + return; + } + + getDuplexConnection().sendErrorAndClose(new ConnectionErrorException("Disposed")); } @Override @@ -195,7 +207,7 @@ public boolean isDisposed() { @Override public Mono onClose() { - return onClose; + return onAllClosed; } private void handleIncomingFrames(ByteBuf frame) { @@ -300,10 +312,34 @@ private void tryTerminateOnKeepAlive(KeepAliveSupport.KeepAlive keepAlive) { () -> new ConnectionErrorException( String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()))); + getDuplexConnection().dispose(); } - 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) { @@ -311,27 +347,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(); @@ -342,22 +418,28 @@ private void terminate(Throwable e) { requesterLeaseTracker.dispose(e); } + final Collection activeStreamsCopy; synchronized (this) { - activeStreams - .values() - .forEach( - receiver -> { - try { - receiver.handleError(e); - } catch (Throwable ignored) { - } - }); + final IntObjectMap activeStreams = this.activeStreams; + activeStreamsCopy = new ArrayList<>(activeStreams.values()); + } + + for (FrameHandler handler : activeStreamsCopy) { + if (handler != null) { + try { + handler.handleError(e); + } catch (Throwable ignored) { + } + } } if (e == CLOSED_CHANNEL_EXCEPTION) { - onClose.onComplete(); + onThisSideClosedSink.tryEmitEmpty(); } else { - onClose.onError(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 f0a052b93..50c5ba54c 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; @@ -32,6 +33,8 @@ import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.plugins.RequestInterceptor; 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.Function; @@ -41,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} */ @@ -51,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; @@ -67,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, @@ -80,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); } @@ -166,6 +180,9 @@ public Mono onClose() { } final void doOnDispose() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("closing responder " + getDuplexConnection()); + } cleanUpSendingSubscriptions(); getDuplexConnection().dispose(); @@ -180,11 +197,24 @@ final void doOnDispose() { } requestHandler.dispose(); + onThisSideClosedSink.tryEmitEmpty(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("responder closed " + getDuplexConnection()); + } } - private synchronized void cleanUpSendingSubscriptions() { - activeStreams.values().forEach(FrameHandler::handleCancel); - activeStreams.clear(); + 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(); + } + } } final void handleFrame(ByteBuf frame) { 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..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. @@ -41,10 +41,12 @@ 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; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; /** * The main class for starting an RSocket server. @@ -70,6 +72,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 +226,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 +307,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 +346,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) { @@ -418,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(), @@ -430,15 +453,24 @@ private Mono acceptSetup( setupPayload.keepAliveMaxLifetime(), keepAliveHandler, interceptors::initRequesterRequestInterceptor, - requesterLeaseTracker); + requesterLeaseTracker, + requesterOnAllClosedSink, + Mono.whenDelayError( + responderOnAllClosedSink.asMono(), requesterOnAllClosedSink.asMono())); RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); 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); @@ -462,19 +494,21 @@ private Mono acceptSetup( ? rSocket -> interceptors.initResponderRequestInterceptor( rSocket, (RequestInterceptor) leases.sender) - : interceptors::initResponderRequestInterceptor); + : interceptors::initResponderRequestInterceptor, + responderOnAllClosedSink); }) .doFinally(signalType -> setupPayload.release()) .then(); }); } - 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/RequestChannelRequesterFlux.java b/rsocket-core/src/main/java/io/rsocket/core/RequestChannelRequesterFlux.java index eee1346eb..aab491793 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, @@ -85,6 +86,8 @@ final class RequestChannelRequesterFlux extends Flux Context cachedContext; CoreSubscriber inboundSubscriber; boolean inboundDone; + long requested; + long produced; CompositeByteBuf frames; @@ -137,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; @@ -705,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); } } @@ -763,7 +789,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/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..6182ca506 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); @@ -234,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); } @@ -272,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); @@ -295,6 +300,35 @@ 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; + + 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); + } + + this.inboundSubscriber.onError(cause); + return; + } + + this.produced = produced + 1; + this.inboundSubscriber.onNext(p); } 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); 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-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; } } 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..50bef5b70 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"); @@ -153,19 +170,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 +191,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 +209,7 @@ public T block(@Nullable Duration timeout) { @SuppressWarnings("unchecked") final void terminate(Throwable t) { if (isDisposed()) { + Operators.onErrorDropped(t, Context.empty()); return; } @@ -307,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/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/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/main/java/io/rsocket/core/ServerSetup.java b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java index 318c54816..5aae22e89 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))); } @@ -53,10 +60,15 @@ void dispose() {} void sendError(DuplexConnection duplexConnection, RSocketErrorException exception) { duplexConnection.sendErrorAndClose(exception); + duplexConnection.receive().subscribe(); } static class DefaultServerSetup extends ServerSetup { + DefaultServerSetup(Duration timeout) { + super(timeout); + } + @Override public Mono acceptRSocketSetup( ByteBuf frame, @@ -67,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); } } @@ -86,11 +98,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; @@ -109,14 +123,15 @@ 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, - duplexConnection, resumableDuplexConnection, - resumeSessionDuration, + duplexConnection, resumableFramesStore, + resumeSessionDuration, cleanupStoreOnKeepAlive); sessionManager.save(serverRSocketSession, resumeToken); @@ -126,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 b6bc87513..3beedf97f 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(); } @@ -167,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/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: 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..0296b0a07 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java @@ -1,42 +1,56 @@ +/* + * 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 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) { if (streamId == 0) { - sender.onNextPrioritized(frame); + sender.tryEmitPrioritized(frame); } else { - sender.onNext(frame); + sender.tryEmitNormal(frame); } } protected abstract void doOnClose(); @Override - public final Mono onClose() { - return onClose; + public Mono onClose() { + return onClose.asMono(); } @Override public final void dispose() { - onClose.onComplete(); + doOnClose(); } @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/internal/UnboundedProcessor.java b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java index d84546944..c96a7aed2 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -26,11 +26,13 @@ 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.Logger; import reactor.util.annotation.Nullable; import reactor.util.concurrent.Queues; import reactor.util.context.Context; @@ -40,29 +42,41 @@ * *

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; + final Runnable onFinalizedHook; + @Nullable final Logger logger; boolean cancelled; boolean done; 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; @@ -79,16 +93,27 @@ public final class UnboundedProcessor extends FluxProcessor static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(UnboundedProcessor.class, "requested"); + ByteBuf last; + boolean outputFused; public UnboundedProcessor() { - this.queue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); - this.priorityQueue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); + this(() -> {}); } - @Override - public int getBufferSize() { - return Integer.MAX_VALUE; + 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 @@ -106,128 +131,206 @@ public Object scanUnsafe(Attr key) { return isCancelled(state) || isDisposed(state); } - return super.scanUnsafe(key); + return null; } - public void onNextPrioritized(ByteBuf t) { - if (this.done) { - release(t); - return; - } - if (this.cancelled) { + public boolean tryEmitPrioritized(ByteBuf t) { + if (this.done || this.cancelled) { release(t); - return; + return false; } if (!this.priorityQueue.offer(t)) { - Throwable ex = - Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); - onError(Operators.onOperatorError(null, ex, t, currentContext())); + onError(Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext())); release(t); - return; + return false; + } + + final long previousState = markValueAdded(this); + if (isFinalized(previousState)) { + this.clearSafely(); + return false; } - drain(); + if (isSubscriberReady(previousState)) { + if (this.outputFused) { + // fast path for fusion + this.actual.onNext(null); + return true; + } + + if (isWorkInProgress(previousState)) { + return true; + } + + if (hasRequest(previousState)) { + drainRegular((previousState | FLAG_HAS_VALUE) + 1); + } + } + return true; } - @Override - public void onNext(ByteBuf t) { - if (this.done) { + public boolean tryEmitNormal(ByteBuf t) { + if (this.done || this.cancelled) { release(t); - return; + return false; } - if (this.cancelled) { + + if (!this.queue.offer(t)) { + onError(Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext())); release(t); - return; + return false; } - if (!this.queue.offer(t)) { - Throwable ex = - Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); - onError(Operators.onOperatorError(null, ex, t, currentContext())); + final long previousState = markValueAdded(this); + if (isFinalized(previousState)) { + this.clearSafely(); + return false; + } + + if (isSubscriberReady(previousState)) { + if (this.outputFused) { + // fast path for fusion + this.actual.onNext(null); + return true; + } + + if (isWorkInProgress(previousState)) { + return true; + } + + if (hasRequest(previousState)) { + drainRegular((previousState | FLAG_HAS_VALUE) + 1); + } + } + + return true; + } + + public boolean tryEmitFinal(ByteBuf t) { + if (this.done || this.cancelled) { release(t); - return; + 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; + } + + drainRegular((previousState | FLAG_TERMINATED | FLAG_HAS_VALUE) + 1); } - drain(); + 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) { + 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; + } + + drainRegular((previousState | FLAG_TERMINATED) + 1); } + } - if (isWorkInProgress(previousState)) { + @Override + @Deprecated + 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; } + + drainRegular((previousState | FLAG_TERMINATED) + 1); } } - void drainRegular(long expectedState, CoreSubscriber a) { + void drainRegular(long expectedState) { + final CoreSubscriber a = this.actual; final Queue q = this.queue; final Queue pq = this.priorityQueue; @@ -236,21 +339,23 @@ void drainRegular(long expectedState, CoreSubscriber a) { 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(); empty = t == null; } - if (checkTerminated(done, empty, a)) { + if (checkTerminated(done, empty, true, a)) { if (!empty) { release(t); } @@ -270,57 +375,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, false, 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; - } - - 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; + r = REQUESTED.addAndGet(this, -e); } - 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; } @@ -331,21 +405,34 @@ void drainFused(long expectedState, CoreSubscriber a) { } } - 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)) { - 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); + if (!isTerminated(state)) { + // proactively return if volatile field is not yet set to needed state + return false; + } + final ByteBuf last = this.last; + if (last != null) { + if (!hasDemand) { + return false; + } + this.last = null; + a.onNext(last); + } + clearAndFinalize(this); Throwable e = this.error; if (e != null) { a.onError(e); @@ -361,7 +448,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 (isSubscriberReady(previousState)) { + return; + } + + 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); + return; + } + + 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 | FLAG_SUBSCRIBER_READY) + 1); } } @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 | FLAG_HAS_REQUEST) + 1); + } } } @@ -415,39 +575,49 @@ 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); } } @Override + @Deprecated 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 @@ -457,7 +627,19 @@ public ByteBuf poll() { if (t != null) { return t; } - return this.queue.poll(); + + t = this.queue.poll(); + if (t != null) { + return t; + } + + t = this.last; + if (t != null) { + this.last = null; + return t; + } + + return null; } @Override @@ -479,7 +661,7 @@ public boolean isEmpty() { */ @Override public void clear() { - clearAndTerminate(this); + clearAndFinalize(this); } void clearSafely() { @@ -502,6 +684,12 @@ void clearUnsafely() { final Queue 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); @@ -523,36 +711,10 @@ 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; - } - - @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); } @@ -569,29 +731,28 @@ 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; + final long nextState = state | FLAG_SUBSCRIBED_ONCE; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mso", state, nextState); + 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 +761,213 @@ 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) + || isSubscriberReady(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); + } + } + + nextState = nextState | FLAG_SUBSCRIBER_READY; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " msr", state, nextState); 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 (isWorkInProgress(state) || (isSubscriberReady(state) && hasValue(state))) { + nextState = addWork(state); } - if (STATE.compareAndSet(instance, state, nextState | FLAG_CANCELLED)) { + nextState = nextState | FLAG_HAS_REQUEST; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mra", state, nextState); 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)) { + nextState = nextState | FLAG_HAS_VALUE; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mva", state, nextState); 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_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 wipIncrement(UnboundedProcessor instance) { + static long markValueAddedAndTerminated(UnboundedProcessor instance) { for (; ; ) { - long state = instance.state; + 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) { + nextState = addWork(state); + } + + nextState = nextState | FLAG_HAS_VALUE | FLAG_TERMINATED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, "mva&t", state, nextState); + 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 + * increments number of work in progress (WIP) + * + * @return previous state + */ + static long markTerminatedOrFinalized(UnboundedProcessor instance) { + for (; ; ) { + final long state = instance.state; + + if (isFinalized(state) || isTerminated(state) || isCancelled(state) || isDisposed(state)) { + return state; + } + + long nextState = state; + 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 { + nextState = addWork(state); + } + } + + nextState = nextState | FLAG_TERMINATED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mt|f", state, nextState); + if (isFinalized(nextState)) { + instance.onFinalizedHook.run(); + } + return state; + } + } + } - if ((state & FLAG_TERMINATED) == FLAG_TERMINATED) { + /** + * 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) | FLAG_CANCELLED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mc", state, nextState); 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; } + final long nextState = addWork(state) | FLAG_DISPOSED; if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " md", state, nextState); 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 +976,46 @@ 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) { for (; ; ) { - long state = instance.state; + final long state = instance.state; - if ((state & FLAG_TERMINATED) == FLAG_TERMINATED) { + if (state != expectedState) { 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; + 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; } } } /** - * 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 +1023,23 @@ 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)) { + 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; } } } + 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 +1056,112 @@ 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; + } + + 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(); + } } 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/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/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/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/LoadbalanceRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java index 4a1625e8a..d59cbb86e 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,17 @@ private LoadbalanceRSocketClient(RSocketPool rSocketPool) { this.rSocketPool = rSocketPool; } + @Override + 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() { return Mono.fromSupplier(rSocketPool::select); @@ -61,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 @@ -75,7 +87,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 +96,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 +106,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 +129,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 +145,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 +155,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 +165,7 @@ public Builder weightedLoadbalanceStrategy() { } /** - * Provide the {@link LoadbalanceStrategy} to use. + * Configure the {@link LoadbalanceStrategy} to use. * *

By default, {@link RoundRobinLoadbalanceStrategy} is used. */ @@ -165,8 +176,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 +191,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/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/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/main/java/io/rsocket/loadbalance/ResolvingOperator.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/ResolvingOperator.java index e03088b7f..52f16e166 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. @@ -18,18 +18,16 @@ import java.time.Duration; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.BiConsumer; -import org.reactivestreams.Subscription; -import reactor.core.CoreSubscriber; import reactor.core.Disposable; import reactor.core.Exceptions; -import reactor.core.Scannable; import reactor.core.publisher.Operators; 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 +70,7 @@ public ResolvingOperator() { } @Override - public void dispose() { + public final void dispose() { this.terminate(ON_DISPOSE); } @@ -168,19 +166,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; @@ -189,6 +187,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) { @@ -201,6 +205,7 @@ public T block(@Nullable Duration timeout) { @SuppressWarnings("unchecked") final void terminate(Throwable t) { if (isDisposed()) { + Operators.onErrorDropped(t, Context.empty()); return; } @@ -322,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; @@ -388,226 +417,4 @@ final void remove(BiConsumer ps) { } } } - - abstract static class DeferredResolution - implements CoreSubscriber, Subscription, Scannable, BiConsumer { - - final ResolvingOperator parent; - final CoreSubscriber actual; - - volatile long requested; - - @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater REQUESTED = - AtomicLongFieldUpdater.newUpdater(DeferredResolution.class, "requested"); - - static final long STATE_SUBSCRIBED = -1; - static final long STATE_CANCELLED = Long.MIN_VALUE; - - Subscription s; - boolean done; - - DeferredResolution(ResolvingOperator parent, CoreSubscriber actual) { - this.parent = parent; - this.actual = actual; - } - - @Override - public final Context currentContext() { - return this.actual.currentContext(); - } - - @Nullable - @Override - public Object scanUnsafe(Attr key) { - long state = this.requested; - - if (key == Attr.PARENT) { - return this.s; - } - if (key == Attr.ACTUAL) { - return this.parent; - } - if (key == Attr.TERMINATED) { - return this.done; - } - if (key == Attr.CANCELLED) { - return state == STATE_CANCELLED; - } - - return null; - } - - @Override - public final void onSubscribe(Subscription s) { - final long state = this.requested; - Subscription a = this.s; - if (state == STATE_CANCELLED) { - s.cancel(); - return; - } - if (a != null) { - s.cancel(); - return; - } - - long r; - long accumulated = 0; - for (; ; ) { - r = this.requested; - - if (r == STATE_CANCELLED || r == STATE_SUBSCRIBED) { - s.cancel(); - return; - } - - this.s = s; - - long toRequest = r - accumulated; - if (toRequest > 0) { // if there is something, - s.request(toRequest); // then we do a request on the given subscription - } - accumulated = r; - - if (REQUESTED.compareAndSet(this, r, STATE_SUBSCRIBED)) { - return; - } - } - } - - @Override - public final void onNext(T payload) { - this.actual.onNext(payload); - } - - @Override - public final void onError(Throwable t) { - if (this.done) { - Operators.onErrorDropped(t, this.actual.currentContext()); - return; - } - - this.done = true; - this.actual.onError(t); - } - - @Override - public final void onComplete() { - if (this.done) { - return; - } - - this.done = true; - this.actual.onComplete(); - } - - @Override - public void request(long n) { - if (Operators.validate(n)) { - long r = this.requested; // volatile read beforehand - - if (r > STATE_SUBSCRIBED) { // works only in case onSubscribe has not happened - long u; - for (; ; ) { // normal CAS loop with overflow protection - if (r == Long.MAX_VALUE) { - // if r == Long.MAX_VALUE then we dont care and we can loose this - // request just in case of racing - return; - } - u = Operators.addCap(r, n); - if (REQUESTED.compareAndSet(this, r, u)) { - // Means increment happened before onSubscribe - return; - } else { - // Means increment happened after onSubscribe - - // update new state to see what exactly happened (onSubscribe |cancel | requestN) - r = this.requested; - - // check state (expect -1 | -2 to exit, otherwise repeat) - if (r < 0) { - break; - } - } - } - } - - if (r == STATE_CANCELLED) { // if canceled, just exit - return; - } - - // if onSubscribe -> subscription exists (and we sure of that because volatile read - // after volatile write) so we can execute requestN on the subscription - this.s.request(n); - } - } - - public boolean isCancelled() { - return this.requested == STATE_CANCELLED; - } - - public void cancel() { - long state = REQUESTED.getAndSet(this, STATE_CANCELLED); - if (state == STATE_CANCELLED) { - return; - } - - if (state == STATE_SUBSCRIBED) { - this.s.cancel(); - } else { - this.parent.remove(this); - } - } - } - - 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..c30c8ad6b 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); @@ -119,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(); @@ -148,7 +156,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 +196,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; 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..ca4f5dcb4 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -18,9 +18,11 @@ 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; +import io.rsocket.exceptions.RejectedResumeException; import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; import io.rsocket.frame.ResumeFrameCodec; @@ -33,6 +35,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; @@ -54,6 +57,8 @@ public class ClientRSocketSession final Retry retry; final boolean cleanupStoreOnKeepAlive; final ByteBuf resumeToken; + final String session; + final Disposable reconnectDisposable; volatile Subscription s; static final AtomicReferenceFieldUpdater S = @@ -71,23 +76,43 @@ public ClientRSocketSession( Retry retry, boolean cleanupStoreOnKeepAlive) { this.resumeToken = resumeToken; + this.session = resumeToken.toString(CharsetUtil.UTF_8); this.connectionFactory = - connectionFactory.flatMap( - dc -> { - dc.sendFrame( - 0, - 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"); - - 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, this::tryReestablishSession); this.resumableFramesStore = resumableFramesStore; this.allocator = resumableDuplexConnection.alloc(); this.resumeSessionDuration = resumeSessionDuration; @@ -96,18 +121,34 @@ public ClientRSocketSession( this.resumableConnection = resumableDuplexConnection; resumableDuplexConnection.onClose().doFinally(__ -> dispose()).subscribe(); - resumableDuplexConnection.onActiveConnectionClosed().subscribe(this::reconnect); - S.lazySet(this, Operators.cancelledSubscription()); + this.reconnectDisposable = + resumableDuplexConnection.onActiveConnectionClosed().subscribe(this::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 to resume..."); - connectionFactory.retryWhen(retry).timeout(resumeSessionDuration).subscribe(this); + 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( + "Side[client]|Session[{}]. Connection[{}] is lost. Reconnecting to resume...", + session, + index); + } + connectionFactory + .doOnNext(this::tryReestablishSession) + .retryWhen(retry) + .timeout(resumeSessionDuration) + .subscribe(this); } @Override @@ -128,9 +169,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(); @@ -142,35 +194,27 @@ public boolean isDisposed() { return resumableConnection.isDisposed(); } - @Override - public void onSubscribe(Subscription s) { - if (Operators.setOnce(S, this, s)) { - s.request(Long.MAX_VALUE); + void tryReestablishSession(Tuple2 tuple2) { + if (logger.isDebugEnabled()) { + logger.debug("Active subscription is canceled {}", s == Operators.cancelledSubscription()); } - } - - @Override - public void onNext(Tuple2 tuple2) { ByteBuf shouldBeResumeOKFrame = 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; - } - 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(nextDuplexConnection, connectionErrorException); nextDuplexConnection.sendErrorAndClose(connectionErrorException); - return; + nextDuplexConnection.receive().subscribe(); + + throw connectionErrorException; // throw to retry connection again } final FrameType frameType = FrameHeaderCodec.nativeFrameType(shouldBeResumeOKFrame); @@ -182,57 +226,145 @@ public void onNext(Tuple2 tuple2) { // observed final long position = resumableFramesStore.framePosition(); final long impliedPosition = resumableFramesStore.frameImpliedPosition(); - logger.debug( - "ResumeOK FRAME received. ServerResumeState{observedFramesPosition[{}]}. ClientResumeState{observedFramesPosition[{}], sentFramesPosition[{}]}", - 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(); + 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(nextDuplexConnection, t); + nextDuplexConnection.sendErrorAndClose(t); + nextDuplexConnection.receive().subscribe(); + + return; + } + + if (!tryCancelSessionTimeout()) { + 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(); return; } - if (resumableConnection.connect(nextDuplexConnection)) { - keepAliveSupport.start(); - logger.debug("Session has been resumed successfully"); - } else { - logger.debug("Session has already been expired. Terminating received connection"); + 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", + session); + } final ConnectionErrorException connectionErrorException = new ConnectionErrorException("resumption_server_pos=[Session Expired]"); nextDuplexConnection.sendErrorAndClose(connectionErrorException); + nextDuplexConnection.receive().subscribe(); + // no need to do anything since connection resumable connection is liklly to + // be disposed } } 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(); + 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(nextDuplexConnection, connectionErrorException); + nextDuplexConnection.sendErrorAndClose(connectionErrorException); + nextDuplexConnection.receive().subscribe(); } } else if (frameType == FrameType.ERROR) { final RuntimeException exception = Exceptions.from(0, shouldBeResumeOKFrame); - logger.debug("Received error frame. Terminating received connection", exception); - resumableConnection.dispose(); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. Received error frame. Terminating received connection", + session, + exception); + } + if (exception instanceof RejectedResumeException) { + resumableConnection.dispose(nextDuplexConnection, exception); + nextDuplexConnection.dispose(); + nextDuplexConnection.receive().subscribe(); + return; + } + + nextDuplexConnection.dispose(); + nextDuplexConnection.receive().subscribe(); + throw exception; // assume retryable exception } else { - 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(nextDuplexConnection, connectionErrorException); + nextDuplexConnection.sendErrorAndClose(connectionErrorException); + nextDuplexConnection.receive().subscribe(); + + // 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)) { 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..e23bc154b 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. @@ -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.MonoProcessor; 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 MonoProcessor disposed = MonoProcessor.create(); - final ArrayList cachedFrames; - final String tag; + final Sinks.Empty disposed = Sinks.empty(); + 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_1111_1111_1111_1111_1111_1111_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)) { + handleTerminated(qs, this.terminal); + } else if (isDisposed()) { + handleDisposed(); + } 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; } } @@ -189,95 +362,113 @@ void resumeImplied() { @Override public Mono onClose() { - return disposed; + return disposed.asMono(); } @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.onComplete(); + final long previousState = markDisposed(this); + if (isFinalized(previousState) + || isDisposed(previousState) + || isWorkInProgress(previousState)) { + return; } - } - @Override - public boolean isDisposed() { - return state == 2; + drain((previousState + 1) | DISPOSED_FLAG); } - @Override - public void onSubscribe(Subscription s) { - saveFramesSubscriber.onSubscribe(Operators.emptySubscription()); - s.request(Long.MAX_VALUE); - } + void clearCache() { + final Queue frames = this.cachedFrames; + this.cacheSize = 0; - @Override - public void onError(Throwable t) { - saveFramesSubscriber.onError(t); + ByteBuf frame; + while ((frame = frames.poll()) != null) { + frame.release(); + } } @Override - public void onComplete() { - saveFramesSubscriber.onComplete(); + public boolean isDisposed() { + return isDisposed(this.state); } - @Override - public void onNext(ByteBuf frame) { - final int state; + void handleFrame(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); - } + handleResumableFrame(frame); + return; + } + + handleConnectionFrame(frame); + } + + void handleTerminated(Fuseable.QueueSubscription qs, @Nullable Throwable t) { + for (; ; ) { + final ByteBuf frame = qs.poll(); + final boolean empty = frame == null; + + if (empty) { + break; } - synchronized (this) { - state = this.state; - if (state != 2) { - frames.add(frame); + + handleFrame(frame); + } + if (t != null) { + this.actual.onError(t); + } else { + this.actual.onComplete(); + } + } + + void handleDisposed() { + this.actual.onError(new CancellationException("Disposed")); + } + + void handleConnectionFrame(ByteBuf frame) { + this.actual.onNext(frame); + } + + void handleResumableFrame(ByteBuf frame) { + final Queue frames = this.cachedFrames; + final int incomingFrameSize = frame.readableBytes(); + final int cacheLimit = this.cacheLimit; + + 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 { + this.cacheSize = cacheSize; + FIRST_AVAILABLE_FRAME_POSITION.lazySet( + this, firstAvailableFramePosition + removedBytes + incomingFrameSize); } - } - if (cacheLimit != Integer.MAX_VALUE) { - CACHE_SIZE.addAndGet(this, incomingFrameSize); + } else { + canBeStore = true; } } else { - state = this.state; + canBeStore = true; } - final CoreSubscriber actual = this.actual; - if (state == 1) { - actual.onNext(frame.retain()); - } else if (!isResumable || state == 2) { - frame.release(); + if (canBeStore) { + frames.offer(frame); + + if (cacheLimit != Integer.MAX_VALUE) { + this.cacheSize = cacheSize + incomingFrameSize; + } } + + this.actual.onNext(canBeStore ? frame.retainedSlice() : frame); } @Override @@ -286,30 +477,378 @@ 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.retain()); + 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)) { + return; + } + + if (isConnected(previousState) || hasPendingConnection(previousState)) { + parent.drain((previousState + 1) | HAS_FRAME_FLAG); + } + } + + @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 + 1) | 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 + 1) | 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 dbf77b902..c8811b9b3 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. @@ -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.ConnectionErrorException; +import io.rsocket.frame.ErrorFrameCodec; import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.internal.UnboundedProcessor; import java.net.SocketAddress; @@ -30,23 +33,25 @@ 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; +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; - final Disposable framesSaverDisposable; - final MonoProcessor onClose; + final Sinks.Empty onQueueClose; + final Sinks.Empty onLastConnectionClose; final SocketAddress remoteAddress; final Sinks.Many onConnectionClosedSink; @@ -66,15 +71,21 @@ 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(); - this.framesSaverDisposable = resumableFramesStore.saveFrames(savableFramesSender).subscribe(); - this.onClose = MonoProcessor.create(); + 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); } @@ -83,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); @@ -94,29 +108,55 @@ public boolean connect(DuplexConnection nextConnection) { } void initConnection(DuplexConnection nextConnection) { - logger.debug("Tag {}. Initializing connection {}", tag, nextConnection); - - final int currentConnectionIndex = connectionIndex; + final int nextConnectionIndex = this.connectionIndex + 1; final FrameReceivingSubscriber frameReceivingSubscriber = - new FrameReceivingSubscriber(tag, resumableFramesStore, receiveSubscriber); + new FrameReceivingSubscriber(side, resumableFramesStore, receiveSubscriber); - this.connectionIndex = currentConnectionIndex + 1; + this.connectionIndex = nextConnectionIndex; this.activeReceivingSubscriber = frameReceivingSubscriber; - final Disposable disposable = + if (logger.isDebugEnabled()) { + logger.debug( + "Side[{}]|Session[{}]|DuplexConnection[{}]. Connecting", side, session, connectionIndex); + } + + final Disposable resumeStreamSubscription = resumableFramesStore .resumeStream() - .subscribe(f -> nextConnection.sendFrame(FrameHeaderCodec.streamId(f), f)); + .subscribe( + f -> nextConnection.sendFrame(FrameHeaderCodec.streamId(f), f), + 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() .doFinally( __ -> { frameReceivingSubscriber.dispose(); - disposable.dispose(); - Sinks.EmitResult result = onConnectionClosedSink.tryEmitNext(currentConnectionIndex); + resumeStreamSubscription.dispose(); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[{}]|Session[{}]|DuplexConnection[{}]. Disconnected", + side, + session, + connectionIndex); + } + Sinks.EmitResult result = onConnectionClosedSink.tryEmitNext(nextConnectionIndex); if (!result.equals(Sinks.EmitResult.OK)) { - logger.error("Failed to notify session of closed connection: {}", result); + logger.error( + "Side[{}]|Session[{}]|DuplexConnection[{}]. Failed to notify session of closed connection: {}", + side, + session, + connectionIndex, + result); } }) .subscribe(); @@ -124,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(); } } @@ -132,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); } } @@ -155,26 +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(); - savableFramesSender.dispose(); onConnectionClosedSink.tryEmitComplete(); - onClose.onError(t); + onLastConnectionClose.tryEmitEmpty(); }, () -> { - framesSaverDisposable.dispose(); - savableFramesSender.dispose(); onConnectionClosedSink.tryEmitComplete(); + final Throwable cause = rSocketErrorException.getCause(); if (cause == null) { - onClose.onComplete(); + onLastConnectionClose.tryEmitEmpty(); } else { - onClose.onError(cause); + onLastConnectionClose.tryEmitError(cause); } }); } @@ -191,7 +230,8 @@ public ByteBufAllocator alloc() { @Override public Mono onClose() { - return onClose; + return Mono.whenDelayError( + onQueueClose.asMono(), resumableFramesStore.onClose(), onLastConnectionClose.asMono()); } @Override @@ -201,21 +241,55 @@ public void dispose() { if (activeConnection == DisposedConnection.INSTANCE) { return; } + savableFramesSender.onComplete(); + activeConnection + .onClose() + .subscribe( + null, + t -> { + onConnectionClosedSink.tryEmitComplete(); + onLastConnectionClose.tryEmitEmpty(); + }, + () -> { + onConnectionClosedSink.tryEmitComplete(); + onLastConnectionClose.tryEmitEmpty(); + }); + } - if (activeConnection != null) { - activeConnection.dispose(); + void dispose(DuplexConnection nextConnection, @Nullable Throwable e) { + final DuplexConnection activeConnection = + ACTIVE_CONNECTION.getAndSet(this, DisposedConnection.INSTANCE); + if (activeConnection == DisposedConnection.INSTANCE) { + return; } - - framesSaverDisposable.dispose(); - activeReceivingSubscriber.dispose(); - savableFramesSender.dispose(); - onConnectionClosedSink.tryEmitComplete(); - onClose.onComplete(); + 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.isDisposed(); + return onQueueClose.scan(Scannable.Attr.TERMINATED) + || onQueueClose.scan(Scannable.Attr.CANCELLED); } @Override @@ -226,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); } } @@ -247,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/main/java/io/rsocket/resume/ServerRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java index b62c615f3..ad1b38375 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,32 +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(); + return; + } long impliedPosition = resumableFramesStore.frameImpliedPosition(); long position = resumableFramesStore.framePosition(); - logger.debug( - "Resume FRAME received. ClientResumeState{observedFramesPosition[{}], sentFramesPosition[{}]}, ServerResumeState{observedFramesPosition[{}], sentFramesPosition[{}]}", - remoteImpliedPos, - 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; - } - - if (S.compareAndSet(this, subscription, null)) { - subscription.cancel(); - break; - } + if (logger.isDebugEnabled()) { + logger.debug( + "Side[server]|Session[{}]. Resume FRAME received. ServerResumeState[impliedPosition[{}], position[{}]]. ClientResumeState[remoteImpliedPosition[{}], remotePosition[{}]]", + session, + impliedPosition, + position, + remoteImpliedPos, + remotePos); } if (remotePos <= impliedPosition && position <= remoteImpliedPos) { @@ -152,38 +160,86 @@ 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", + session, + impliedPosition); + } } catch (Throwable t) { - logger.debug("Exception occurred while releasing frames in the frameStore", t); - tryTimeoutSession(); - nextDuplexConnection.sendErrorAndClose(new RejectedResumeException(t.getMessage(), t)); + if (logger.isDebugEnabled()) { + logger.debug( + "Side[server]|Session[{}]. Exception occurred while releasing frames in the frameStore", + session, + t); + } + + dispose(); + + final RejectedResumeException rejectedResumeException = + new RejectedResumeException(t.getMessage(), t); + nextDuplexConnection.sendErrorAndClose(rejectedResumeException); + nextDuplexConnection.receive().subscribe(); + return; } - if (resumableConnection.connect(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); + + 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", + session); + } + final RejectedResumeException rejectedResumeException = + new RejectedResumeException("resume_internal_error: Session Expired"); + nextDuplexConnection.sendErrorAndClose(rejectedResumeException); + nextDuplexConnection.receive().subscribe(); + + // resumableConnection is likely to be disposed at this stage. Thus we have + // nothing to do } } 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); - tryTimeoutSession(); + 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", + session, + remoteImpliedPos, + position, + remotePos, + impliedPosition); + } + + 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); + nextDuplexConnection.receive().subscribe(); + } + } + + boolean tryCancelSessionTimeout() { + for (; ; ) { + final Subscription subscription = this.s; + + if (subscription == Operators.cancelledSubscription()) { + return false; + } + + if (S.compareAndSet(this, subscription, null)) { + subscription.cancel(); + return true; + } } } @@ -195,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)); + } } } @@ -227,9 +287,11 @@ 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(); - resumableFramesStore.dispose(); } @Override 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-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/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-core/src/test/java/io/rsocket/core/AbstractSocketRule.java b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java index 53323a26e..310e15b3e 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,40 +35,33 @@ 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() { - if (socket != null) { - socket.dispose(); - } + protected void doInit() { if (connection != null) { connection.dispose(); } + if (socket != null) { + socket.dispose(); + } connection = new TestDuplexConnection(allocator); socket = newRSocket(); } 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..195df9434 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; @@ -56,41 +56,49 @@ 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()); - 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 @@ -101,41 +109,49 @@ 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()); - 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 1e3f86a7c..84576e6ce 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. @@ -15,32 +15,31 @@ * 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; import io.netty.util.ReferenceCounted; +import io.rsocket.FrameAssert; import io.rsocket.Payload; import io.rsocket.RSocket; +import io.rsocket.RaceTestConstants; import io.rsocket.frame.ErrorFrameCodec; import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; 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; 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; 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; @@ -51,18 +50,19 @@ 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.mockito.Mockito; import org.reactivestreams.Publisher; import reactor.core.Disposable; 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; import reactor.util.context.Context; +import reactor.util.context.ContextView; import reactor.util.retry.Retry; public class DefaultRSocketClientTests { @@ -74,19 +74,33 @@ 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 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() { @@ -178,19 +192,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 < 10000; i++) { + 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); @@ -240,19 +247,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 < 10000; i++) { + 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); @@ -310,14 +310,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); @@ -454,6 +457,8 @@ public void shouldBeAbleToResolveOriginalSource() { assertSubscriber1.assertTerminated().assertValueCount(1); Assertions.assertThat(assertSubscriber1.values()).isEqualTo(assertSubscriber.values()); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -477,19 +482,173 @@ public void shouldDisposeOriginalSource() { .assertErrorMessage("Disposed"); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); + } + + @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(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test - public void shouldDisposeOriginalSourceIfRacing() throws Throwable { - for (int i = 0; i < 10000; i++) { + 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(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); + } + + @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(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); + } + + @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(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + 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(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); + } + + @Test + 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); @@ -509,29 +668,79 @@ public void evaluate() {} .assertTerminated() .assertError(CancellationException.class) .assertErrorMessage("Disposed"); + + ByteBuf buf; + while ((buf = rule.connection.pollFrame()) != null) { + FrameAssert.assertThat(buf).hasStreamIdZero().hasData("Disposed").hasNoLeaks(); + } + + rule.allocator.assertHasNoLeaks(); + } + } + + @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); + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + assertSubscriber1.assertTerminated().assertComplete(); + + rule.allocator.assertHasNoLeaks(); } } - public static class ClientSocketRule extends AbstractSocketRule { + public static class ClientSocketRule extends AbstractSocketRule { protected RSocketClient client; protected Runnable delayer; - protected MonoProcessor producer; + protected Sinks.One producer; + + protected Sinks.Empty thisClosedSink; @Override - protected void init() { - super.init(); - delayer = () -> producer.onNext(socket); - producer = MonoProcessor.create(); + protected void doInit() { + super.doInit(); + delayer = () -> producer.tryEmitValue(socket); + producer = Sinks.one(); client = new DefaultRSocketClient( - producer - .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() { + this.thisClosedSink = Sinks.empty(); return new RSocketRequester( connection, PayloadDecoder.ZERO_COPY, @@ -543,24 +752,9 @@ protected RSocketRequester newRSocket() { Integer.MAX_VALUE, null, __ -> 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); + null, + thisClosedSink, + thisClosedSink.asMono()); } } } 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 1a41346e5..7cf12a81e 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; @@ -85,11 +100,13 @@ public void unexpectedFramesBeforeResumeOKFrame(String frameType) { .hasData("RESUME_OK frame must be received before any others") .hasStreamIdZero() .hasNoLeaks(); + + transport.alloc().assertHasNoLeaks(); } @Test public void ensuresThatSetupPayloadCanBeRetained() { - MonoProcessor retainedSetupPayload = MonoProcessor.create(); + AtomicReference retainedSetupPayload = new AtomicReference<>(); TestClientTransport transport = new TestClientTransport(); ByteBuf data = transport.alloc().buffer(); @@ -100,13 +117,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 +138,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 +149,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 +167,7 @@ public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions .expectComplete() .verify(Duration.ofMillis(100)); - Assertions.assertThat(testClientTransport.testConnection().getSent()) + assertThat(testClientTransport.testConnection().getSent()) .hasSize(1) .allMatch( bb -> { @@ -182,7 +192,7 @@ public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions metadataBuf.writeChar('m'); metadataBuf.release(); - Assertions.assertThat(testClientTransport.testConnection().getSent()) + assertThat(testClientTransport.testConnection().getSent()) .hasSize(1) .allMatch( bb -> { @@ -195,7 +205,9 @@ public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions System.out.println("calling release " + byteBuf.refCnt()); return byteBuf.release(); }); - Assertions.assertThat(setupPayload.refCnt()).isZero(); + assertThat(setupPayload.refCnt()).isZero(); + + testClientTransport.alloc().assertHasNoLeaks(); } @Test @@ -222,7 +234,7 @@ public void ensuresThatSetupPayloadProvidedAsMonoIsReleased() { .expectComplete() .verify(Duration.ofMillis(100)); - Assertions.assertThat(testClientTransport.testConnection().getSent()) + assertThat(testClientTransport.testConnection().getSent()) .hasSize(1) .allMatch( bb -> { @@ -238,7 +250,7 @@ public void ensuresThatSetupPayloadProvidedAsMonoIsReleased() { .expectComplete() .verify(Duration.ofMillis(100)); - Assertions.assertThat(testClientTransport.testConnection().getSent()) + assertThat(testClientTransport.testConnection().getSent()) .hasSize(1) .allMatch( bb -> { @@ -248,13 +260,15 @@ public void ensuresThatSetupPayloadProvidedAsMonoIsReleased() { }) .allMatch(ReferenceCounted::release); - Assertions.assertThat(saved) + assertThat(saved) .as("Metadata and data were consumed and released as slices") .allMatch( payload -> 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 a4978bd4f..a461833d3 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; @@ -59,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; @@ -68,10 +74,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,8 +90,10 @@ class RSocketLeaseTest { private RSocketResponder rSocketResponder; private RSocket mockRSocketHandler; - private EmitterProcessor leaseSender = EmitterProcessor.create(); + private Sinks.Many leaseSender = Sinks.many().multicast().onBackpressureBuffer(); private RequesterLeaseTracker requesterLeaseTracker; + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; @BeforeEach void setUp() { @@ -94,7 +102,9 @@ void setUp() { connection = new TestDuplexConnection(byteBufAllocator); requesterLeaseTracker = new RequesterLeaseTracker(TAG, 0); - responderLeaseTracker = new ResponderLeaseTracker(TAG, connection, () -> leaseSender); + responderLeaseTracker = new ResponderLeaseTracker(TAG, connection, () -> leaseSender.asFlux()); + this.thisClosedSink = Sinks.empty(); + this.otherClosedSink = Sinks.empty(); ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(connection, new InitializingInterceptorRegistry(), true); @@ -110,7 +120,9 @@ void setUp() { 0, null, __ -> null, - requesterLeaseTracker); + requesterLeaseTracker, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); mockRSocketHandler = mock(RSocket.class); when(mockRSocketHandler.metadataPush(any())) @@ -177,7 +189,13 @@ protected void hookOnError(Throwable throwable) { 0, FRAME_LENGTH_MASK, Integer.MAX_VALUE, - __ -> null); + __ -> null, + otherClosedSink); + } + + @AfterEach + void tearDownAndCheckForLeaks() { + byteBufAllocator.assertHasNoLeaks(); } @Test @@ -205,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 @@ -370,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 @@ -425,7 +457,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 +524,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 +618,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 +647,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 @@ -625,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 34810b6bd..966fd65f2 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java @@ -15,10 +15,13 @@ */ package io.rsocket.core; -import static org.junit.Assert.assertEquals; +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(); - Assertions.assertThat(rSocket1).isEqualTo(rSocket2); + 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(); - Assertions.assertThat(rSocket3).isEqualTo(rSocket4).isNotEqualTo(rSocket2); + 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( @@ -81,25 +101,35 @@ 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, 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,27 +151,48 @@ 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(); - Assertions.assertThat(rSocket1).isNotEqualTo(rSocket2); + connection1.dispose(); + connection2.dispose(); + rSocket1.onClose().block(Duration.ofSeconds(1)); + rSocket2.onClose().block(Duration.ofSeconds(1)); + transport.alloc().assertHasNoLeaks(); } @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/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java index 25d91b25b..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; @@ -44,6 +46,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 +63,20 @@ class RSocketRequesterSubscribersTest { private LeaksTrackingByteBufAllocator allocator; private RSocket rSocketRequester; private TestDuplexConnection connection; + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; + + @AfterEach + void tearDownAndCheckNoLeaks() { + allocator.assertHasNoLeaks(); + } @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,11 +89,14 @@ void setUp() { 0, null, __ -> null, - null); + null, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); } @ParameterizedTest @MethodSource("allInteractions") + @SuppressWarnings({"rawtypes", "unchecked"}) void singleSubscriber(Function> interaction, FrameType requestType) { Flux response = Flux.from(interaction.apply(rSocketRequester)); @@ -98,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 de6f86c57..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,45 +1,59 @@ 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.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +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; 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() { - RSocketRequester rSocket = rule.socket; + @AfterEach + public void tearDownAndCheckNoLeaks() { + rule.assertHasNoLeaks(); + } - Mono.delay(Duration.ofSeconds(1)).doOnNext(v -> rule.connection.dispose()).subscribe(); + @ParameterizedTest + @MethodSource("rsocketInteractions") + public void testCurrentStreamIsTerminatedOnConnectionClose( + FrameType requestType, Function> interaction) { + RSocketRequester rSocket = rule.socket; 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)); } - @Test - public void testSubsequentStreamIsTerminatedAfterConnectionClose() { + @ParameterizedTest + @MethodSource("rsocketInteractions") + public void testSubsequentStreamIsTerminatedAfterConnectionClose( + FrameType requestType, Function> interaction) { RSocketRequester rSocket = rule.socket; rule.connection.dispose(); @@ -48,14 +62,51 @@ public void testSubsequentStreamIsTerminatedAfterConnectionClose() { .verify(Duration.ofSeconds(5)); } - @Parameterized.Parameters - public static Iterable>> rsocketInteractions() { + 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); + 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"; + } + }); + 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"; + } + }); + 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"; + } + }); 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 0904abe0d..a1199f698 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,11 +25,15 @@ 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 org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; +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.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; @@ -45,6 +49,7 @@ import io.rsocket.Payload; import io.rsocket.PayloadAssert; import io.rsocket.RSocket; +import io.rsocket.RaceTestConstants; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.CustomRSocketException; @@ -77,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; @@ -89,17 +93,15 @@ 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; +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.scheduler.Schedulers; +import reactor.core.publisher.Sinks; import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; import reactor.test.util.RaceTestUtils; @@ -113,26 +115,21 @@ 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 public void tearDown() { Hooks.resetOnErrorDropped(); Hooks.resetOnNextDropped(); + rule.assertHasNoLeaks(); } @Test @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(); } @@ -150,19 +147,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(); } @@ -171,7 +170,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(); } @@ -190,7 +189,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); @@ -211,7 +210,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(); } @@ -227,10 +226,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(); } @@ -260,11 +262,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,36 +276,39 @@ 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(); - Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); } @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(rule.connection.getSent()) + assertThat(request.scan(Scannable.Attr.TERMINATED) || request.scan(Scannable.Attr.CANCELLED)) + .isTrue(); + assertThat(rule.connection.getSent()) .hasSize(1) .first() .matches(bb -> frameType(bb) == REQUEST_CHANNEL) @@ -313,7 +318,7 @@ public void testChannelRequestServerSideCancellation() { @Test public void testCorrectFrameOrder() { - MonoProcessor delayer = MonoProcessor.create(); + Sinks.One delayer = Sinks.one(); BaseSubscriber subscriber = new BaseSubscriber() { @Override @@ -321,26 +326,25 @@ 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(); 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(); } @@ -361,7 +365,7 @@ public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmen .expectSubscription() .expectErrorSatisfies( t -> - Assertions.assertThat(t) + assertThat(t) .isInstanceOf(IllegalArgumentException.class) .hasMessage( String.format(INVALID_PAYLOAD_ERROR_MESSAGE, maxFrameLength))) @@ -370,6 +374,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, @@ -403,11 +466,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) @@ -420,20 +483,10 @@ 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 - .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); @@ -446,8 +499,7 @@ public void evaluate() {} runner.accept(assertSubscriber, clientSocketRule); - Assertions.assertThat(clientSocketRule.connection.getSent()) - .allMatch(ReferenceCounted::release); + assertThat(clientSocketRule.connection.getSent()).allMatch(ReferenceCounted::release); clientSocketRule.assertHasNoLeaks(); } @@ -508,8 +560,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, @@ -518,7 +570,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, @@ -555,8 +607,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, @@ -566,7 +618,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, @@ -577,7 +629,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, @@ -589,7 +641,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, @@ -714,20 +766,25 @@ 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(); - Assertions.assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); + assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); rule.assertHasNoLeaks(); } @@ -736,22 +793,29 @@ 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( allocator, streamId, new CustomRSocketException(0x00000404, "test"))); - Assertions.assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); + assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); rule.assertHasNoLeaks(); } @@ -769,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)); @@ -785,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); @@ -819,21 +883,21 @@ 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))); } - 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) @@ -857,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); @@ -875,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( @@ -896,12 +964,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) @@ -919,7 +987,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) @@ -943,7 +1011,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); @@ -952,12 +1020,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) @@ -986,7 +1054,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( @@ -1000,7 +1068,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) @@ -1029,7 +1097,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( @@ -1067,7 +1135,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); @@ -1075,7 +1143,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()); @@ -1152,7 +1220,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(); @@ -1161,12 +1229,8 @@ 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 && interactionType1 != METADATA_PUSH) { @@ -1191,11 +1255,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(); } } @@ -1210,13 +1274,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(); } @@ -1277,7 +1341,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() @@ -1393,9 +1457,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(); @@ -1406,8 +1470,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, @@ -1419,11 +1489,15 @@ protected RSocketRequester newRSocket() { Integer.MAX_VALUE, null, (__) -> null, - null); + null, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); } 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 b82848b91..4f689e396 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. @@ -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; @@ -49,6 +46,7 @@ import io.rsocket.Payload; import io.rsocket.PayloadAssert; import io.rsocket.RSocket; +import io.rsocket.RaceTestConstants; import io.rsocket.frame.CancelFrameCodec; import io.rsocket.frame.ErrorFrameCodec; import io.rsocket.frame.FrameHeaderCodec; @@ -75,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; @@ -87,7 +84,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; @@ -96,10 +92,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.scheduler.Scheduler; -import reactor.core.scheduler.Schedulers; +import reactor.core.publisher.Sinks; import reactor.test.publisher.TestPublisher; import reactor.test.util.RaceTestUtils; @@ -108,43 +102,39 @@ 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 public void tearDown() { Hooks.resetOnErrorDropped(); Hooks.resetOnNextDropped(); + rule.assertHasNoLeaks(); } @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(); - 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 @Timeout(2_000) - public void testHandleResponseFrameNoError() throws Exception { + public void testHandleResponseFrameNoError() { final int streamId = 4; rule.connection.clearSendReceiveBuffers(); final TestPublisher testPublisher = TestPublisher.create(); @@ -157,21 +147,20 @@ 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))); + FrameAssert.assertThat(rule.connection.awaitFrame()).typeOf(FrameType.COMPLETE).hasNoLeaks(); testPublisher.assertWasNotCancelled(); } @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); - assertThat( - "Unexpected frame sent.", frameType(rule.connection.awaitFrame()), is(FrameType.ERROR)); + FrameAssert.assertThat(rule.connection.awaitFrame()) + .typeOf(FrameType.ERROR) + .hasData("Request-Stream not implemented.") + .hasNoLeaks(); } @Test @@ -190,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(); } @@ -251,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) @@ -261,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(); @@ -272,16 +261,16 @@ public void checkNoLeaksOnRacingCancelFromRequestChannelAndNextFromUpstream() { ByteBufAllocator allocator = rule.alloc(); final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); rule.setRequestInterceptor(testRequestInterceptor); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; 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); @@ -310,17 +299,15 @@ 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(); - monoProcessor.onComplete(); + 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(); @@ -333,7 +320,7 @@ public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChann ByteBufAllocator allocator = rule.alloc(); final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); rule.setRequestInterceptor(testRequestInterceptor); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { AssertSubscriber assertSubscriber = AssertSubscriber.create(); FluxSink[] sinks = new FluxSink[1]; @@ -363,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(); @@ -372,12 +359,11 @@ public Flux requestChannel(Publisher payloads) { @Test public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChannelTest1() { - 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++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { AssertSubscriber assertSubscriber = AssertSubscriber.create(); FluxSink[] sinks = new FluxSink[1]; @@ -400,20 +386,16 @@ 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")); sink.complete(); - }, - parallel); + }); - 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(); } @@ -422,12 +404,11 @@ public Flux requestChannel(Publisher payloads) { @Test public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromUpstreamOnErrorFromRequestChannelTest1() { - 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++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { FluxSink[] sinks = new FluxSink[1]; AssertSubscriber assertSubscriber = AssertSubscriber.create(); rule.setAcceptingSocket( @@ -490,26 +471,22 @@ 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); + 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); @@ -522,12 +499,11 @@ public Flux requestChannel(Publisher payloads) { @Test public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestStreamTest1() { - 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++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { FluxSink[] sinks = new FluxSink[1]; rule.setAcceptingSocket( @@ -550,10 +526,9 @@ 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); + assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); @@ -563,12 +538,11 @@ public Flux requestStream(Payload payload) { @Test public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestResponseTest1() { - 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++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { Operators.MonoSubscriber[] sources = new Operators.MonoSubscriber[1]; rule.setAcceptingSocket( @@ -594,10 +568,9 @@ 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); + assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); @@ -605,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)) @@ -638,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(); } @@ -684,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(); } @@ -754,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) @@ -763,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) @@ -773,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); @@ -820,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( @@ -861,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(); @@ -1097,32 +1068,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 +1109,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 +1137,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 +1163,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(); @@ -1213,9 +1184,10 @@ public static class ServerSocketRule extends AbstractSocketRule onCloseSink; @Override - protected void init() { + protected void doInit() { acceptingSocket = new RSocket() { @Override @@ -1223,7 +1195,7 @@ public Mono requestResponse(Payload payload) { return Mono.just(payload); } }; - super.init(); + super.doInit(); } public void setAcceptingSocket(RSocket acceptingSocket) { @@ -1231,12 +1203,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) { @@ -1244,11 +1216,12 @@ public void setAcceptingSocket(RSocket acceptingSocket, int prefetch) { connection = new TestDuplexConnection(alloc()); connectSub = TestSubscriber.create(); this.prefetch = prefetch; - super.init(); + super.doInit(); } @Override protected RSocketResponder newRSocket() { + onCloseSink = Sinks.empty(); return new RSocketResponder( connection, acceptingSocket, @@ -1257,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/RSocketServerFragmentationTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java index 073ebfd06..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,9 +1,12 @@ 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; -import org.junit.Test; +import org.junit.jupiter.api.Test; public class RSocketServerFragmentationTest { @@ -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 fc3da93dd..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,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.core; import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; @@ -5,19 +20,26 @@ 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.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; 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; +import reactor.test.scheduler.VirtualTimeScheduler; public class RSocketServerTest { @@ -42,6 +64,34 @@ 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 { + 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 { + transport.alloc().assertHasNoLeaks(); + VirtualTimeScheduler.reset(); + } } @Test @@ -81,17 +131,18 @@ 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(); - 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); @@ -106,6 +157,45 @@ 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(); + FrameAssert.assertThat(connection.pollFrame()) + .hasStreamIdZero() + .hasData("SETUP or RESUME frame must be received before any others") + .hasNoLeaks(); + 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(); } } 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..e01e6ebdc 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. @@ -35,23 +35,32 @@ 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.AfterEach; +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; -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; public class RSocketTest { - @Rule public final SocketRule rule = new SocketRule(); + public final SocketRule rule = new SocketRule(); + + @BeforeEach + public void setup() { + rule.init(); + } + + @AfterEach + public void tearDownAndCheckOnLeaks() { + rule.alloc().assertHasNoLeaks(); + } @Test public void rsocketDisposalShouldEndupWithNoErrorsOnClose() { @@ -81,7 +90,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 +99,8 @@ public void testRequestReplyNoError() { .verify(); } - @Test(timeout = 2000) + @Test + @Timeout(2000) public void testHandlerEmitsError() { rule.setRequestAcceptor( new RSocket() { @@ -109,7 +120,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 +143,8 @@ public Mono requestResponse(Payload payload) { .verify(); } - @Test(timeout = 2000) + @Test + @Timeout(2000) public void testRequestPropagatesCorrectlyForRequestChannel() { rule.setRequestAcceptor( new RSocket() { @@ -140,7 +153,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 +167,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,10 +503,10 @@ void errorFromRequesterPublisher( responderPublisher.assertNoSubscribers(); } - public static class SocketRule extends ExternalResource { + public static class SocketRule { - DirectProcessor serverProcessor; - DirectProcessor clientProcessor; + Sinks.Many serverProcessor; + Sinks.Many clientProcessor; private RSocketRequester crs; @SuppressWarnings("unused") @@ -499,26 +515,20 @@ public static class SocketRule extends ExternalResource { private RSocket requestAcceptor; private LeaksTrackingByteBufAllocator allocator; - - @Override - public Statement apply(Statement base, Description description) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - init(); - base.evaluate(); - } - }; - } + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; public LeaksTrackingByteBufAllocator alloc() { return allocator; } - protected void init() { + public void init() { allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); - serverProcessor = DirectProcessor.create(); - clientProcessor = DirectProcessor.create(); + 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); @@ -540,8 +550,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 @@ -568,7 +577,8 @@ public Flux requestChannel(Publisher payloads) { 0, FRAME_LENGTH_MASK, Integer.MAX_VALUE, - __ -> null); + __ -> null, + otherClosedSink); crs = new RSocketRequester( @@ -582,7 +592,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/ReconnectMonoTests.java b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java index 8d96222df..3112a0943 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. @@ -16,8 +16,11 @@ package io.rsocket.core; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import io.rsocket.RaceTestConstants; +import io.rsocket.internal.subscriber.AssertSubscriber; import java.io.IOException; import java.time.Duration; import java.util.ArrayList; @@ -29,9 +32,10 @@ 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; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; @@ -39,7 +43,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; @@ -58,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); @@ -76,26 +79,27 @@ 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(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); RaceTestUtils.race(() -> monoSubscribers[0].onNext("value" + index), reconnectMono::dispose); 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); + assertThat(expired).containsOnly("value" + i); } else { - Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + subscriber.assertValues("value" + i); } expired.clear(); @@ -106,41 +110,38 @@ 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); 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(); + assertThat(expired).isEmpty(); + 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); + assertThat(reconnectMono.resolvingInner.subscribers).isEqualTo(ResolvingOperator.READY); - Assertions.assertThat( + assertThat( reconnectMono.resolvingInner.add( new ResolvingOperator.MonoDeferredResolutionOperator<>( - reconnectMono.resolvingInner, processor))) + 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(); } @@ -149,7 +150,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); @@ -157,11 +158,12 @@ 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(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); reconnectMono.resolvingInner.mainSubscriber.onNext("value_to_expire" + i); reconnectMono.resolvingInner.mainSubscriber.onComplete(); @@ -169,38 +171,33 @@ 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) { + assertThat(v).isEqualTo("value_to_not_expire" + index); + } else { + 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)); } @@ -213,7 +210,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); @@ -221,55 +218,50 @@ 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(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); reconnectMono.resolvingInner.mainSubscriber.onNext("value_to_expire" + i); reconnectMono.resolvingInner.mainSubscriber.onComplete(); RaceTestUtils.race( - () -> - RaceTestUtils.race( - reconnectMono::invalidate, reconnectMono::invalidate, Schedulers.parallel()), + reconnectMono::invalidate, + reconnectMono::invalidate, () -> { - 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(); } - }, - 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(); + 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)); } @@ -282,58 +274,62 @@ 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 TestPublisher cold = - TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + final Mono source = + Mono.fromSupplier( + new Supplier() { + boolean once = false; + + @Override + public String get() { + + if (!once) { + once = true; + return "value_to_expire" + index; + } + + return "value_to_not_expire" + index; + } + }); final ReconnectMono reconnectMono = - cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + new ReconnectMono<>( + source.subscribeOn(Schedulers.boundedElastic()), onExpire(), onValue()); - final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); + final AssertSubscriber subscriber = + reconnectMono.subscribeWith(new AssertSubscriber<>()); - reconnectMono.resolvingInner.mainSubscriber.onNext("value_to_expire" + i); - reconnectMono.resolvingInner.mainSubscriber.onComplete(); + subscriber.await().assertComplete(); + + assertThat(expired).isEmpty(); RaceTestUtils.race( () -> - Assertions.assertThat(reconnectMono.block()) + 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()); - - Assertions.assertThat(processor.isTerminated()).isTrue(); - - Assertions.assertThat(processor.peek()).isEqualTo("value_to_expire" + i); - - Assertions.assertThat(expired).hasSize(1).containsOnly("value_to_expire" + i); + reconnectMono::invalidate); + + subscriber.assertTerminated(); + + subscriber.assertValues("value_to_expire" + i); + + assertThat(expired).hasSize(1).containsOnly("value_to_expire" + i); if (reconnectMono.resolvingInner.subscribers == ResolvingOperator.READY) { - Assertions.assertThat(received) + await().atMost(Duration.ofSeconds(5)).until(() -> received.size() == 2); + 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)); } @@ -345,45 +341,42 @@ public void shouldNotExpireNewlyResolvedValueIfBlockIsRacingWithInvalidate() { @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); 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(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); - Assertions.assertThat(cold.subscribeCount()).isZero(); + 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(); + 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); + 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, processor))) + 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(); } @@ -392,45 +385,43 @@ 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); 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(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); - Assertions.assertThat(cold.subscribeCount()).isZero(); + assertThat(cold.subscribeCount()).isZero(); 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); - Assertions.assertThat(values).containsExactly("value" + i); + subscriber.assertValues("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, processor))) + 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(); } @@ -439,17 +430,17 @@ 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); 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]; @@ -458,24 +449,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, MonoProcessor.create()))) + 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(); } @@ -484,35 +472,36 @@ 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); 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(); + assertThat(expired).isEmpty(); + 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) + 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(); @@ -522,36 +511,35 @@ 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); 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(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); cold.next("value" + i); RaceTestUtils.race(cold::complete, reconnectMono::dispose); - Assertions.assertThat(processor.isTerminated()).isTrue(); + subscriber.assertTerminated(); - if (processor.isError()) { - Assertions.assertThat(processor.getError()) + if (!subscriber.errors().isEmpty()) { + 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); + 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(); @@ -562,42 +550,40 @@ 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); 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(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); cold.next("value" + i); 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) { + assertThat(error) .isInstanceOf(CancellationException.class) .hasMessage("ReconnectMono has already been disposed"); } else { - Assertions.assertThat(processor.getError()) - .isInstanceOf(RuntimeException.class) - .hasMessage("test"); + 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); + 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(); @@ -608,7 +594,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); @@ -617,35 +603,32 @@ 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(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); cold.next("value" + i); 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) { + assertThat(error) .isInstanceOf(CancellationException.class) .hasMessage("ReconnectMono has already been disposed"); } else { - Assertions.assertThat(processor.getError()) - .matches(t -> Exceptions.isRetryExhausted(t)) - .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)); - Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + assertThat(received).hasSize(1).containsOnly(Tuples.of("value" + i, reconnectMono)); + subscriber.assertValues("value" + i); } expired.clear(); @@ -693,20 +676,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 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( + assertThat( (List) scannableOfMonoProcessor .parents() @@ -721,9 +704,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); } @@ -735,36 +717,36 @@ 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(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(subscriber.isTerminated()).isFalse(); publisher.next("test"); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(processor.isTerminated()).isFalse(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(subscriber.isTerminated()).isFalse(); reconnectMono.invalidate(); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(processor.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); - Assertions.assertThat(processor.isTerminated()).isTrue(); + assertThat(expired).isEmpty(); + assertThat(received).hasSize(1); + subscriber.assertTerminated(); publisher.assertSubscribers(0); - Assertions.assertThat(publisher.subscribeCount()).isEqualTo(1); + assertThat(publisher.subscribeCount()).isEqualTo(1); } @Test @@ -775,26 +757,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(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(subscriber.isTerminated()).isFalse(); publisher.next("test"); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(processor.isTerminated()).isFalse(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + 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"); + assertThat(expired).isEmpty(); + assertThat(received).hasSize(1); + subscriber.assertTerminated(); + subscriber.assertValues("test"); } @Test @@ -805,31 +787,30 @@ 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(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(subscriber.isTerminated()).isFalse(); publisher.next("test"); - Assertions.assertThat(expired).isEmpty(); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(processor.isTerminated()).isFalse(); + assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(subscriber.isTerminated()).isFalse(); - processor.cancel(); + 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(processor.isTerminated()).isFalse(); - Assertions.assertThat(processor.peek()).isNull(); + assertThat(expired).isEmpty(); + assertThat(received).hasSize(1); + assertThat(subscriber.values()).isEmpty(); } @Test @@ -848,16 +829,16 @@ 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.elastic())) + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.boundedElastic())) .expectSubscription() .expectError(CancellationException.class) .verify(Duration.ofSeconds(timeout)); @@ -870,85 +851,91 @@ 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); reconnectMono.subscribe(sub3); reconnectMono.subscribe(sub4); - Assertions.assertThat(reconnectMono.resolvingInner.subscribers).hasSize(4); + 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); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + 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); + assertThat(reconnectMono.resolvingInner.subscribers).hasSize(RaceTestConstants.REPEATS * 2 + 4); - sub1.dispose(); + sub1.cancel(); - Assertions.assertThat(reconnectMono.resolvingInner.subscribers).hasSize(203); + assertThat(reconnectMono.resolvingInner.subscribers).hasSize(RaceTestConstants.REPEATS * 2 + 3); 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"); + 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 (MonoProcessor sub : processors) { - Assertions.assertThat(sub.peek()).isEqualTo("value"); - Assertions.assertThat(sub.isTerminated()).isTrue(); + for (AssertSubscriber sub : subscribers) { + assertThat(sub.values().get(0)).isEqualTo("value"); + assertThat(sub.isTerminated()).isTrue(); } - Assertions.assertThat(publisher.subscribeCount()).isEqualTo(1); + assertThat(publisher.subscribeCount()).isEqualTo(1); } @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"); + 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.elastic())) + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.boundedElastic())) .expectSubscription() .expectNext("value") .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)); - StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + 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) + assertThat(expired).hasSize(1).containsOnly("value"); + 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); + assertThat(cold.subscribeCount()).isEqualTo(2); expired.clear(); received.clear(); @@ -957,7 +944,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; @@ -965,29 +952,29 @@ 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() .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.elastic())) + 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(); @@ -1011,14 +998,14 @@ 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) .verify(Duration.ofSeconds(timeout)); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); } @Test @@ -1027,11 +1014,11 @@ 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 -> - Assertions.assertThat(t) + assertThat(t) .hasMessage("Source completed empty") .isInstanceOf(IllegalStateException.class)) .verify(Duration.ofSeconds(timeout)); @@ -1047,8 +1034,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 @@ -1066,8 +1053,8 @@ public void monoRetryFixedBackoff() { assertRetries(IOException.class); - Assertions.assertThat(received).isEmpty(); - Assertions.assertThat(expired).isEmpty(); + assertThat(received).isEmpty(); + assertThat(expired).isEmpty(); } @Test @@ -1091,8 +1078,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() { @@ -1109,12 +1096,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/RequestChannelRequesterFluxTest.java b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelRequesterFluxTest.java index b9d63cf5e..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; @@ -24,6 +25,7 @@ import io.rsocket.FrameAssert; import io.rsocket.Payload; import io.rsocket.PayloadAssert; +import io.rsocket.RaceTestConstants; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.frame.FrameType; @@ -39,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; @@ -512,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 | @@ -545,7 +619,7 @@ public void shouldHaveEventsDeliveredSeriallyWhenOutboundErrorRacingWithInboundS Hooks.onErrorDropped(droppedErrors::add); try { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); final TestDuplexConnection sender = activeStreams.getDuplexConnection(); @@ -706,7 +780,7 @@ public void shouldHaveEventsDeliveredSeriallyWhenOutboundErrorRacingWithInboundS @ValueSource(strings = {"complete", "cancel"}) public void shouldRemoveItselfFromActiveStreamsWhenInboundAndOutboundAreTerminated( String outboundTerminationMode) { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); final TestDuplexConnection sender = activeStreams.getDuplexConnection(); 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 54033e249..890458caf 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequestChannelResponderSubscriberTest.java @@ -28,6 +28,7 @@ import io.rsocket.FrameAssert; import io.rsocket.Payload; import io.rsocket.PayloadAssert; +import io.rsocket.RaceTestConstants; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.frame.FrameType; @@ -262,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 | @@ -270,7 +408,7 @@ public void requestNFrameShouldBeSentOnSubscriptionAndThenSeparately(String comp @Test public void streamShouldWorkCorrectlyWhenRacingHandleCompleteWithSubscription() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); final TestDuplexConnection sender = activeStreams.getDuplexConnection(); @@ -331,7 +469,7 @@ public void streamShouldWorkCorrectlyWhenRacingHandleCompleteWithSubscription() public void streamShouldWorkCorrectlyWhenRacingHandleErrorWithSubscription() { ApplicationErrorException applicationErrorException = new ApplicationErrorException("test"); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); final Payload firstPayload = TestRequesterResponderSupport.randomPayload(allocator); @@ -377,7 +515,7 @@ public void streamShouldWorkCorrectlyWhenRacingHandleErrorWithSubscription() { public void streamShouldWorkCorrectlyWhenRacingOutboundErrorWithSubscription() { RuntimeException exception = new RuntimeException("test"); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); final Payload firstPayload = TestRequesterResponderSupport.randomPayload(allocator); @@ -425,7 +563,7 @@ public void streamShouldWorkCorrectlyWhenRacingOutboundErrorWithSubscription() { @Test public void streamShouldWorkCorrectlyWhenRacingHandleCancelWithSubscription() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); final Payload firstPayload = TestRequesterResponderSupport.randomPayload(allocator); @@ -493,7 +631,7 @@ public void shouldHaveEventsDeliveredSeriallyWhenOutboundErrorRacingWithInboundS Hooks.onErrorDropped(droppedErrors::add); try { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); final TestDuplexConnection sender = activeStreams.getDuplexConnection(); @@ -656,14 +794,14 @@ public void shouldHaveNoLeaksOnReassemblyAndCancelRacing(String terminationMode) final Payload oversizePayload = DefaultPayload.create(new byte[FRAME_LENGTH_MASK], new byte[FRAME_LENGTH_MASK]); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(); final LeaksTrackingByteBufAllocator allocator = activeStreams.getAllocator(); final TestDuplexConnection sender = activeStreams.getDuplexConnection(); ; 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 = @@ -724,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(); 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 1396774c4..06d050f6f 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RequesterOperatorsRacingTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RequesterOperatorsRacingTest.java @@ -27,6 +27,7 @@ import io.netty.util.CharsetUtil; import io.rsocket.FrameAssert; import io.rsocket.Payload; +import io.rsocket.RaceTestConstants; import io.rsocket.frame.FrameType; import io.rsocket.frame.RequestStreamFrameCodec; import io.rsocket.internal.subscriber.AssertSubscriber; @@ -168,7 +169,7 @@ public String toString() { @ParameterizedTest(name = "Should subscribe exactly once to {0}") @MethodSource("scenarios") public void shouldSubscribeExactlyOnce(Scenario scenario) { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); final TestRequesterResponderSupport requesterResponderSupport = TestRequesterResponderSupport.client(testRequestInterceptor); @@ -277,7 +278,7 @@ public void shouldSentRequestFrameOnceInCaseOfRequestRacing(Scenario scenario) { Assumptions.assumeThat(scenario.requestType()) .isIn(REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(testRequestInterceptor); @@ -364,7 +365,7 @@ public void shouldHaveNoLeaksOnReassemblyAndCancelRacing(Scenario scenario) { Assumptions.assumeThat(scenario.requestType()) .isIn(REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(testRequestInterceptor); @@ -472,7 +473,7 @@ public void shouldHaveNoLeaksOnNextAndCancelRacing(Scenario scenario) { Assumptions.assumeThat(scenario.requestType()) .isIn(REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(testRequestInterceptor); @@ -550,7 +551,7 @@ public void shouldHaveNoUnexpectedErrorDuringOnErrorAndCancelRacing(Scenario sce try { for (boolean withReassembly : withReassemblyOptions) { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(testRequestInterceptor); @@ -668,7 +669,7 @@ public void shouldHaveNoUnexpectedErrorDuringOnErrorAndCancelRacing(Scenario sce public void shouldBeConsistentInCaseOfRacingOfCancellationAndRequest(Scenario scenario) { Assumptions.assumeThat(scenario.requestType()) .isIn(REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(testRequestInterceptor); @@ -727,7 +728,7 @@ public void shouldBeConsistentInCaseOfRacingOfCancellationAndRequest(Scenario sc public void shouldSentCancelFrameExactlyOnce(Scenario scenario) { Assumptions.assumeThat(scenario.requestType()) .isIn(REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); final TestRequesterResponderSupport activeStreams = TestRequesterResponderSupport.client(testRequestInterceptor); 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..382240c4a 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; @@ -36,31 +37,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.scheduler.Schedulers; +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++) { + for (int i = 0; i < RaceTestConstants.REPEATS; 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() @@ -76,42 +77,48 @@ 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); } } } @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(); + 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() @@ -123,10 +130,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) @@ -134,39 +138,40 @@ 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(); } } @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; - 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() @@ -179,10 +184,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( @@ -191,11 +193,10 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() self::invalidate, () -> { self.observe(consumer2); - if (!processor2.isTerminated()) { + if (!subscriber2.isTerminated()) { self.complete(valueToSend2); } - }, - Schedulers.parallel())) + })) .then( self -> { if (self.isPending()) { @@ -209,46 +210,51 @@ 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()); } } @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; - 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() @@ -261,25 +267,20 @@ 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( self -> RaceTestUtils.race( - () -> - RaceTestUtils.race( - self::invalidate, self::invalidate, Schedulers.parallel()), + self::invalidate, + self::invalidate, () -> { self.observe(consumer2); - if (!processor2.isTerminated()) { + if (!subscriber2.isTerminated()) { self.complete(valueToSend2); } - }, - Schedulers.parallel())) + })) .then( self -> { if (!self.isPending()) { @@ -296,7 +297,7 @@ public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates( .haveAtMost( 2, new Condition<>( - new Predicate() { + new Predicate() { int time = 0; @Override @@ -313,40 +314,39 @@ 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()); } } @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; - 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() @@ -359,10 +359,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( @@ -371,19 +368,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()) { @@ -400,29 +393,33 @@ 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(); + 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() @@ -442,11 +439,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); @@ -457,20 +454,23 @@ 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(); + 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() @@ -482,7 +482,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) @@ -490,11 +494,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); @@ -506,11 +510,14 @@ 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(); - MonoProcessor processor2 = MonoProcessor.create(); + AssertSubscriber subscriber = AssertSubscriber.create(); + subscriber.onSubscribe(Operators.emptySubscription()); + + AssertSubscriber subscriber2 = AssertSubscriber.create(); + subscriber2.onSubscribe(Operators.emptySubscription()); ResolvingTest.create() .assertNothingExpired() @@ -521,8 +528,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) @@ -530,11 +543,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); @@ -548,26 +561,31 @@ public void shouldEstablishValueOnceInCaseOfRacingBetweenBlocks() { public void shouldExpireValueOnRacingDisposeAndError() { Hooks.onErrorDropped(t -> {}); RuntimeException runtimeException = new RuntimeException("test"); - for (int i = 0; i < 10000; i++) { - MonoProcessor processor = MonoProcessor.create(); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + 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() @@ -591,8 +609,9 @@ public void shouldExpireValueOnRacingDisposeAndError() { }) .thenAddObserver(consumer2); - StepVerifier.create(processor) - .expectErrorSatisfies( + subscriber + .await(Duration.ofMillis(10)) + .assertErrorWith( t -> { if (t instanceof CancellationException) { Assertions.assertThat(t) @@ -601,11 +620,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) @@ -614,8 +633,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()) @@ -664,9 +682,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) { @@ -677,14 +696,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); }); } @@ -737,12 +763,13 @@ 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<>(RaceTestConstants.REPEATS * 2); ResolvingTest.create() .assertDisposeCalled(0) @@ -761,9 +788,9 @@ public void shouldNotifyAllTheSubscribers( .assertPendingSubscribers(4) .then( self -> { - for (int i = 0; i < 100; i++) { - final MonoProcessor subA = MonoProcessor.create(); - final MonoProcessor subB = MonoProcessor.create(); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + AssertSubscriber subA = AssertSubscriber.create(); + AssertSubscriber subB = AssertSubscriber.create(); processors.add(subA); processors.add(subB); RaceTestUtils.race( @@ -772,21 +799,21 @@ public void shouldNotifyAllTheSubscribers( } }) .assertSubscribeCalled(1) - .assertPendingSubscribers(204) - .then(self -> sub1.dispose()) - .assertPendingSubscribers(203) + .assertPendingSubscribers(RaceTestConstants.REPEATS * 2 + 4) + .then(self -> sub1.cancel()) + .assertPendingSubscribers(RaceTestConstants.REPEATS * 2 + 3) .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(); } }) @@ -797,7 +824,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( @@ -833,7 +860,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() @@ -847,7 +874,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() @@ -967,4 +994,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(); + } + } + } } 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(); + } +} 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..87c3a865f 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 { @@ -38,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 @@ -49,6 +71,8 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); TestDuplexConnection conn = new TestDuplexConnection(allocator); + Sinks.Empty onThisSideClosedSink = Sinks.empty(); + RSocketRequester rSocket = new RSocketRequester( conn, @@ -61,7 +85,9 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { 0, null, __ -> null, - null); + null, + onThisSideClosedSink, + onThisSideClosedSink.asMono()); String errorMsg = "error"; @@ -80,6 +106,7 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { .verify(Duration.ofSeconds(5)); assertThat(rSocket.isDisposed()).isTrue(); + allocator.assertHasNoLeaks(); } @Test @@ -87,6 +114,7 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); TestDuplexConnection conn = new TestDuplexConnection(allocator); + Sinks.Empty onThisSideClosedSink = Sinks.empty(); RSocketRequester rSocket = new RSocketRequester( conn, @@ -99,7 +127,9 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { 0, null, __ -> null, - null); + null, + onThisSideClosedSink, + onThisSideClosedSink.asMono()); conn.addToReceivedBuffer( ErrorFrameCodec.encode(ByteBufAllocator.DEFAULT, 0, new RejectedSetupException("error"))); @@ -111,11 +141,13 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { .expectErrorMatches( err -> err instanceof RejectedSetupException && "error".equals(err.getMessage())) .verify(Duration.ofSeconds(5)); + allocator.assertHasNoLeaks(); } 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 +155,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/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/exceptions/ExceptionsTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java index fd05cb7da..a316aed8b 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; @@ -42,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"); + 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"); + 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") @@ -57,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") @@ -86,122 +100,152 @@ 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(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"); + 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") @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() @@ -209,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/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/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/internal/UnboundedProcessorTest.java b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java index f90607957..343a93beb 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java @@ -23,18 +23,16 @@ 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; 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.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; @@ -91,7 +89,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); } @@ -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 < RaceTestConstants.REPEATS; i++) { final UnboundedProcessor unboundedProcessor = new UnboundedProcessor(); final ByteBuf buffer1 = allocator.buffer(1); @@ -123,68 +121,246 @@ public void ensureUnboundedProcessorDisposesQueueProperly(boolean withFusionEnab unboundedProcessor.subscribe(assertSubscriber); RaceTestUtils.race( - () -> - RaceTestUtils.race( - () -> - RaceTestUtils.race( - () -> { - unboundedProcessor.onNext(buffer1); - unboundedProcessor.onNext(buffer2); - }, - unboundedProcessor::dispose, - Schedulers.boundedElastic()), - assertSubscriber::cancel, - Schedulers.boundedElastic()), + () -> { + 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 < RaceTestConstants.REPEATS; 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}) + public void smokeTest2(boolean withFusionEnabled) { + final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + final RuntimeException runtimeException = new RuntimeException("test"); + 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); + 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.boundedElastic()); + 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 < RaceTestConstants.REPEATS; 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 < RaceTestConstants.REPEATS; 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 < RaceTestConstants.REPEATS; 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..b6eac9835 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); @@ -864,8 +865,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 +928,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 +983,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 +1020,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)); } } } @@ -1197,6 +1246,10 @@ public List values() { return values; } + public List errors() { + return errors; + } + public final AssertSubscriber assertNoEvents() { return assertNoValues().assertNoError().assertNotComplete(); } @@ -1205,4 +1258,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/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/loadbalance/LoadbalanceRSocketClientTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java new file mode 100644 index 000000000..a35e89391 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java @@ -0,0 +1,94 @@ +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) + .onBackpressureBuffer() + .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); + } +} 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..c1b509297 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java @@ -1,8 +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.Payload; import io.rsocket.RSocket; +import io.rsocket.RaceTestConstants; import io.rsocket.core.RSocketConnector; +import io.rsocket.internal.subscriber.AssertSubscriber; +import io.rsocket.test.util.TestClientTransport; import io.rsocket.transport.ClientTransport; import io.rsocket.util.EmptyPayload; import io.rsocket.util.RSocketProxy; @@ -17,10 +35,12 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; 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; @@ -39,49 +59,56 @@ static void afterAll() { } @Test - public void shouldDeliverAllTheRequestsWithRoundRobinStrategy() throws Exception { + public void shouldDeliverAllTheRequestsWithRoundRobinStrategy() { final AtomicInteger counter = new AtomicInteger(); - final ClientTransport mockTransport = Mockito.mock(ClientTransport.class); - final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + final ClientTransport mockTransport = new TestClientTransport(); + final RSocket rSocket = + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + counter.incrementAndGet(); + return Mono.empty(); + } + }; + 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( - 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(); + .then(im -> Mono.just(new TestRSocket(rSocket))); + + final List collectionOfDestination1 = + Collections.singletonList(LoadbalanceTarget.from("1", mockTransport)); + final List collectionOfDestination2 = + Collections.singletonList(LoadbalanceTarget.from("2", mockTransport)); + final List collectionOfDestinations1And2 = + Arrays.asList( + LoadbalanceTarget.from("1", mockTransport), LoadbalanceTarget.from("2", mockTransport)); + + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final Sinks.Many> source = + Sinks.unsafe().many().unicast().onBackpressureError(); final RSocketPool rSocketPool = - new RSocketPool(rSocketConnectorMock, source, new RoundRobinLoadbalanceStrategy()); + new RSocketPool( + rSocketConnectorMock, source.asFlux(), new RoundRobinLoadbalanceStrategy()); + final Mono fnfSource = + Mono.defer(() -> rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE)); RaceTestUtils.race( () -> { for (int j = 0; j < 1000; j++) { - Mono.defer(() -> rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE)) - .retry() - .subscribe(); + fnfSource.subscribe(new RetrySubscriber(fnfSource)); } }, () -> { 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))); + source.emitNext(Collections.emptyList(), Sinks.EmitFailureHandler.FAIL_FAST); + source.emitNext(collectionOfDestination1, Sinks.EmitFailureHandler.FAIL_FAST); + source.emitNext(collectionOfDestinations1And2, Sinks.EmitFailureHandler.FAIL_FAST); + source.emitNext(collectionOfDestination1, Sinks.EmitFailureHandler.FAIL_FAST); + source.emitNext(collectionOfDestination2, Sinks.EmitFailureHandler.FAIL_FAST); + source.emitNext(Collections.emptyList(), Sinks.EmitFailureHandler.FAIL_FAST); + source.emitNext(collectionOfDestination2, Sinks.EmitFailureHandler.FAIL_FAST); + source.emitNext(collectionOfDestinations1And2, Sinks.EmitFailureHandler.FAIL_FAST); } }); @@ -108,37 +135,47 @@ public void shouldDeliverAllTheRequestsWithWeightedStrategy() throws Interrupted .then(im -> Mono.just(new TestRSocket(weightedRSocket1))); Mockito.when(rSocketConnectorMock.connect(mockTransport2)) .then(im -> Mono.just(new TestRSocket(weightedRSocket2))); + final List collectionOfDestination1 = Collections.singletonList(target1); + final List collectionOfDestination2 = Collections.singletonList(target2); + final List collectionOfDestinations1And2 = Arrays.asList(target1, target2); - for (int i = 0; i < 1000; i++) { - final TestPublisher> source = TestPublisher.create(); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final Sinks.Many> source = + Sinks.unsafe().many().unicast().onBackpressureError(); final RSocketPool rSocketPool = new RSocketPool( rSocketConnectorMock, - source, + source.asFlux(), 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()); + final Mono fnfSource = + Mono.defer(() -> rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE)); RaceTestUtils.race( () -> { for (int j = 0; j < 1000; j++) { - Mono.defer(() -> rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE)) - .retry() - .subscribe(aVoid -> {}, Throwable::printStackTrace); + fnfSource.subscribe(new RetrySubscriber(fnfSource)); } }, () -> { for (int j = 0; j < 100; j++) { - source.next(Collections.emptyList()); - 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(target2)); + source.emitNext(Collections.emptyList(), Sinks.EmitFailureHandler.FAIL_FAST); + source.emitNext(collectionOfDestination1, Sinks.EmitFailureHandler.FAIL_FAST); + source.emitNext(collectionOfDestinations1And2, Sinks.EmitFailureHandler.FAIL_FAST); + source.emitNext(collectionOfDestination1, Sinks.EmitFailureHandler.FAIL_FAST); + source.emitNext(collectionOfDestination2, Sinks.EmitFailureHandler.FAIL_FAST); + source.emitNext(Collections.emptyList(), Sinks.EmitFailureHandler.FAIL_FAST); + source.emitNext(collectionOfDestination2, Sinks.EmitFailureHandler.FAIL_FAST); + source.emitNext(collectionOfDestinations1And2, Sinks.EmitFailureHandler.FAIL_FAST); } }); @@ -283,9 +320,88 @@ 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 MonoProcessor processor = MonoProcessor.create(); + final Sinks.Empty sink = Sinks.empty(); public TestRSocket(RSocket rSocket) { super(rSocket); @@ -293,12 +409,16 @@ public TestRSocket(RSocket rSocket) { @Override public Mono onClose() { - return processor; + return sink.asMono(); } @Override public void dispose() { - processor.onComplete(); + sink.tryEmitEmpty(); + } + + public RSocket source() { + return source; } } @@ -322,4 +442,29 @@ public Mono fireAndForget(Payload payload) { }); } } + + static class RetrySubscriber implements CoreSubscriber { + + final Publisher source; + + private RetrySubscriber(Publisher source) { + this.source = source; + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Void unused) {} + + @Override + public void onError(Throwable t) { + source.subscribe(this); + } + + @Override + public void onComplete() {} + } } 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 a177bf2f0..e43068dbd 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategyTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/RoundRobinLoadbalanceStrategyTest.java @@ -2,6 +2,7 @@ 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.EmptyPayload; @@ -66,7 +67,7 @@ public Mono fireAndForget(Payload payload) { final RSocketPool rSocketPool = new RSocketPool(rSocketConnectorMock, source, new RoundRobinLoadbalanceStrategy()); - for (int j = 0; j < 1000; j++) { + for (int j = 0; j < RaceTestConstants.REPEATS; j++) { rSocketPool.select().fireAndForget(EmptyPayload.INSTANCE).subscribe(); } @@ -108,55 +109,62 @@ public Mono fireAndForget(Payload payload) { final RSocketPool rSocketPool = new RSocketPool(rSocketConnectorMock, source, new RoundRobinLoadbalanceStrategy()); - for (int j = 0; j < 1000; j++) { + 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(1000, Offset.offset(1)); + Assertions.assertThat(counter1.get()).isCloseTo(RaceTestConstants.REPEATS, Offset.offset(1)); source.next(Collections.emptyList()); - for (int j = 0; j < 1000; j++) { + 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(2000, Offset.offset(1)); + 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 < 1000; j++) { + for (int j = 0; j < RaceTestConstants.REPEATS; 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)); + Assertions.assertThat(counter1.get()) + .isCloseTo(RaceTestConstants.REPEATS * 2 + RaceTestConstants.REPEATS / 2, Offset.offset(1)); + Assertions.assertThat(counter2.get()) + .isCloseTo(RaceTestConstants.REPEATS / 2, Offset.offset(1)); source.next(Collections.singletonList(LoadbalanceTarget.from("2", mockTransport1))); - for (int j = 0; j < 1000; j++) { + for (int j = 0; j < RaceTestConstants.REPEATS; 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)); + Assertions.assertThat(counter1.get()) + .isCloseTo(RaceTestConstants.REPEATS * 2 + RaceTestConstants.REPEATS / 2, Offset.offset(1)); + Assertions.assertThat(counter2.get()) + .isCloseTo(RaceTestConstants.REPEATS + RaceTestConstants.REPEATS / 2, Offset.offset(1)); source.next( Arrays.asList( LoadbalanceTarget.from("1", mockTransport1), LoadbalanceTarget.from("2", mockTransport2))); - for (int j = 0; j < 1000; j++) { + for (int j = 0; j < RaceTestConstants.REPEATS; 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)); + Assertions.assertThat(counter1.get()) + .isCloseTo(RaceTestConstants.REPEATS * 3, Offset.offset(1)); + Assertions.assertThat(counter2.get()) + .isCloseTo(RaceTestConstants.REPEATS * 2, Offset.offset(1)); } } 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..8cc254cbb --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategyTest.java @@ -0,0 +1,254 @@ +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()) + .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 + 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(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); + + 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(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( + 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(Math.round(RaceTestConstants.REPEATS * 5 * 0.1f))); + Assertions.assertThat(counter2.get()) + .isCloseTo( + RaceTestConstants.REPEATS * 2, + Offset.offset(Math.round(RaceTestConstants.REPEATS * 5 * 0.1f))); + } + + 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; + } + } +} 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 a39caed2b..5c8d40306 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 { @@ -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 new file mode 100644 index 000000000..8229bf42b --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java @@ -0,0 +1,470 @@ +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(); + 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(); + keepAliveSupport.dispose(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } + } + + @Test + void sessionTerminationOnWrongFrameTest() { + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); + 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(); + keepAliveSupport.dispose(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } + } + + @Test + void shouldErrorWithNoRetriesOnErrorFrameTest() { + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); + 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(); + keepAliveSupport.dispose(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } + } + + @Test + void shouldTerminateConnectionOnIllegalStateInKeepAliveFrame() { + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); + 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(); + keepAliveSupport.dispose(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } + } +} 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..bba40d674 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,547 @@ -// 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 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 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() { + 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.firstAvailableFramePosition).isZero(); + + assertSubscriber.assertValueCount(2).values().forEach(ByteBuf::release); + + assertThat(frame1.refCnt()).isZero(); + assertThat(frame2.refCnt()).isZero(); + } + + @Test + void saveWithoutTailRemoval() { + 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.firstAvailableFramePosition).isZero(); + + assertSubscriber.assertValueCount(1).values().forEach(ByteBuf::release); + + assertThat(frame.refCnt()).isOne(); + } + + @Test + void saveRemoveOneFromTail() { + 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.firstAvailableFramePosition).isEqualTo(frame1.readableBytes()); + + assertSubscriber.assertValueCount(2).values().forEach(ByteBuf::release); + + assertThat(frame1.refCnt()).isZero(); + assertThat(frame2.refCnt()).isOne(); + } + + @Test + void saveRemoveTwoFromTail() { + 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.firstAvailableFramePosition).isEqualTo(size(frame1, frame2)); + + assertSubscriber.assertValueCount(3).values().forEach(ByteBuf::release); + + assertThat(frame1.refCnt()).isZero(); + assertThat(frame2.refCnt()).isZero(); + assertThat(frame3.refCnt()).isOne(); + } + + @Test + void saveBiggerThanStore() { + 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.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(); + } + + @Test + void releaseFrames() { + 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.firstAvailableFramePosition).isEqualTo(size(frame1, frame2)); + + assertSubscriber.assertValueCount(3).values().forEach(ByteBuf::release); + + assertThat(frame1.refCnt()).isZero(); + assertThat(frame2.refCnt()).isZero(); + assertThat(frame3.refCnt()).isOne(); + } + + @Test + void receiveImpliedPosition() { + 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", Unpooled.EMPTY_BUFFER, 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); + } +} 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..b5625bf8e --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java @@ -0,0 +1,190 @@ +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(); + 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(); + 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/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/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-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-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-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-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-examples/build.gradle b/rsocket-examples/build.gradle index e2a7ad31a..4059eb957 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -24,7 +24,14 @@ 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") + implementation 'com.netflix.concurrency-limits:concurrency-limits-core' + implementation "io.micrometer:micrometer-core" + implementation "io.micrometer:micrometer-tracing" + implementation project(":rsocket-micrometer") runtimeOnly 'ch.qos.logback:logback-classic' @@ -33,11 +40,11 @@ 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" - // 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/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(); + } +} 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 de27bcb9b..1924668fb 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. @@ -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,12 +30,13 @@ 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.UnicastProcessor; +import reactor.core.publisher.Sinks; import reactor.core.scheduler.Schedulers; public class TcpIntegrationTest { @@ -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,23 +145,24 @@ 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<>(); - 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 +182,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-examples/src/test/java/io/rsocket/integration/TestingStreaming.java b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java index 7d34ba478..cd96584ed 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(); @@ -76,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(); } 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-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-load-balancer/build.gradle b/rsocket-load-balancer/build.gradle index 29003feaf..6d91324ae 100644 --- a/rsocket-load-balancer/build.gradle +++ b/rsocket-load-balancer/build.gradle @@ -17,8 +17,7 @@ plugins { id 'java-library' id 'maven-publish' - id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' + id 'signing' } dependencies { @@ -33,10 +32,7 @@ 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/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-load-balancer/src/test/java/io/rsocket/client/TimeoutClientTest.java b/rsocket-load-balancer/src/test/java/io/rsocket/client/TimeoutClientTest.java index 9a5ac644b..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,15 +16,14 @@ 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.Test; +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 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-micrometer/build.gradle b/rsocket-micrometer/build.gradle index 4be616623..debf02f34 100644 --- a/rsocket-micrometer/build.gradle +++ b/rsocket-micrometer/build.gradle @@ -17,13 +17,14 @@ plugins { id 'java-library' id 'maven-publish' - id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' + id 'signing' } dependencies { api project(':rsocket-core') + api 'io.micrometer:micrometer-observation' api 'io.micrometer:micrometer-core' + api 'io.micrometer:micrometer-tracing' implementation 'org.slf4j:slf4j-api' @@ -37,4 +38,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-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..fb80ea317 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java @@ -0,0 +1,208 @@ +/* + * 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.annotation.Nullable; +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 { + + /** Aligned with ObservationThreadLocalAccessor#KEY */ + private static final String MICROMETER_OBSERVATION_KEY = "micrometer.observation"; + + private final ObservationRegistry observationRegistry; + + @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 + 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 -> observe(input, payload, frameType, observation, contextView)); + } + + 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, + 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.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; + } + return input + .apply(newPayload) + .doOnError(observation::error) + .doFinally(signalType -> observation.stop()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, observation)); + } + + @Override + public Flux requestStream(Payload payload) { + return observationFlux( + super::requestStream, + payload, + 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 observationFlux( + p -> super.requestChannel(flux.skip(1).startWith(p)), + firstPayload, + FrameType.REQUEST_CHANNEL, + RSocketObservationDocumentation.RSOCKET_REQUESTER_REQUEST_CHANNEL); + } + return flux; + }); + } + + 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 parentObservation = + contextView.getOrDefault(MICROMETER_OBSERVATION_KEY, null); + Observation newObservation = + 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()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); + }); + } + + private void setContextualName(FrameType frameType, String route, Observation newObservation) { + if (StringUtils.isNotBlank(route)) { + newObservation.contextualName(frameType.name() + " " + route); + } else { + newObservation.contextualName(frameType.name()); + } + } +} 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..9ed27adf3 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java @@ -0,0 +1,179 @@ +/* + * 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; +import reactor.util.annotation.Nullable; + +/** + * Tracing representation of a {@link RSocketProxy} for the responder. + * + * @author Marcin Grzejszczak + * @author Oleh Dokuka + * @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; + + @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 + 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()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); + } + + 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()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); + } + + @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()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); + } + + @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()) + .contextWrite( + context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); + } + 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; + } +} 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..996267d4a --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java @@ -0,0 +1,131 @@ +/* + * 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 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 + 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-test/build.gradle b/rsocket-test/build.gradle index 5ec1a8061..bcdf88f28 100644 --- a/rsocket-test/build.gradle +++ b/rsocket-test/build.gradle @@ -17,8 +17,7 @@ plugins { id 'java-library' id 'maven-publish' - id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' + id 'signing' } dependencies { @@ -29,9 +28,14 @@ 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' +} - // TODO: Remove after JUnit5 migration - implementation 'junit:junit' +jar { + manifest { + attributes("Automatic-Module-Name": "rsocket.test") + } } description = 'Test utilities for RSocket projects' 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() { 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..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 */ -class LeaksTrackingByteBufAllocator implements ByteBufAllocator { +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(); } 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); } } 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); + } + } +} 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..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. @@ -39,12 +39,14 @@ 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; 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; @@ -57,13 +59,14 @@ 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; 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( @@ -94,32 +96,54 @@ static String read(String resourceName) { } @BeforeEach - default void setUp() { + default void setup() { Hooks.onOperatorDebug(); } @AfterEach default void close() { - Hooks.resetOnOperatorDebug(); - getTransportPair().responder.awaitAllInteractionTermination(getTimeout()); - getTransportPair().dispose(); - getTransportPair().awaitClosed(); - 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; + 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(); } } @@ -221,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()); } @@ -236,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()); } @@ -250,7 +274,7 @@ default void largePayloadRequestChannel50() { .requestChannel(payloads) .doOnNext(Payload::release) .as(StepVerifier::create) - .expectNextCount(50) + .thenConsumeWhile(new PayloadPredicate(50)) .expectComplete() .verify(getTimeout()); } @@ -265,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()); } @@ -280,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()); } @@ -296,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()); @@ -317,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) { @@ -328,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()); @@ -460,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( @@ -500,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; @@ -530,7 +563,7 @@ public TransportPair( registry .forConnection( (type, duplexConnection) -> - new AsyncDuplexConnection(duplexConnection)) + new AsyncDuplexConnection(duplexConnection, "server")) .forSocketAcceptor( delegate -> (connectionSetupPayload, sendingSocket) -> @@ -547,7 +580,7 @@ public TransportPair( "Server", duplexConnection, Duration.ofMillis( - ThreadLocalRandom.current().nextInt(10, 1500))) + ThreadLocalRandom.current().nextInt(100, 1000))) : duplexConnection); } }); @@ -555,7 +588,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 +602,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) { @@ -577,7 +611,7 @@ public TransportPair( registry .forConnection( (type, duplexConnection) -> - new AsyncDuplexConnection(duplexConnection)) + new AsyncDuplexConnection(duplexConnection, "client")) .forSocketAcceptor( delegate -> (connectionSetupPayload, sendingSocket) -> @@ -594,7 +628,7 @@ public TransportPair( "Client", duplexConnection, Duration.ofMillis( - ThreadLocalRandom.current().nextInt(1, 2000))) + ThreadLocalRandom.current().nextInt(10, 1500))) : duplexConnection); } }); @@ -602,7 +636,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) { @@ -618,7 +653,7 @@ public TransportPair( @Override public void dispose() { - server.dispose(); + logger.info("terminating transport pair"); client.dispose(); } @@ -634,17 +669,46 @@ public String expectedPayloadMetadata() { return metadata; } - public void awaitClosed() { - server.onClose().and(client.onClose()).block(Duration.ofMinutes(1)); + 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() + .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(); } @@ -662,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( @@ -686,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 { @@ -700,6 +785,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; @@ -709,12 +795,15 @@ private static class DisconnectingDuplexConnection implements DuplexConnection { @Override public void dispose() { + disposables.dispose(); source.dispose(); } @Override public Mono onClose() { - return source.onClose(); + return source + .onClose() + .doOnTerminate(() -> logger.info("[" + this + "] Source Connection is done")); } @Override @@ -733,18 +822,21 @@ public void sendErrorAndClose(RSocketErrorException errorException) { public Flux receive() { return source .receive() + .doOnSubscribe( + __ -> logger.warn("Tag {}. Subscribing Connection[{}]", tag, source.hashCode())) .doOnNext( 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(); + })); } }); } @@ -758,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 @@ -782,24 +887,27 @@ 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() { - 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 @@ -810,7 +918,7 @@ public void request(long n) { @Override public void cancel() { s.cancel(); - closeableMono.onComplete(); + closeableMonoSink.tryEmitEmpty(); } @Override @@ -837,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/build.gradle b/rsocket-transport-local/build.gradle index a5ba84d5c..fc32125e2 100644 --- a/rsocket-transport-local/build.gradle +++ b/rsocket-transport-local/build.gradle @@ -17,8 +17,7 @@ plugins { id 'java-library' id 'maven-publish' - id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' + id 'signing' } dependencies { @@ -33,4 +32,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-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..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 @@ -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 @@ -77,17 +77,17 @@ public Mono connect() { return Mono.error(new IllegalArgumentException("Could not find server: " + name)); } - UnboundedProcessor in = new UnboundedProcessor(); - UnboundedProcessor out = new UnboundedProcessor(); - MonoProcessor closeNotifier = MonoProcessor.create(); + Sinks.One inSink = Sinks.one(); + Sinks.One outSink = Sinks.one(); + UnboundedProcessor in = new UnboundedProcessor(inSink::tryEmitEmpty); + UnboundedProcessor out = new UnboundedProcessor(outSink::tryEmitEmpty); - server - .apply(new LocalDuplexConnection(name, allocator, out, in, closeNotifier)) - .subscribe(); + Mono onClose = inSink.asMono().and(outSink.asMono()); - return Mono.just( - (DuplexConnection) - new LocalDuplexConnection(name, allocator, in, out, closeNotifier)); + 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 6c1782073..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 @@ -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. @@ -29,7 +29,6 @@ import reactor.core.Fuseable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Operators; /** An implementation of {@link DuplexConnection} that connects inside the same JVM. */ @@ -37,9 +36,9 @@ final class LocalDuplexConnection implements DuplexConnection { private final LocalSocketAddress address; private final ByteBufAllocator allocator; - private final Flux in; + private final UnboundedProcessor in; - private final MonoProcessor onClose; + private final Mono onClose; private final UnboundedProcessor out; @@ -55,9 +54,9 @@ final class LocalDuplexConnection implements DuplexConnection { LocalDuplexConnection( String name, ByteBufAllocator allocator, - Flux in, + UnboundedProcessor in, UnboundedProcessor out, - MonoProcessor 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"); @@ -68,12 +67,11 @@ final class LocalDuplexConnection implements DuplexConnection { @Override public void dispose() { out.onComplete(); - onClose.onComplete(); } @Override public boolean isDisposed() { - return onClose.isDisposed(); + return out.isDisposed(); } @Override @@ -84,23 +82,23 @@ public Mono onClose() { @Override public Flux receive() { return in.transform( - Operators.lift((__, actual) -> new ByteBufReleaserOperator(actual))); + Operators.lift( + (__, actual) -> new ByteBufReleaserOperator(actual, this))); } @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 @@ -113,15 +111,23 @@ public SocketAddress remoteAddress() { return address; } + @Override + public String toString() { + return "LocalDuplexConnection{" + "address=" + address + "hash=" + hashCode() + '}'; + } + 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 @@ -134,17 +140,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(); } @@ -156,6 +167,7 @@ public void request(long n) { @Override public void cancel() { s.cancel(); + parent.out.onComplete(); } @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..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 @@ -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,14 +17,18 @@ 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.MonoProcessor; +import reactor.core.publisher.Sinks; import reactor.util.annotation.Nullable; /** @@ -33,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; @@ -71,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(); + } } /** @@ -106,44 +113,66 @@ 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 MonoProcessor onClose = MonoProcessor.create(); + 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.onComplete(); + + Mono.whenDelayError( + activeConnections + .stream() + .peek(DuplexConnection::dispose) + .map(DuplexConnection::onClose) + .collect(Collectors.toList())) + .subscribe(null, onClose::tryEmitError, 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/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/LocalResumableTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java index 8bea7c682..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,22 +19,31 @@ import io.rsocket.test.TransportTest; import java.time.Duration; import java.util.UUID; -import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; -@Disabled("leaking somewhere for no clear reason") 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 new file mode 100644 index 000000000..8ae16a0a5 --- /dev/null +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableWithFragmentationTransportTest.java @@ -0,0 +1,53 @@ +/* + * 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.local; + +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 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.ofMinutes(1); + } + + @Override + public TransportPair getTransportPair() { + return transportPair; + } +} 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") 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/build.gradle b/rsocket-transport-netty/build.gradle index 70db87b3a..39a5ceac5 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -17,13 +17,12 @@ plugins { id 'java-library' id 'maven-publish' - id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' + id 'signing' id "com.google.osdetector" version "1.4.0" } 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 } @@ -41,9 +40,21 @@ 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 } +jar { + manifest { + attributes("Automatic-Module-Name": "rsocket.transport.netty") + } +} + +test { + minHeapSize = "512m" + maxHeapSize = "4096m" +} + description = 'Reactor Netty RSocket transport implementations (TCP, Websocket)' 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..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 @@ -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. @@ -26,11 +26,12 @@ 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. */ public final class TcpDuplexConnection extends BaseDuplexConnection { - + private final String side; private final Connection connection; /** @@ -39,17 +40,19 @@ public final class TcpDuplexConnection extends BaseDuplexConnection { * @param connection the {@link Connection} for managing the server */ public TcpDuplexConnection(Connection connection) { - this.connection = Objects.requireNonNull(connection, "connection must not be null"); + this("unknown", connection); + } - connection - .channel() - .closeFuture() - .addListener( - future -> { - if (!isDisposed()) dispose(); - }); + /** + * 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).then().subscribe(); + connection.outbound().send(sender).then().doFinally(__ -> connection.dispose()).subscribe(); } @Override @@ -64,28 +67,18 @@ public SocketAddress remoteAddress() { @Override protected void doOnClose() { - sender.dispose(); connection.dispose(); } + @Override + public Mono onClose() { + 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)) - .then() - .subscribe( - null, - t -> onClose.onError(t), - () -> { - final Throwable cause = e.getCause(); - if (cause == null) { - onClose.onComplete(); - } else { - onClose.onError(cause); - } - }); + sender.tryEmitFinal(FrameLengthCodec.encode(alloc(), errorFrame.readableBytes(), errorFrame)); } @Override @@ -97,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 b6d542dcb..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 @@ -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. @@ -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; /** @@ -35,7 +36,7 @@ * stitched back on for frames received. */ public final class WebsocketDuplexConnection extends BaseDuplexConnection { - + private final String side; private final Connection connection; /** @@ -44,17 +45,24 @@ 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 - .channel() - .closeFuture() - .addListener( - future -> { - if (!isDisposed()) dispose(); - }); - - connection.outbound().sendObject(sender.map(BinaryWebSocketFrame::new)).then().subscribe(); + .outbound() + .sendObject(sender.map(BinaryWebSocketFrame::new)) + .then() + .doFinally(__ -> connection.dispose()) + .subscribe(); } @Override @@ -69,10 +77,14 @@ public SocketAddress remoteAddress() { @Override protected void doOnClose() { - sender.dispose(); connection.dispose(); } + @Override + public Mono onClose() { + return Mono.whenDelayError(super.onClose(), connection.onTerminate()); + } + @Override public Flux receive() { return connection.inbound().receive(); @@ -81,20 +93,17 @@ public Flux receive() { @Override public void sendErrorAndClose(RSocketErrorException e) { final ByteBuf errorFrame = ErrorFrameCodec.encode(alloc(), 0, e); - connection - .outbound() - .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); - } - }); + 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/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/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) { 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()); }) 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 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/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/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 new file mode 100644 index 000000000..39b3cec67 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableWithFragmentationTransportTest.java @@ -0,0 +1,61 @@ +/* + * 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; +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.BeforeEach; +import reactor.netty.tcp.TcpClient; +import reactor.netty.tcp.TcpServer; + +final class TcpResumableWithFragmentationTransportTest implements TransportTest { + private TransportPair transportPair; + + @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); + } + + @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/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/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(); } } } 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..043f6bc64 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableTransportTest.java @@ -0,0 +1,64 @@ +/* + * 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; +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 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; + + @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); + } + + @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..b1ca65fcc --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableWithFragmentationTransportTest.java @@ -0,0 +1,64 @@ +/* + * 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; +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 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; + + @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); + } + + @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/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() { 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); 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 @@ -