diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..a32c57bb2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Handle line endings automatically for files detected as text +* text=auto + +# These files are text and should be normalized +*.java text diff=java \ No newline at end of file diff --git a/.github/workflows/gradle-all.yml b/.github/workflows/gradle-all.yml new file mode 100644 index 000000000..a1ad36d32 --- /dev/null +++ b/.github/workflows/gradle-all.yml @@ -0,0 +1,44 @@ +name: Branches Java CI + +on: + # Trigger the workflow on push + # but only for the non master/1.0.x branches + push: + branches-ignore: + - 1.0.x + - master + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 14 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build + - name: Publish Packages to Artifactory + if: ${{ matrix.jdk == '1.8' }} + run: ./gradlew -PversionSuffix="-${githubRef#refs/heads/}-SNAPSHOT" -PbuildNumber="${buildNumber}" publishMavenPublicationToGitHubPackagesRepository --stacktrace + env: + 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 new file mode 100644 index 000000000..950dc80ce --- /dev/null +++ b/.github/workflows/gradle-main.yml @@ -0,0 +1,52 @@ +name: Main Branches Java CI + +on: + # Trigger the workflow on push + # but only for the master/1.0.x branch + push: + branches: + - master + - 1.0.x + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 14 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build + - name: Publish Packages to Artifactory + if: ${{ matrix.jdk == '1.8' }} + run: ./gradlew -PversionSuffix="-SNAPSHOT" -PbuildNumber="${buildNumber}" publishMavenPublicationToGitHubPackagesRepository --stacktrace + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + buildNumber: ${{ github.run_number }} + - name: Aggregate test reports with ciMate + if: always() + continue-on-error: true + env: + CIMATE_PROJECT_ID: m84qx17y + run: | + wget -q https://get.cimate.io/release/linux/cimate + chmod +x cimate + ./cimate "**/TEST-*.xml" \ No newline at end of file diff --git a/.github/workflows/gradle-pr.yml b/.github/workflows/gradle-pr.yml new file mode 100644 index 000000000..994450faf --- /dev/null +++ b/.github/workflows/gradle-pr.yml @@ -0,0 +1,31 @@ +name: Pull Request Java CI + +on: [pull_request] + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + jdk: [ 1.8, 11, 14 ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.jdk }} + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build \ No newline at end of file diff --git a/.github/workflows/gradle-release.yml b/.github/workflows/gradle-release.yml new file mode 100644 index 000000000..922eb0e3e --- /dev/null +++ b/.github/workflows/gradle-release.yml @@ -0,0 +1,44 @@ +name: Release Java CI + +on: + # Trigger the workflow on push + push: + # Sequence of patterns matched against refs/tags + tags: + - '*' # Push events to matching *, i.e. 1.0, 20.15.10 + +jobs: + publish: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew clean build -x test + - name: Publish Packages to Sonotype + run: ./gradlew -Pversion="${githubRef#refs/tags/}" -PbuildNumber="${buildNumber}" sign publishMavenPublicationToSonatypeRepository + env: + githubRef: ${{ github.ref }} + 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/.gitignore b/.gitignore index bde7e8f50..92865ccca 100644 --- a/.gitignore +++ b/.gitignore @@ -65,7 +65,7 @@ atlassian-ide-plugin.xml # NetBeans specific files/directories .nbattrs -/bin +**/bin/* #.gitignore in subdirectory .gitignore diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 89f3d4b42..000000000 --- a/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -language: java -jdk: -- oraclejdk8 - -# force upgrade Java8 as per https://github.com/travis-ci/travis-ci/issues/4042 (fixes compilation issue) -addons: - apt: - packages: - - oracle-java8-installer - -dist: trusty -#group: edge -sudo: false -# as per http://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/ - -# script for build and release via Travis to Bintray -script: gradle/buildViaTravis.sh - -# cache between builds -cache: - directories: - - $HOME/.m2 - - $HOME/.gradle - -env: - global: - - secure: "WBCy0hsF96Xybj4n0AUrGY2m5FWCUa30XR+aVElSOO8d7v7BMypAT8mAd+yC2Y+j8WUGpIv59CqgeK1JrYdR9b3qRKhJmoE1Q92TotrxXMTIC9OKuU51LaaOqGYqx4SqiA2AyaikTFPd8um7KZfUpW/dG4IXySsiJ2OKT1jMUq6TmbWHnAYtjbl3u3WdjBQTIZNMtqG1+H1vIpsWyZrvbB4TWlNzhKBAu/YnlzMtvStrDaF7XrCJ2BQdMomQO18NH2gWxUEvLbQb6ip3wFl9CRe6vID7K1dmFwm08RPt9hRPC9yDahlIy8VvuNcWrP42TV+BVYy8V/hfaIo1pPsDBrtmVyc7YZjXSUM68orDFOkRB35qGkNIaAhy5Yt6G9QfwLXJkDFofW5KMKtDFUzf+j4DwS0CiDMF4k6Qq7YN1tYFXE9R8xa6Gv+wTNHqs4RURbYMS9IlbkhKxNbtyuema2sIUbsIfDezIzLI5BnfH2uli7O6/z0/G0Vfmf6A4q5Olm+7uhzMTI0GKheUIKr16SOxABlrwJtLJftzoKz9hYd3b7C9t61vYzccC3rWYobplwIcK2w50gFHQS8HLeiCjo8yjCx+IRSvAGaZIBPQdHCktrEYCVDUTXOxdaD6k6Ef+ppm8Nn+M+iC8x/G1wYE4x1lDqHw3GfhKsEQmiHL/98=" - - secure: "mbB+rv9eWUFQ9/yr2REH2ztH6r/Uq7cq/OJ5WK6yFp0TmPzlJ8jbEVwe/sdAMW2E4qrfMu1c2h3qsVm41pNx0MwEsIW/lTIZRiRmNYon32n+SHlRWyTn8dJeY/p1HoHs450OjLgB4X4jmRmfSt8IQ/w9ZCjF6HVcgR4ctt+myECTNcRidEIOahljnSJmnFFDsKbt2UJN96AfvvhbxcarEKgKLXLd9tQT2GlvEOM+hVOY9hKD5FvIoRp9heyCEAsSBXe+MIWQlh4jx+B4zCajZJ+8KN6M8KIt40lV8z4Zbc11jgq/xULJwkQIuVZvkJ3huIfUrxwLPgYWeai/TR/m3+2jy1hFajt96pnhJzFEz0IBL0wFALwAY1n2R/6uugEUYnDsFcGQGTsO5OeeOixiRPH5HNgfOhInqJoFh/887f+gq7OLXjlRCTsw+S9KknZ3iBpHX/+khurfAUC9khiMvufEq6Wyu0TvxhmGERFrs7uugeJ1VA85SDVQ6Au9MV831PeBGqzHpYG7w2kJj1EiFjBRUhCthxyDfX2b04egozlKF8JEifZ9EVj7pNMQUvVG2c9Wj6M0fG84NusnlZlA16XxAmfLevc9b/BOSSrqc2r9Z1ZvxFnBPP9H94Uqt9ZninhW/T49jRF+lQzD45MTVogzVk77XtdpzUemf4t5mHc=" - - secure: "GcPu3U4o2Dp7QLCqaAo3mGMJTl9yd+w+elXqqt7WDjrjm5p8mrzvQfyiJA7mRJVDTGpgib8fLctL1X1+QOX4fNKElrDUFhE3bWAqwVwHGPK4D3HCb6THD5XVqE4qcPmdLWPkvJ9ZY5nSIfuRVASjZTcc4XSXISK2jUSGar0PNYlo62/OFGvNvMz/qINU9RU7iYdDlL19yd72TKDfuK0UOKhQEGypamEHam3SMNCw/p8Q5K1vQe+Oba3ILCvYHJvqWc2NLjRXJjXfIaOq/NpCK6Lx2U9etdpkb5lyW5Cx1lkzIcRUq8ZUCwbkHog9LJoZGrZFh5AzlZ6kRuejBqu7AISmZy4s9HVAb7AQmNxvXkK9EIt8lavcaHnLYUIfuxvBqK/ptcUN5P/KXCs1DsbpADjB7YbUu/EQ2OAWncV31Z+O4uMHV29eGTtaz9LoK28+mHRfFHqoazWyuUejor6iSSkrCeqsLEvU8o6rH4oenKz7hLlZsJqHGACYtYNYi2CXYlTu0bMX+Hb1EtTu6Awm9Gn04TqVdmNexgF5CdqW4A696i6jlkPpVCt4B4nq4VPs2RMTkjVl3B7uOkDm18u35dncuhgsnMfVmo9cWX5COeyefdh6kdnKsUf0+IPbV/hix/OCP72dpuhxgcyzN+DvaVLzX7YOx7TpJTzPSKNEQZc=" - - secure: "UFJEzDEv6H2Qscg9UgZFVJq5oFvq7nQkVoSuGfh5Y4ZhL9PCK5f3Ft9oYEZOQwXaxWD1qivtJjQV3DdBiqsHkrnPrJ0hi3iYVDJo26xLNtu3welFw5Veqmgu2NuwjaDn6cjRFCJRLzpszMUWO1DvfLJTs3LuJDuXEyAKDw9eQgfOakqO4xeloyXgM7xnoXz11rgqtJNU6snjVPHftXNPTHGsNDlTR7SAIbjYwLMbdIKM2qjzrXkg+a94QOz2stnTDz9V5iYNH+3XXCcYxD9nb1Ol1XGWvtDnNGEhtGmylLdjHXwGLHiW2HOXskLzSkm7ASie1WdyHVHZb4X8LjxCy62S0FPevBgat1a443Khx5HCMYR/8dQrlOI82GYTr8n9U6QQE4Li8XLw64DVP9HGs9jdbsfEdlIsiPWqB6ujlwiO6pyfmQGQCgjALA+oD87uDQLcgh+SDYgE0ZwmwGzbjeynZpoCrEE8A1GHhSwkM9khx6EJFacm9XzqoUGK0wB1f8su+51fqPglF1zye80IFA4wOMMAY+KUc9du/vQ98f0lfjsNSOC02CKYxbA5RaakQMAYjirsZraA57xLmCSIGMhhW4wClQdJBww6LLz463yZU4WPwyqU+ZW12aV5dVLb5RWXIbZKmdT74DfZajHvqgTYpb05L5cJl7ApMspUkKk=" diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 000000000..ef7dd9dda --- /dev/null +++ b/AUTHORS @@ -0,0 +1,21 @@ +benjchristensen = Ben Christensen +gregwhitaker = Greg Whitaker +junaidkhalid = Junaid Khalid +kojilin = Kang-Sze Lin +krisskross = Kristoffer Sjogren +ktoso = Konrad Malawski +lehecka = Ondrej Lehecka +lexs = Alexander Blom +mostroverkhov = Maksym Ostroverkhov +nebhale = Ben Hale +NiteshKant = Nitesh Kant +qweek = Alex Novoselov +rdegnan = Ryland Degnan +robertroeser = Robert Roeser +rstoyanchev = Rossen Stoyanchev +simonbasle = Simon Baslé +somasun = somasun +stevegury = Steve Gury +tmontgomery = Todd L. Montgomery +yschimke = Yuri Schimke +OlegDokuka = Oleh Dokuka diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 47e7b87eb..56a5a7b69 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,22 +6,22 @@ When submitting code, please make every effort to follow existing conventions an ## License -By contributing your code, you agree to license your contribution under the terms of the APLv2: https://github.com/RSocket/reactivesocket-java/blob/master/LICENSE +By contributing your code, you agree to license your contribution under the terms of the APLv2: https://github.com/rsocket/rsocket-java/blob/1.0.x/LICENSE All files are released with the Apache 2.0 license. If you are adding a new file it should have a header like this: -``` -/** - * Copyright 2015 Netflix, Inc. - * +```java +/* + * 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 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..ea8e324f1 --- /dev/null +++ b/NOTICE @@ -0,0 +1,15 @@ +RSocket Java + +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. diff --git a/README.md b/README.md index 9a5ac0897..cda6d3c0a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # RSocket -[![Join the chat at https://gitter.im/RSocket/reactivesocket-java](https://badges.gitter.im/RSocket/reactivesocket-java.svg)](https://gitter.im/RSocket/reactivesocket-java?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Join the chat at https://gitter.im/RSocket/RSocket-Java](https://badges.gitter.im/rsocket/rsocket-java.svg)](https://gitter.im/rsocket/rsocket-java) RSocket is a binary protocol for use on byte stream transports such as TCP, WebSockets, and Aeron. @@ -15,23 +15,38 @@ Learn more at http://rsocket.io ## Build and Binaries - +[![Main Branches Java CI](https://github.com/rsocket/rsocket-java/actions/workflows/gradle-main.yml/badge.svg?branch=1.0.x)](https://github.com/rsocket/rsocket-java/actions/workflows/gradle-main.yml) -Snapshots are available via JFrog. +Releases and milestones are available via Maven Central. Example: ```groovy repositories { - maven { url 'https://oss.jfrog.org/libs-snapshot' } + mavenCentral() + maven { url 'https://repo.spring.io/milestone' } // Reactor milestones (if needed) } +dependencies { + implementation 'io.rsocket:rsocket-core:1.0.5' + implementation 'io.rsocket:rsocket-transport-netty:1.0.5' +} +``` + +Snapshots are available via [oss.jfrog.org](oss.jfrog.org) (OJO). + +Example: +```groovy +repositories { + maven { url 'https://maven.pkg.github.com/rsocket/rsocket-java' } + maven { url 'https://repo.spring.io/snapshot' } // Reactor snapshots (if needed) +} dependencies { - compile 'io.rsocket:reactivesocket:0.9-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:1.0.6-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-netty:1.0.6-SNAPSHOT' } ``` -No releases to Maven Central or JCenter have occurred yet. ## Development @@ -47,19 +62,82 @@ $./gradlew goJF ## Debugging Frames can be printed out to help debugging. Set the logger `io.rsocket.FrameLogger` to debug to print the frames. +## Requirements + +- Java 8 - heavy dependence on Java 8 functional APIs and java.time, also on Reactor +- Android O - https://github.com/rsocket/rsocket-demo-android-java8 + +## Trivial Client + +```java +package io.rsocket.transport.netty; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.transport.netty.client.WebsocketClientTransport; +import io.rsocket.util.DefaultPayload; +import reactor.core.publisher.Flux; + +import java.net.URI; + +public class ExampleClient { + public static void main(String[] args) { + WebsocketClientTransport ws = WebsocketClientTransport.create(URI.create("ws://rsocket-demo.herokuapp.com/ws")); + RSocket clientRSocket = RSocketConnector.connectWith(ws).block(); + + try { + Flux s = clientRSocket.requestStream(DefaultPayload.create("peace")); + + s.take(10).doOnNext(p -> System.out.println(p.getDataUtf8())).blockLast(); + } finally { + clientRSocket.dispose(); + } + } +} +``` + +## Zero Copy +By default to make RSocket easier to use it copies the incoming Payload. Copying the payload comes at cost to performance +and latency. If you want to use zero copy you must disable this. To disable copying you must include a `payloadDecoder` +argument in your `RSocketFactory`. This will let you manage the Payload without copying the data from the underlying +transport. You must free the Payload when you are done with them +or you will get a memory leak. Used correctly this will reduce latency and increase performance. + +### Example Server setup +```java +RSocketServer.create(new PingHandler()) + // Enable Zero Copy + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(TcpServerTransport.create(7878)) + .block() + .onClose() + .block(); +``` + +### Example Client setup +```java +RSocket clientRSocket = + RSocketConnector.create() + // Enable Zero Copy + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(TcpClientTransport.create(7878)) + .block(); +``` + ## Bugs and Feedback For bugs, questions and discussions please use the [Github Issues](https://github.com/RSocket/reactivesocket-java/issues). ## LICENSE -Copyright 2015 Netflix, Inc. +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, diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 000000000..6ba6755a6 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,47 @@ +## Usage of JMH tasks + +Only execute specific benchmark(s) (wildcards are added before and after): +``` +../gradlew jmh --include="(BenchmarkPrimary|OtherBench)" +``` +If you want to specify the wildcards yourself, you can pass the full regexp: +``` +../gradlew jmh --fullInclude=.*MyBenchmark.* +``` + +Specify extra profilers: +``` +../gradlew jmh --profilers="gc,stack" +``` + +Prominent profilers (for full list call `jmhProfilers` task): +- comp - JitCompilations, tune your iterations +- stack - which methods used most time +- gc - print garbage collection stats +- hs_thr - thread usage + +Change report format from JSON to one of [CSV, JSON, NONE, SCSV, TEXT]: +``` +./gradlew jmh --format=csv +``` + +Specify JVM arguments: +``` +../gradlew jmh --jvmArgs="-Dtest.cluster=local" +``` + +Run in verification mode (execute benchmarks with minimum of fork/warmup-/benchmark-iterations): +``` +../gradlew jmh --verify=true +``` + +## Comparing with the baseline +If you wish you run two sets of benchmarks, one for the current change and another one for the "baseline", +there is an additional task `jmhBaseline` that will use the latest release: +``` +../gradlew jmh jmhBaseline --include=MyBenchmark +``` + +## Resources +- http://tutorials.jenkov.com/java-performance/jmh.html (Introduction) +- http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/ (Samples) diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle new file mode 100644 index 000000000..f07f7c6f5 --- /dev/null +++ b/benchmarks/build.gradle @@ -0,0 +1,167 @@ +apply plugin: 'java' +apply plugin: 'idea' + +configurations { + current + baseline { + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' + } +} + +dependencies { + // Use the baseline to avoid using new APIs in the benchmarks + compileOnly "io.rsocket:rsocket-core:${perfBaselineVersion}" + compileOnly "io.rsocket:rsocket-transport-local:${perfBaselineVersion}" + + implementation "org.openjdk.jmh:jmh-core:1.21" + annotationProcessor "org.openjdk.jmh:jmh-generator-annprocess:1.21" + + current project(':rsocket-core') + current project(':rsocket-transport-local') + baseline "io.rsocket:rsocket-core:${perfBaselineVersion}", { + changing = true + } + baseline "io.rsocket:rsocket-transport-local:${perfBaselineVersion}", { + changing = true + } +} + +task jmhProfilers(type: JavaExec, description:'Lists the available profilers for the jmh task', group: 'Development') { + classpath = sourceSets.main.runtimeClasspath + main = 'org.openjdk.jmh.Main' + args '-lprof' +} + +task jmh(type: JmhExecTask, description: 'Executing JMH benchmarks') { + classpath = sourceSets.main.runtimeClasspath + configurations.current +} + +task jmhBaseline(type: JmhExecTask, description: 'Executing JMH baseline benchmarks') { + classpath = sourceSets.main.runtimeClasspath + configurations.baseline +} + +clean { + delete "${projectDir}/src/main/generated" +} + +class JmhExecTask extends JavaExec { + + private String include; + private String fullInclude; + private String exclude; + private String format = "json"; + private String profilers; + private String jmhJvmArgs; + private String verify; + + public JmhExecTask() { + super(); + } + + public String getInclude() { + return include; + } + + @Option(option = "include", description="configure bench inclusion using substring") + public void setInclude(String include) { + this.include = include; + } + + public String getFullInclude() { + return fullInclude; + } + + @Option(option = "fullInclude", description = "explicitly configure bench inclusion using full JMH style regexp") + public void setFullInclude(String fullInclude) { + this.fullInclude = fullInclude; + } + + public String getExclude() { + return exclude; + } + + @Option(option = "exclude", description = "explicitly configure bench exclusion using full JMH style regexp") + public void setExclude(String exclude) { + this.exclude = exclude; + } + + public String getFormat() { + return format; + } + + @Option(option = "format", description = "configure report format") + public void setFormat(String format) { + this.format = format; + } + + public String getProfilers() { + return profilers; + } + + @Option(option = "profilers", description = "configure jmh profiler(s) to use, comma separated") + public void setProfilers(String profilers) { + this.profilers = profilers; + } + + public String getJmhJvmArgs() { + return jmhJvmArgs; + } + + @Option(option = "jvmArgs", description = "configure additional JMH JVM arguments, comma separated") + public void setJmhJvmArgs(String jvmArgs) { + this.jmhJvmArgs = jvmArgs; + } + + public String getVerify() { + return verify; + } + + @Option(option = "verify", description = "run in verify mode") + public void setVerify(String verify) { + this.verify = verify; + } + + @TaskAction + public void exec() { + setMain("org.openjdk.jmh.Main"); + File resultFile = getProject().file("build/reports/" + getName() + "/result." + format); + + if (include != null) { + args(".*" + include + ".*"); + } + else if (fullInclude != null) { + args(fullInclude); + } + + if(exclude != null) { + args("-e", exclude); + } + if(verify != null) { // execute benchmarks with the minimum amount of execution (only to check if they are working) + System.out.println("Running in verify mode"); + args("-f", 1); + args("-wi", 1); + args("-i", 1); + } + args("-foe", "true"); //fail-on-error + args("-v", "NORMAL"); //verbosity [SILENT, NORMAL, EXTRA] + if(profilers != null) { + for (String prof : profilers.split(",")) { + args("-prof", prof); + } + } + args("-jvmArgsPrepend", "-Xmx3072m"); + args("-jvmArgsPrepend", "-Xms3072m"); + if(jmhJvmArgs != null) { + for(String jvmArg : jmhJvmArgs.split(" ")) { + args("-jvmArgsPrepend", jvmArg); + } + } + args("-rf", format); + args("-rff", resultFile); + + System.out.println("\nExecuting JMH with: " + getArgs() + "\n"); + resultFile.getParentFile().mkdirs(); + + super.exec(); + } +} diff --git a/benchmarks/src/main/java/io/rsocket/MaxPerfSubscriber.java b/benchmarks/src/main/java/io/rsocket/MaxPerfSubscriber.java new file mode 100644 index 000000000..2e6fa6acc --- /dev/null +++ b/benchmarks/src/main/java/io/rsocket/MaxPerfSubscriber.java @@ -0,0 +1,37 @@ +package io.rsocket; + +import java.util.concurrent.CountDownLatch; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; + +public class MaxPerfSubscriber extends CountDownLatch implements CoreSubscriber { + + final Blackhole blackhole; + + public MaxPerfSubscriber(Blackhole blackhole) { + super(1); + this.blackhole = blackhole; + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(T payload) { + blackhole.consume(payload); + } + + @Override + public void onError(Throwable t) { + blackhole.consume(t); + countDown(); + } + + @Override + public void onComplete() { + countDown(); + } +} diff --git a/benchmarks/src/main/java/io/rsocket/PayloadsMaxPerfSubscriber.java b/benchmarks/src/main/java/io/rsocket/PayloadsMaxPerfSubscriber.java new file mode 100644 index 000000000..7a7a1fdd6 --- /dev/null +++ b/benchmarks/src/main/java/io/rsocket/PayloadsMaxPerfSubscriber.java @@ -0,0 +1,16 @@ +package io.rsocket; + +import org.openjdk.jmh.infra.Blackhole; + +public class PayloadsMaxPerfSubscriber extends MaxPerfSubscriber { + + public PayloadsMaxPerfSubscriber(Blackhole blackhole) { + super(blackhole); + } + + @Override + public void onNext(Payload payload) { + payload.release(); + super.onNext(payload); + } +} diff --git a/benchmarks/src/main/java/io/rsocket/PayloadsPerfSubscriber.java b/benchmarks/src/main/java/io/rsocket/PayloadsPerfSubscriber.java new file mode 100644 index 000000000..efc116958 --- /dev/null +++ b/benchmarks/src/main/java/io/rsocket/PayloadsPerfSubscriber.java @@ -0,0 +1,16 @@ +package io.rsocket; + +import org.openjdk.jmh.infra.Blackhole; + +public class PayloadsPerfSubscriber extends PerfSubscriber { + + public PayloadsPerfSubscriber(Blackhole blackhole) { + super(blackhole); + } + + @Override + public void onNext(Payload payload) { + payload.release(); + super.onNext(payload); + } +} diff --git a/benchmarks/src/main/java/io/rsocket/PerfSubscriber.java b/benchmarks/src/main/java/io/rsocket/PerfSubscriber.java new file mode 100644 index 000000000..92577d95c --- /dev/null +++ b/benchmarks/src/main/java/io/rsocket/PerfSubscriber.java @@ -0,0 +1,41 @@ +package io.rsocket; + +import java.util.concurrent.CountDownLatch; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; + +public class PerfSubscriber extends CountDownLatch implements CoreSubscriber { + + final Blackhole blackhole; + + Subscription s; + + public PerfSubscriber(Blackhole blackhole) { + super(1); + this.blackhole = blackhole; + } + + @Override + public void onSubscribe(Subscription s) { + this.s = s; + s.request(1); + } + + @Override + public void onNext(T payload) { + blackhole.consume(payload); + s.request(1); + } + + @Override + public void onError(Throwable t) { + blackhole.consume(t); + countDown(); + } + + @Override + public void onComplete() { + countDown(); + } +} diff --git a/benchmarks/src/main/java/io/rsocket/core/RSocketPerf.java b/benchmarks/src/main/java/io/rsocket/core/RSocketPerf.java new file mode 100644 index 000000000..f78843f5b --- /dev/null +++ b/benchmarks/src/main/java/io/rsocket/core/RSocketPerf.java @@ -0,0 +1,170 @@ +package io.rsocket.core; + +import io.rsocket.AbstractRSocket; +import io.rsocket.Closeable; +import io.rsocket.Payload; +import io.rsocket.PayloadsMaxPerfSubscriber; +import io.rsocket.PayloadsPerfSubscriber; +import io.rsocket.RSocket; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.transport.local.LocalClientTransport; +import io.rsocket.transport.local.LocalServerTransport; +import io.rsocket.util.EmptyPayload; +import java.lang.reflect.Field; +import java.util.Queue; +import java.util.concurrent.locks.LockSupport; +import java.util.stream.IntStream; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@BenchmarkMode(Mode.Throughput) +@Fork( + value = 1 // , jvmArgsAppend = {"-Dio.netty.leakDetection.level=advanced"} + ) +@Warmup(iterations = 10) +@Measurement(iterations = 10, time = 20) +@State(Scope.Benchmark) +public class RSocketPerf { + + static final Payload PAYLOAD = EmptyPayload.INSTANCE; + static final Mono PAYLOAD_MONO = Mono.just(PAYLOAD); + static final Flux PAYLOAD_FLUX = + Flux.fromArray(IntStream.range(0, 100000).mapToObj(__ -> PAYLOAD).toArray(Payload[]::new)); + + RSocket client; + Closeable server; + Queue clientsQueue; + + @TearDown + public void tearDown() { + client.dispose(); + server.dispose(); + } + + @TearDown(Level.Iteration) + public void awaitToBeConsumed() { + while (!clientsQueue.isEmpty()) { + LockSupport.parkNanos(1000); + } + } + + @Setup + public void setUp() throws NoSuchFieldException, IllegalAccessException { + server = + RSocketServer.create( + (setup, sendingSocket) -> + Mono.just( + new AbstractRSocket() { + + @Override + public Mono fireAndForget(Payload payload) { + payload.release(); + return Mono.empty(); + } + + @Override + public Mono requestResponse(Payload payload) { + payload.release(); + return PAYLOAD_MONO; + } + + @Override + public Flux requestStream(Payload payload) { + payload.release(); + return PAYLOAD_FLUX; + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads); + } + })) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(LocalServerTransport.create("server")) + .block(); + + client = + RSocketConnector.create() + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(LocalClientTransport.create("server")) + .block(); + + Field sendProcessorField = RSocketRequester.class.getDeclaredField("sendProcessor"); + sendProcessorField.setAccessible(true); + + clientsQueue = (Queue) sendProcessorField.get(client); + } + + @Benchmark + @SuppressWarnings("unchecked") + public PayloadsPerfSubscriber fireAndForget(Blackhole blackhole) throws InterruptedException { + PayloadsPerfSubscriber subscriber = new PayloadsPerfSubscriber(blackhole); + client.fireAndForget(PAYLOAD).subscribe((CoreSubscriber) subscriber); + subscriber.await(); + + return subscriber; + } + + @Benchmark + public PayloadsPerfSubscriber requestResponse(Blackhole blackhole) throws InterruptedException { + PayloadsPerfSubscriber subscriber = new PayloadsPerfSubscriber(blackhole); + client.requestResponse(PAYLOAD).subscribe(subscriber); + subscriber.await(); + + return subscriber; + } + + @Benchmark + public PayloadsPerfSubscriber requestStreamWithRequestByOneStrategy(Blackhole blackhole) + throws InterruptedException { + PayloadsPerfSubscriber subscriber = new PayloadsPerfSubscriber(blackhole); + client.requestStream(PAYLOAD).subscribe(subscriber); + subscriber.await(); + + return subscriber; + } + + @Benchmark + public PayloadsMaxPerfSubscriber requestStreamWithRequestAllStrategy(Blackhole blackhole) + throws InterruptedException { + PayloadsMaxPerfSubscriber subscriber = new PayloadsMaxPerfSubscriber(blackhole); + client.requestStream(PAYLOAD).subscribe(subscriber); + subscriber.await(); + + return subscriber; + } + + @Benchmark + public PayloadsPerfSubscriber requestChannelWithRequestByOneStrategy(Blackhole blackhole) + throws InterruptedException { + PayloadsPerfSubscriber subscriber = new PayloadsPerfSubscriber(blackhole); + client.requestChannel(PAYLOAD_FLUX).subscribe(subscriber); + subscriber.await(); + + return subscriber; + } + + @Benchmark + public PayloadsMaxPerfSubscriber requestChannelWithRequestAllStrategy(Blackhole blackhole) + throws InterruptedException { + PayloadsMaxPerfSubscriber subscriber = new PayloadsMaxPerfSubscriber(blackhole); + client.requestChannel(PAYLOAD_FLUX).subscribe(subscriber); + subscriber.await(); + + return subscriber; + } +} diff --git a/benchmarks/src/main/java/io/rsocket/core/StreamIdSupplierPerf.java b/benchmarks/src/main/java/io/rsocket/core/StreamIdSupplierPerf.java new file mode 100644 index 000000000..6b4f3f624 --- /dev/null +++ b/benchmarks/src/main/java/io/rsocket/core/StreamIdSupplierPerf.java @@ -0,0 +1,43 @@ +package io.rsocket.core; + +import io.netty.util.collection.IntObjectMap; +import io.rsocket.internal.SynchronizedIntObjectHashMap; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.Throughput) +@Fork( + value = 1 // , jvmArgsAppend = {"-Dio.netty.leakDetection.level=advanced"} + ) +@Warmup(iterations = 10) +@Measurement(iterations = 10) +@State(Scope.Thread) +public class StreamIdSupplierPerf { + @Benchmark + public void benchmarkStreamId(Input input) { + int i = input.supplier.nextStreamId(input.map); + input.bh.consume(i); + } + + @State(Scope.Benchmark) + public static class Input { + Blackhole bh; + IntObjectMap map; + StreamIdSupplier supplier; + + @Setup + public void setup(Blackhole bh) { + this.supplier = StreamIdSupplier.clientSupplier(); + this.bh = bh; + this.map = new SynchronizedIntObjectHashMap(); + } + } +} diff --git a/benchmarks/src/main/java/io/rsocket/frame/FrameHeaderFlyweightPerf.java b/benchmarks/src/main/java/io/rsocket/frame/FrameHeaderFlyweightPerf.java new file mode 100644 index 000000000..b4ac808d0 --- /dev/null +++ b/benchmarks/src/main/java/io/rsocket/frame/FrameHeaderFlyweightPerf.java @@ -0,0 +1,55 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.Throughput) +@Fork( + value = 1 // , jvmArgsAppend = {"-Dio.netty.leakDetection.level=advanced"} + ) +@Warmup(iterations = 10) +@Measurement(iterations = 10) +@State(Scope.Thread) +public class FrameHeaderFlyweightPerf { + + @Benchmark + public void encode(Input input) { + ByteBuf byteBuf = FrameHeaderCodec.encodeStreamZero(input.allocator, FrameType.SETUP, 0); + boolean release = byteBuf.release(); + input.bh.consume(release); + } + + @Benchmark + public void decode(Input input) { + ByteBuf frame = input.frame; + FrameType frameType = FrameHeaderCodec.frameType(frame); + int streamId = FrameHeaderCodec.streamId(frame); + int flags = FrameHeaderCodec.flags(frame); + input.bh.consume(streamId); + input.bh.consume(flags); + input.bh.consume(frameType); + } + + @State(Scope.Benchmark) + public static class Input { + Blackhole bh; + FrameType frameType; + ByteBufAllocator allocator; + ByteBuf frame; + + @Setup + public void setup(Blackhole bh) { + this.bh = bh; + this.frameType = FrameType.REQUEST_RESPONSE; + allocator = ByteBufAllocator.DEFAULT; + frame = FrameHeaderCodec.encode(allocator, 123, FrameType.SETUP, 0); + } + + @TearDown + public void teardown() { + frame.release(); + } + } +} diff --git a/benchmarks/src/main/java/io/rsocket/frame/FrameTypePerf.java b/benchmarks/src/main/java/io/rsocket/frame/FrameTypePerf.java new file mode 100644 index 000000000..efa22104f --- /dev/null +++ b/benchmarks/src/main/java/io/rsocket/frame/FrameTypePerf.java @@ -0,0 +1,38 @@ +package io.rsocket.frame; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.Throughput) +@Fork( + value = 1 // , jvmArgsAppend = {"-Dio.netty.leakDetection.level=advanced"} + ) +@Warmup(iterations = 10) +@Measurement(iterations = 10) +@State(Scope.Thread) +public class FrameTypePerf { + @Benchmark + public void lookup(Input input) { + FrameType frameType = input.frameType; + boolean b = + frameType.canHaveData() + && frameType.canHaveMetadata() + && frameType.isFragmentable() + && frameType.isRequestType() + && frameType.hasInitialRequestN(); + + input.bh.consume(b); + } + + @State(Scope.Benchmark) + public static class Input { + Blackhole bh; + FrameType frameType; + + @Setup + public void setup(Blackhole bh) { + this.bh = bh; + this.frameType = FrameType.REQUEST_RESPONSE; + } + } +} diff --git a/benchmarks/src/main/java/io/rsocket/frame/PayloadFlyweightPerf.java b/benchmarks/src/main/java/io/rsocket/frame/PayloadFlyweightPerf.java new file mode 100644 index 000000000..01d82a08f --- /dev/null +++ b/benchmarks/src/main/java/io/rsocket/frame/PayloadFlyweightPerf.java @@ -0,0 +1,77 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.Throughput) +@Fork( + value = 1 // , jvmArgsAppend = {"-Dio.netty.leakDetection.level=advanced"} + ) +@Warmup(iterations = 10) +@Measurement(iterations = 10) +@State(Scope.Thread) +public class PayloadFlyweightPerf { + + @Benchmark + public void encode(Input input) { + ByteBuf encode = + PayloadFrameCodec.encode( + input.allocator, + 100, + false, + true, + false, + Unpooled.wrappedBuffer(input.metadata), + Unpooled.wrappedBuffer(input.data)); + boolean release = encode.release(); + input.bh.consume(release); + } + + @Benchmark + public void decode(Input input) { + ByteBuf frame = input.payload; + ByteBuf data = PayloadFrameCodec.data(frame); + ByteBuf metadata = PayloadFrameCodec.metadata(frame); + input.bh.consume(data); + input.bh.consume(metadata); + } + + @State(Scope.Benchmark) + public static class Input { + Blackhole bh; + FrameType frameType; + ByteBufAllocator allocator; + ByteBuf payload; + byte[] metadata = new byte[512]; + byte[] data = new byte[4096]; + + @Setup + public void setup(Blackhole bh) { + this.bh = bh; + this.frameType = FrameType.REQUEST_RESPONSE; + allocator = ByteBufAllocator.DEFAULT; + + // Encode a payload and then copy it a single bytebuf + payload = allocator.buffer(); + ByteBuf encode = + PayloadFrameCodec.encode( + allocator, + 100, + false, + true, + false, + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data)); + payload.writeBytes(encode); + encode.release(); + } + + @TearDown + public void teardown() { + payload.release(); + } + } +} diff --git a/benchmarks/src/main/java/io/rsocket/metadata/WellKnownMimeTypePerf.java b/benchmarks/src/main/java/io/rsocket/metadata/WellKnownMimeTypePerf.java new file mode 100644 index 000000000..8f429fc19 --- /dev/null +++ b/benchmarks/src/main/java/io/rsocket/metadata/WellKnownMimeTypePerf.java @@ -0,0 +1,96 @@ +package io.rsocket.metadata; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.Throughput) +@Fork(value = 1) +@Warmup(iterations = 10) +@Measurement(iterations = 10) +@State(Scope.Thread) +public class WellKnownMimeTypePerf { + + // this is the old values() looping implementation of fromIdentifier + private WellKnownMimeType fromIdValuesLoop(int id) { + if (id < 0 || id > 127) { + return WellKnownMimeType.UNPARSEABLE_MIME_TYPE; + } + for (WellKnownMimeType value : WellKnownMimeType.values()) { + if (value.getIdentifier() == id) { + return value; + } + } + return WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE; + } + + // this is the core of the old values() looping implementation of fromString + private WellKnownMimeType fromStringValuesLoop(String mimeType) { + for (WellKnownMimeType value : WellKnownMimeType.values()) { + if (mimeType.equals(value.getString())) { + return value; + } + } + return WellKnownMimeType.UNPARSEABLE_MIME_TYPE; + } + + @Benchmark + public void fromIdArrayLookup(final Blackhole bh) { + // negative lookup + bh.consume(WellKnownMimeType.fromIdentifier(-10)); + bh.consume(WellKnownMimeType.fromIdentifier(-1)); + // too large lookup + bh.consume(WellKnownMimeType.fromIdentifier(129)); + // first lookup + bh.consume(WellKnownMimeType.fromIdentifier(0)); + // middle lookup + bh.consume(WellKnownMimeType.fromIdentifier(37)); + // reserved lookup + bh.consume(WellKnownMimeType.fromIdentifier(63)); + // last lookup + bh.consume(WellKnownMimeType.fromIdentifier(127)); + } + + @Benchmark + public void fromIdValuesLoopLookup(final Blackhole bh) { + // negative lookup + bh.consume(fromIdValuesLoop(-10)); + bh.consume(fromIdValuesLoop(-1)); + // too large lookup + bh.consume(fromIdValuesLoop(129)); + // first lookup + bh.consume(fromIdValuesLoop(0)); + // middle lookup + bh.consume(fromIdValuesLoop(37)); + // reserved lookup + bh.consume(fromIdValuesLoop(63)); + // last lookup + bh.consume(fromIdValuesLoop(127)); + } + + @Benchmark + public void fromStringMapLookup(final Blackhole bh) { + // unknown lookup + bh.consume(WellKnownMimeType.fromString("foo/bar")); + // first lookup + bh.consume(WellKnownMimeType.fromString(WellKnownMimeType.APPLICATION_AVRO.getString())); + // middle lookup + bh.consume(WellKnownMimeType.fromString(WellKnownMimeType.VIDEO_VP8.getString())); + // last lookup + bh.consume( + WellKnownMimeType.fromString( + WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString())); + } + + @Benchmark + public void fromStringValuesLoopLookup(final Blackhole bh) { + // unknown lookup + bh.consume(fromStringValuesLoop("foo/bar")); + // first lookup + bh.consume(fromStringValuesLoop(WellKnownMimeType.APPLICATION_AVRO.getString())); + // middle lookup + bh.consume(fromStringValuesLoop(WellKnownMimeType.VIDEO_VP8.getString())); + // last lookup + bh.consume( + fromStringValuesLoop(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString())); + } +} diff --git a/build.gradle b/build.gradle index f5b25bf63..00c5d2b9d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,67 +13,222 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -buildscript { - repositories { - jcenter() - } - dependencies { - classpath 'io.reactivesocket:gradle-nebula-plugin-reactivesocket:1.0.6' - } -} plugins { - id 'com.github.sherter.google-java-format' version '0.6' + id 'com.github.sherter.google-java-format' version '0.8' apply false + id 'me.champeau.gradle.jmh' version '0.5.0' apply false + id 'io.spring.dependency-management' version '1.0.11.RELEASE' apply false + id 'io.morethan.jmhreport' version '0.9.0' apply false } -description = 'RSocket: stream oriented messaging passing with Reactive Stream semantics.' - -apply plugin: 'reactivesocket-project' +boolean isCiServer = ["CI", "CONTINUOUS_INTEGRATION", "TRAVIS", "CIRCLECI", "bamboo_planKey", "GITHUB_ACTION"].with { + retainAll(System.getenv().keySet()) + return !isEmpty() +} subprojects { - apply plugin: 'reactivesocket-project' - apply plugin: 'java' + apply plugin: 'io.spring.dependency-management' + apply plugin: 'com.github.sherter.google-java-format' - project.group = 'io.rsocket' + ext['reactor-bom.version'] = 'Dysprosium-SR20' + ext['logback.version'] = '1.2.3' + ext['netty-bom.version'] = '4.1.59.Final' + ext['netty-boringssl.version'] = '2.0.36.Final' + 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' - compileJava { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + group = "io.rsocket" + + googleJavaFormat { + toolVersion = '1.6' + } + + ext { + if (project.hasProperty('versionSuffix')) { + project.version += project.getProperty('versionSuffix') + } + } + + dependencyManagement { + imports { + mavenBom "io.projectreactor:reactor-bom:${ext['reactor-bom.version']}" + mavenBom "io.netty:netty-bom:${ext['netty-bom.version']}" + mavenBom "org.junit:junit-bom:${ext['junit.version']}" + } + + dependencies { + dependency "ch.qos.logback:logback-classic:${ext['logback.version']}" + dependency "io.netty:netty-tcnative-boringssl-static:${ext['netty-boringssl.version']}" + dependency "io.micrometer:micrometer-core:${ext['micrometer.version']}" + dependency "org.assertj:assertj-core:${ext['assertj.version']}" + dependency "org.hdrhistogram:HdrHistogram:${ext['hdrhistogram.version']}" + dependency "org.slf4j:slf4j-api:${ext['slf4j.version']}" + dependencySet(group: 'org.mockito', version: ext['mockito.version']) { + entry 'mockito-junit-jupiter' + entry 'mockito-core' + } + // 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' + } + } + generatedPomCustomization { + enabled = false + } } repositories { - jcenter() - maven { url 'http://repo.spring.io/milestone' } - maven { url 'https://oss.jfrog.org/libs-snapshot' } - maven { url 'https://dl.bintray.com/rsocket/RSocket' } - maven { url 'https://dl.bintray.com/reactivesocket/ReactiveSocket' } + mavenCentral() + + maven { + url 'https://repo.spring.io/libs-snapshot' + content { + includeGroup "io.projectreactor" + includeGroup "io.projectreactor.netty" + } + } + + 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' } + } + } + + tasks.withType(GenerateModuleMetadata) { + enabled = false } - dependencies { - compile 'io.projectreactor:reactor-core:3.1.0.M2' - compile 'io.netty:netty-buffer:4.1.12.Final' - compile 'org.reactivestreams:reactive-streams:1.0.0' - compile 'org.slf4j:slf4j-api:1.7.25' - compile 'com.google.code.findbugs:jsr305:3.0.0' - - testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:1.10.19' - testCompile 'org.hamcrest:hamcrest-library:1.3' - testCompile 'org.slf4j:slf4j-log4j12:1.7.25' - testCompile 'io.projectreactor:reactor-test:3.1.0.M2' + plugins.withType(JavaPlugin) { + compileJava { + sourceCompatibility = 1.8 + + // TODO: Cleanup warnings so no need to exclude + options.compilerArgs << '-Xlint:all,-overloads,-rawtypes,-unchecked' + } + + javadoc { + def jdk = JavaVersion.current().majorVersion + def jdkJavadoc = "https://docs.oracle.com/javase/$jdk/docs/api/" + if (JavaVersion.current().isJava11Compatible()) { + jdkJavadoc = "https://docs.oracle.com/en/java/javase/$jdk/docs/api/" + } + options.with { + links jdkJavadoc + links 'https://projectreactor.io/docs/core/release/api/' + links 'https://netty.io/4.1/api/' + } + failOnError = false + } + + tasks.named("javadoc").configure { + onlyIf { System.getenv('SKIP_RELEASE') != "true" } + } + + test { + useJUnitPlatform() + testLogging { + events "FAILED" + showExceptions true + exceptionFormat "FULL" + stackTraceFilters "ENTRY_POINT" + maxGranularity 3 + } + + //show progress by displaying test classes, avoiding test suite timeouts + TestDescriptor last + afterTest { TestDescriptor td, TestResult tr -> + if (last != td.getParent()) { + last = td.getParent() + println last + } + } + + if (isCiServer) { + def stdout = new LinkedList() + beforeTest { TestDescriptor td -> + stdout.clear() + } + onOutput { TestDescriptor td, TestOutputEvent toe -> + stdout.add(toe) + } + afterTest { TestDescriptor td, TestResult tr -> + if (tr.resultType == TestResult.ResultType.FAILURE && stdout.size() > 0) { + def stdOutput = stdout.collect { + it.getDestination().name() == "StdErr" + ? "STD_ERR: ${it.getMessage()}" + : "STD_OUT: ${it.getMessage()}" + } + .join() + println "This is the console output of the failing test below:\n$stdOutput" + } + } + } + + 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"] + } + + systemProperty("java.awt.headless", "true") + systemProperty("testGroups", project.properties.get("testGroups")) + + //allow re-run of failed tests only without special test tasks failing + // because the filter is too restrictive + filter.setFailOnNoMatchingTests(false) + + //display intermediate results for special test tasks + afterSuite { desc, result -> + if (!desc.parent) { // will match the outermost suite + println('\n' + "${desc} Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)") + } + } + } } - test { - testLogging.showStandardStreams = true + plugins.withType(JavaLibraryPlugin) { + task sourcesJar(type: Jar) { + classifier 'sources' + from sourceSets.main.allJava + } + + task javadocJar(type: Jar, dependsOn: javadoc) { + classifier 'javadoc' + from javadoc.destinationDir + } + + plugins.withType(MavenPublishPlugin) { + publishing { + publications { + maven(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar + } + } + } + } } } -// support for snapshot/final releases via versioned branch names like 1.x -nebulaRelease { - addReleaseBranchPattern(/\d+\.\d+\.\d+/) - addReleaseBranchPattern('HEAD') +apply from: "${rootDir}/gradle/publications.gradle" + +buildScan { + termsOfServiceUrl = 'https://gradle.com/terms-of-service' + termsOfServiceAgree = 'yes' } -if (project.hasProperty('release.useLastTag')) { - tasks.prepare.enabled = false +description = 'RSocket: Stream Oriented Messaging Passing with Reactive Stream Semantics.' + +repositories { + mavenCentral() } diff --git a/gradle.properties b/gradle.properties index 7d87c7e6d..4f89f4d88 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,4 @@ # -# Copyright 2016 Netflix, Inc. -# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -13,6 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # - -release.scope=patch -release.version=0.9-SNAPSHOT \ No newline at end of file +version=1.0.6 +perfBaselineVersion=1.0.5 diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh deleted file mode 100755 index d76c7f289..000000000 --- a/gradle/buildViaTravis.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -# This script will build the project. - -if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then - echo -e "Build Pull Request #$TRAVIS_PULL_REQUEST => Branch [$TRAVIS_BRANCH]" - ./gradlew -Prelease.useLastTag=true build -elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then - echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' - ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" build snapshot --stacktrace -elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then - echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' - ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" final --stacktrace --info -else - echo -e 'WARN: Should not be here => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG'] Pull Request ['$TRAVIS_PULL_REQUEST']' - ./gradlew -Prelease.useLastTag=true build -fi 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 new file mode 100644 index 000000000..c405a4032 --- /dev/null +++ b/gradle/publications.gradle @@ -0,0 +1,68 @@ +apply from: "${rootDir}/gradle/github-pkg.gradle" +apply from: "${rootDir}/gradle/sonotype.gradle" + +subprojects { + plugins.withType(MavenPublishPlugin) { + publishing { + publications { + maven(MavenPublication) { + pom { + name = project.name + afterEvaluate { + description = project.description + } + groupId = 'io.rsocket' + url = 'http://rsocket.io' + licenses { + license { + name = "The Apache Software License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "repo" + } + } + developers { + developer { + id = 'robertroeser' + name = 'Robert Roeser' + email = 'robert@netifi.com' + } + developer { + id = 'rdegnan' + name = 'Ryland Degnan' + email = 'ryland@netifi.com' + } + developer { + id = 'yschimke' + name = 'Yuri Schimke' + email = 'yuri@schimke.ee' + } + developer { + id = 'OlegDokuka' + name = 'Oleh Dokuka' + email = 'oleh@netifi.com' + } + developer { + id = 'mostroverkhov' + name = 'Maksym Ostroverkhov' + email = 'm.ostroverkhov@gmail.com' + } + } + scm { + connection = 'scm:git:https://github.com/rsocket/rsocket-java.git' + developerConnection = 'scm:git:https://github.com/rsocket/rsocket-java.git' + url = 'https://github.com/rsocket/rsocket-java' + } + versionMapping { + usage('java-api') { + fromResolutionResult() + } + usage('java-runtime') { + fromResolutionResult() + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/gradle/sonotype.gradle b/gradle/sonotype.gradle new file mode 100644 index 000000000..1effd76b0 --- /dev/null +++ b/gradle/sonotype.gradle @@ -0,0 +1,34 @@ +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 = "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 2c6137b87..5c2d1cf01 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 b79f78dae..8f6e03af5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat May 13 00:07:32 PDT 2017 +#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 -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-all.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 9d82f7891..83f2acfdc 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,20 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.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. +# ############################################################################## ## @@ -6,20 +22,38 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# 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 +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$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" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +64,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +75,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# 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 -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +105,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +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 @@ -105,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# 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"` @@ -150,11 +170,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 5f192121e..24467a141 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,90 +1,100 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@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= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -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 -if "%@eval[2+2]" == "4" goto 4NT_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=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -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% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="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 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +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% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/rsocket-bom/build.gradle b/rsocket-bom/build.gradle new file mode 100755 index 000000000..a75ab3bc8 --- /dev/null +++ b/rsocket-bom/build.gradle @@ -0,0 +1,40 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins { + id 'java-platform' + id 'maven-publish' + id 'signing' +} + +description = 'RSocket Java Bill of materials.' + +def excluded = ["rsocket-examples", "benchmarks"] + +dependencies { + constraints { + parent.subprojects.findAll { it.name != project.name && !excluded.contains(it.name) } .sort { "$it.name" }.each { + api it + } + } +} + +publishing { + publications { + maven(MavenPublication) { + from components.javaPlatform + } + } +} \ No newline at end of file diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index 13f87b597..fbeee37ce 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,19 +15,32 @@ */ plugins { - id "me.champeau.gradle.jmh" version "0.3.1" -} - -jmh { - jmhVersion = '1.15' - profilers = ['gc'] - zip64 = true - duplicateClassesStrategy = 'warn' + id 'java-library' + id 'maven-publish' + id 'signing' + id 'io.morethan.jmhreport' + id 'me.champeau.gradle.jmh' } dependencies { - compile 'io.projectreactor:reactor-core:3.1.0.M2' + api 'io.netty:netty-buffer' + api 'io.projectreactor:reactor-core' + + implementation 'org.slf4j:slf4j-api' - jmh 'org.openjdk.jmh:jmh-core:1.15' - jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.15' + testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'org.junit.jupiter:junit-jupiter-params' + testImplementation 'org.mockito:mockito-core' + + 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' } + +description = "Core functionality for the RSocket library" \ No newline at end of file diff --git a/rsocket-core/src/jmh/java/io/rsocket/FragmentationPerf.java b/rsocket-core/src/jmh/java/io/rsocket/FragmentationPerf.java deleted file mode 100644 index 451367089..000000000 --- a/rsocket-core/src/jmh/java/io/rsocket/FragmentationPerf.java +++ /dev/null @@ -1,95 +0,0 @@ -package io.rsocket; - -import io.rsocket.fragmentation.FrameFragmenter; -import io.rsocket.fragmentation.FrameReassembler; -import io.rsocket.util.PayloadImpl; -import java.nio.ByteBuffer; -import java.util.concurrent.ThreadLocalRandom; -import java.util.stream.Collectors; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Warmup; -import org.openjdk.jmh.infra.Blackhole; - -@BenchmarkMode(Mode.Throughput) -@Fork( - value = 1, - jvmArgsAppend = {"-XX:+UnlockCommercialFeatures", "-XX:+FlightRecorder"} -) //, "-Dio.netty.leakDetection.level=advanced"}) -@Warmup(iterations = 10) -@Measurement(iterations = 10_000) -@State(Scope.Thread) -public class FragmentationPerf { - @State(Scope.Benchmark) - public static class Input { - Blackhole bh; - Frame smallFrame; - FrameFragmenter smallFrameFragmenter; - - Frame largeFrame; - FrameFragmenter largeFrameFragmenter; - - Iterable smallFramesIterable; - - @Setup - public void setup(Blackhole bh) { - this.bh = bh; - - ByteBuffer data = createRandomBytes(1 << 18); - ByteBuffer metadata = createRandomBytes(1 << 18); - largeFrame = - Frame.Request.from(1, FrameType.REQUEST_RESPONSE, new PayloadImpl(data, metadata), 1); - largeFrameFragmenter = new FrameFragmenter(1024); - - data = createRandomBytes(16); - metadata = createRandomBytes(16); - smallFrame = - Frame.Request.from(1, FrameType.REQUEST_RESPONSE, new PayloadImpl(data, metadata), 1); - smallFrameFragmenter = new FrameFragmenter(2); - smallFramesIterable = - smallFrameFragmenter - .fragment(smallFrame) - .map(frame -> frame.copy()) - .toStream() - .collect(Collectors.toList()); - } - } - - @Benchmark - public void smallFragmentationPerf(Input input) { - Frame frame = - input.smallFrameFragmenter.fragment(input.smallFrame).doOnNext(Frame::release).blockLast(); - input.bh.consume(frame); - } - - @Benchmark - public void largeFragmentationPerf(Input input) { - Frame frame = - input.largeFrameFragmenter.fragment(input.largeFrame).doOnNext(Frame::release).blockLast(); - input.bh.consume(frame); - } - - @Benchmark - public void smallFragmentationFrameReassembler(Input input) { - FrameReassembler smallFragmentAssembler = new FrameReassembler(input.smallFrame); - - input.smallFramesIterable.forEach(smallFragmentAssembler::append); - - Frame frame = smallFragmentAssembler.reassemble(); - input.bh.consume(frame); - frame.release(); - //input.smallFragmentAssembler.clear(); - } - - private static ByteBuffer createRandomBytes(int size) { - byte[] bytes = new byte[size]; - ThreadLocalRandom.current().nextBytes(bytes); - return ByteBuffer.wrap(bytes); - } -} diff --git a/rsocket-core/src/jmh/java/io/rsocket/RSocketPerf.java b/rsocket-core/src/jmh/java/io/rsocket/RSocketPerf.java deleted file mode 100644 index 96c6ed790..000000000 --- a/rsocket-core/src/jmh/java/io/rsocket/RSocketPerf.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import io.rsocket.perfutil.TestDuplexConnection; -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import io.rsocket.util.PayloadImpl; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Warmup; -import org.openjdk.jmh.infra.Blackhole; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.publisher.DirectProcessor; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; - -@BenchmarkMode(Mode.Throughput) -@Fork( - value = 1, - jvmArgsAppend = {"-XX:+UnlockCommercialFeatures", "-XX:+FlightRecorder"} -) -@Warmup(iterations = 10) -@Measurement(iterations = 10) -@State(Scope.Thread) -public class RSocketPerf { - - @Benchmark - public void requestResponseHello(Input input) { - try { - input.client.requestResponse(Input.HELLO_PAYLOAD).subscribe(input.blackHoleSubscriber); - } catch (Throwable t) { - t.printStackTrace(); - } - } - - @Benchmark - public void requestStreamHello1000(Input input) { - try { - input.client.requestStream(Input.HELLO_PAYLOAD).subscribe(input.blackHoleSubscriber); - } catch (Throwable t) { - t.printStackTrace(); - } - } - - @Benchmark - public void fireAndForgetHello(Input input) { - // this is synchronous so we don't need to use a CountdownLatch to wait - input.client.fireAndForget(Input.HELLO_PAYLOAD).subscribe(input.blackHoleSubscriber); - } - - @State(Scope.Benchmark) - public static class Input { - /** Use to consume values when the test needs to return more than a single value. */ - public Blackhole bh; - - static final ByteBuffer HELLO = ByteBuffer.wrap("HELLO".getBytes(StandardCharsets.UTF_8)); - - static final Payload HELLO_PAYLOAD = new PayloadImpl(HELLO); - - static final DirectProcessor clientReceive = DirectProcessor.create(); - static final DirectProcessor serverReceive = DirectProcessor.create(); - - static final TestDuplexConnection clientConnection = - new TestDuplexConnection(serverReceive, clientReceive); - static final TestDuplexConnection serverConnection = - new TestDuplexConnection(clientReceive, serverReceive); - - static final Object server = - RSocketFactory.receive() - .acceptor( - new SocketAcceptor() { - @Override - public Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket) { - RSocket rSocket = - new RSocket() { - @Override - public Mono fireAndForget(Payload payload) { - return Mono.empty(); - } - - @Override - public Mono requestResponse(Payload payload) { - return Mono.just(HELLO_PAYLOAD); - } - - @Override - public Flux requestStream(Payload payload) { - return Flux.range(1, 1_000).flatMap(i -> requestResponse(payload)); - } - - @Override - public Flux requestChannel(Publisher payloads) { - return Flux.empty(); - } - - @Override - public Mono metadataPush(Payload payload) { - return Mono.empty(); - } - - @Override - public Mono close() { - return Mono.empty(); - } - - @Override - public Mono onClose() { - return Mono.empty(); - } - }; - - return Mono.just(rSocket); - } - }) - .transport( - new ServerTransport() { - @Override - public Mono start(ConnectionAcceptor acceptor) { - Closeable closeable = - new Closeable() { - MonoProcessor onClose = MonoProcessor.create(); - - @Override - public Mono close() { - return Mono.empty().doFinally(s -> onClose.onComplete()).then(); - } - - @Override - public Mono onClose() { - return onClose; - } - }; - - acceptor.apply(serverConnection).subscribe(); - - return Mono.just(closeable); - } - }); - - Subscriber blackHoleSubscriber; - - RSocket client; - - @Setup - public void setup(Blackhole bh) { - blackHoleSubscriber = - new Subscriber() { - @Override - public void onSubscribe(Subscription s) { - s.request(Long.MAX_VALUE); - } - - @Override - public void onNext(Object o) { - bh.consume(o); - } - - @Override - public void onError(Throwable t) { - t.printStackTrace(); - } - - @Override - public void onComplete() {} - }; - - client = - RSocketFactory.connect() - .transport( - new ClientTransport() { - @Override - public Mono connect() { - return Mono.just(clientConnection); - } - }) - .start() - .block(); - - this.bh = bh; - } - } -} diff --git a/rsocket-core/src/jmh/java/io/rsocket/perfutil/TestDuplexConnection.java b/rsocket-core/src/jmh/java/io/rsocket/perfutil/TestDuplexConnection.java deleted file mode 100644 index 47db25d45..000000000 --- a/rsocket-core/src/jmh/java/io/rsocket/perfutil/TestDuplexConnection.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.perfutil; - -import io.rsocket.DuplexConnection; -import io.rsocket.Frame; -import org.reactivestreams.Publisher; -import reactor.core.publisher.DirectProcessor; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** - * An implementation of {@link DuplexConnection} that provides functionality to modify the behavior - * dynamically. - */ -public class TestDuplexConnection implements DuplexConnection { - - private final DirectProcessor send; - private final DirectProcessor receive; - - public TestDuplexConnection(DirectProcessor send, DirectProcessor receive) { - this.send = send; - this.receive = receive; - } - - @Override - public Mono send(Publisher frame) { - return Flux.from(frame) - .doOnNext( - f -> { - try { - send.onNext(f); - } finally { - f.release(); - } - }) - .then(); - } - - @Override - public Mono sendOne(Frame frame) { - send.onNext(frame); - return Mono.empty(); - } - - @Override - public Flux receive() { - return receive; - } - - @Override - public double availability() { - return 1.0; - } - - @Override - public Mono close() { - return Mono.empty(); - } - - @Override - public Mono onClose() { - return Mono.empty(); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/AbstractRSocket.java b/rsocket-core/src/main/java/io/rsocket/AbstractRSocket.java index 3553309d8..7f39956dc 100644 --- a/rsocket-core/src/main/java/io/rsocket/AbstractRSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/AbstractRSocket.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,8 +16,6 @@ package io.rsocket; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; @@ -25,44 +23,22 @@ * An abstract implementation of {@link RSocket}. All request handling methods emit {@link * UnsupportedOperationException} and hence must be overridden to provide a valid implementation. * - *

{@link #close()} and {@link #onClose()} returns a {@code Publisher} that never terminates. + * @deprecated as of 1.0 in favor of implementing {@link RSocket} directly which has default + * methods. */ +@Deprecated public abstract class AbstractRSocket implements RSocket { private final MonoProcessor onClose = MonoProcessor.create(); @Override - public Mono fireAndForget(Payload payload) { - return Mono.error(new UnsupportedOperationException("Fire and forget not implemented.")); + public void dispose() { + onClose.onComplete(); } @Override - public Mono requestResponse(Payload payload) { - return Mono.error(new UnsupportedOperationException("Request-Response not implemented.")); - } - - @Override - public Flux requestStream(Payload payload) { - return Flux.error(new UnsupportedOperationException("Request-Stream not implemented.")); - } - - @Override - public Flux requestChannel(Publisher payloads) { - return Flux.error(new UnsupportedOperationException("Request-Channel not implemented.")); - } - - @Override - public Mono metadataPush(Payload payload) { - return Mono.error(new UnsupportedOperationException("Metadata-Push not implemented.")); - } - - @Override - public Mono close() { - return Mono.defer( - () -> { - onClose.onComplete(); - return onClose; - }); + public boolean isDisposed() { + return onClose.isDisposed(); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/Availability.java b/rsocket-core/src/main/java/io/rsocket/Availability.java index b655f6d43..3361bcf8d 100644 --- a/rsocket-core/src/main/java/io/rsocket/Availability.java +++ b/rsocket-core/src/main/java/io/rsocket/Availability.java @@ -1,14 +1,17 @@ /* - * Copyright 2016 Netflix, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket; diff --git a/rsocket-core/src/main/java/io/rsocket/Closeable.java b/rsocket-core/src/main/java/io/rsocket/Closeable.java index 0909de63d..2ea9a0371 100644 --- a/rsocket-core/src/main/java/io/rsocket/Closeable.java +++ b/rsocket-core/src/main/java/io/rsocket/Closeable.java @@ -1,41 +1,36 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket; +import org.reactivestreams.Subscriber; +import reactor.core.Disposable; import reactor.core.publisher.Mono; -/** */ -public interface Closeable { +/** An interface which allows listening to when a specific instance of this interface is closed */ +public interface Closeable extends Disposable { /** - * Close this {@code RSocket} upon subscribing to the returned {@code Publisher} + * Returns a {@link Mono} that terminates when the instance is terminated by any reason. Note, in + * case of error termination, the cause of error will be propagated as an error signal through + * {@link org.reactivestreams.Subscriber#onError(Throwable)}. Otherwise, {@link + * Subscriber#onComplete()} will be called. * - *

This method is idempotent and hence can be called as many times at any point with same - * outcome. - * - * @return A {@code Publisher} that completes when this {@code RSocket} close is complete. - */ - Mono close(); - - /** - * Returns a {@code Publisher} that completes when this {@code RSocket} is closed. A {@code - * RSocket} can be closed by explicitly calling {@link #close()} or when the underlying transport - * connection is closed. - * - * @return A {@code Publisher} that completes when this {@code RSocket} close is complete. + * @return a {@link Mono} to track completion with success or error of the underlying resource. + * When the underlying resource is an `RSocket`, the {@code Mono} exposes stream 0 (i.e. + * connection level) errors. */ Mono onClose(); } diff --git a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java index 7b2d9f9bf..ece2aa9fa 100644 --- a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,107 +13,60 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket; -import io.rsocket.frame.SetupFrameFlyweight; -import java.nio.ByteBuffer; +import io.netty.buffer.ByteBuf; +import io.netty.util.AbstractReferenceCounted; +import io.rsocket.core.DefaultConnectionSetupPayload; +import reactor.util.annotation.Nullable; /** - * Exposed to server for determination of RequestHandler based on mime types and SETUP metadata/data + * Exposes information from the {@code SETUP} frame to a server, as well as to client responders. */ -public abstract class ConnectionSetupPayload implements Payload { - - public static final int NO_FLAGS = 0; - public static final int HONOR_LEASE = SetupFrameFlyweight.FLAGS_WILL_HONOR_LEASE; - public static final int STRICT_INTERPRETATION = SetupFrameFlyweight.FLAGS_STRICT_INTERPRETATION; - - public static ConnectionSetupPayload create(String metadataMimeType, String dataMimeType) { - return new ConnectionSetupPayloadImpl( - metadataMimeType, dataMimeType, Frame.NULL_BYTEBUFFER, Frame.NULL_BYTEBUFFER, NO_FLAGS); - } - - public static ConnectionSetupPayload create( - String metadataMimeType, String dataMimeType, Payload payload) { - return new ConnectionSetupPayloadImpl( - metadataMimeType, dataMimeType, payload.getData(), payload.getMetadata(), NO_FLAGS); - } - - public static ConnectionSetupPayload create( - String metadataMimeType, String dataMimeType, int flags) { - return new ConnectionSetupPayloadImpl( - metadataMimeType, dataMimeType, Frame.NULL_BYTEBUFFER, Frame.NULL_BYTEBUFFER, flags); - } - - public static ConnectionSetupPayload create(final Frame setupFrame) { - Frame.ensureFrameType(FrameType.SETUP, setupFrame); - return new ConnectionSetupPayloadImpl( - Frame.Setup.metadataMimeType(setupFrame), - Frame.Setup.dataMimeType(setupFrame), - setupFrame.getData(), - setupFrame.getMetadata(), - Frame.Setup.getFlags(setupFrame)); - } +public abstract class ConnectionSetupPayload extends AbstractReferenceCounted implements Payload { public abstract String metadataMimeType(); public abstract String dataMimeType(); - public int getFlags() { - return HONOR_LEASE; - } - - public boolean willClientHonorLease() { - return HONOR_LEASE == (getFlags() & HONOR_LEASE); - } - - public boolean doesClientRequestStrictInterpretation() { - return STRICT_INTERPRETATION == (getFlags() & STRICT_INTERPRETATION); - } + public abstract int keepAliveInterval(); - private static final class ConnectionSetupPayloadImpl extends ConnectionSetupPayload { + public abstract int keepAliveMaxLifetime(); - private final String metadataMimeType; - private final String dataMimeType; - private final ByteBuffer data; - private final ByteBuffer metadata; - private final int flags; + public abstract int getFlags(); - public ConnectionSetupPayloadImpl( - String metadataMimeType, - String dataMimeType, - ByteBuffer data, - ByteBuffer metadata, - int flags) { - this.metadataMimeType = metadataMimeType; - this.dataMimeType = dataMimeType; - this.data = data; - this.metadata = metadata; - this.flags = flags; - } + public abstract boolean willClientHonorLease(); - @Override - public String metadataMimeType() { - return metadataMimeType; - } + public abstract boolean isResumeEnabled(); - @Override - public String dataMimeType() { - return dataMimeType; - } + @Nullable + public abstract ByteBuf resumeToken(); - @Override - public ByteBuffer getData() { - return data; - } + @Override + public ConnectionSetupPayload retain() { + super.retain(); + return this; + } - @Override - public ByteBuffer getMetadata() { - return metadata; - } + @Override + public ConnectionSetupPayload retain(int increment) { + super.retain(increment); + return this; + } - @Override - public int getFlags() { - return flags; - } + @Override + public abstract ConnectionSetupPayload touch(); + + /** + * Create a {@code ConnectionSetupPayload}. + * + * @deprecated as of 1.0 RC7. Please, use {@link + * DefaultConnectionSetupPayload#DefaultConnectionSetupPayload(ByteBuf) + * DefaultConnectionSetupPayload} constructor. + */ + @Deprecated + public static ConnectionSetupPayload create(final ByteBuf setupFrame) { + return new DefaultConnectionSetupPayload(setupFrame); } } diff --git a/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java index 0dea9d9d2..6190d24e3 100644 --- a/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,8 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import java.nio.channels.ClosedChannelException; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; @@ -25,18 +28,19 @@ public interface DuplexConnection extends Availability, Closeable { /** - * Sends the source of {@link Frame}s on this connection and returns the {@code Publisher} - * representing the result of this send. + * Sends the source of Frames on this connection and returns the {@code Publisher} representing + * the result of this send. * - *

Flow control

+ *

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

The passed {@code Publisher} must * - * @param frame Stream of {@code Frame}s to send on the connection. + * @param frames Stream of {@code Frame}s to send on the connection. * @return {@code Publisher} that completes when all the frames are written on the connection * successfully and errors when it fails. + * @throws NullPointerException if {@code frames} is {@code null} */ - Mono send(Publisher frame); + Mono send(Publisher frames); /** * Sends a single {@code Frame} on this connection and returns the {@code Publisher} representing @@ -46,32 +50,44 @@ public interface DuplexConnection extends Availability, Closeable { * @return {@code Publisher} that completes when the frame is written on the connection * successfully and errors when it fails. */ - default Mono sendOne(Frame frame) { + default Mono sendOne(ByteBuf frame) { return send(Mono.just(frame)); } /** * Returns a stream of all {@code Frame}s received on this connection. * - *

Completion

+ *

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

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

Error

+ *

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

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

Multiple Subscriptions

+ *

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

Returned {@code Publisher} is not required to support multiple concurrent subscriptions. * RSocket will never have multiple subscriptions to this source. Implementations MUST * emit an {@link IllegalStateException} for subsequent concurrent subscriptions, if they do not * support multiple concurrent subscriptions. * * @return Stream of all {@code Frame}s received. */ - Flux receive(); + Flux receive(); + + /** + * Returns the assigned {@link ByteBufAllocator}. + * + * @return the {@link ByteBufAllocator} + */ + ByteBufAllocator alloc(); + + @Override + default double availability() { + return isDisposed() ? 0.0 : 1.0; + } } diff --git a/rsocket-core/src/main/java/io/rsocket/Frame.java b/rsocket-core/src/main/java/io/rsocket/Frame.java deleted file mode 100644 index 92e54e872..000000000 --- a/rsocket-core/src/main/java/io/rsocket/Frame.java +++ /dev/null @@ -1,697 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufHolder; -import io.netty.buffer.Unpooled; -import io.netty.util.IllegalReferenceCountException; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import io.netty.util.ResourceLeakDetector; -import io.rsocket.frame.ErrorFrameFlyweight; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.KeepaliveFrameFlyweight; -import io.rsocket.frame.LeaseFrameFlyweight; -import io.rsocket.frame.RequestFrameFlyweight; -import io.rsocket.frame.RequestNFrameFlyweight; -import io.rsocket.frame.SetupFrameFlyweight; -import io.rsocket.frame.VersionFlyweight; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import javax.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Represents a Frame sent over a {@link DuplexConnection}. - * - *

This provides encoding, decoding and field accessors. - */ -public class Frame implements ByteBufHolder { - - private static final Logger logger = LoggerFactory.getLogger(Frame.class); - - public static final ByteBuffer NULL_BYTEBUFFER = ByteBuffer.allocateDirect(0); - public static final int METADATA_MTU = 32 * 1024; - public static final int DATA_MTU = 32 * 1024; - - private static final Recycler RECYCLER = - new Recycler() { - protected Frame newObject(Handle handle) { - return new Frame(handle); - } - }; - - private final Handle handle; - private @Nullable ByteBuf content; - - private Frame(final Handle handle) { - this.handle = handle; - } - - /** Clear and recycle this instance. */ - private void recycle() { - content = null; - handle.recycle(this); - } - - /** Return the content which is held by this {@link Frame}. */ - @Override - public ByteBuf content() { - if (content.refCnt() <= 0) { - throw new IllegalReferenceCountException(content.refCnt()); - } - return content; - } - - /** Creates a deep copy of this {@link Frame}. */ - @Override - public Frame copy() { - return replace(content.copy()); - } - - /** - * Duplicates this {@link Frame}. Be aware that this will not automatically call {@link - * #retain()}. - */ - @Override - public Frame duplicate() { - return replace(content.duplicate()); - } - - /** - * Duplicates this {@link Frame}. This method returns a retained duplicate unlike {@link - * #duplicate()}. - * - * @see ByteBuf#retainedDuplicate() - */ - @Override - public Frame retainedDuplicate() { - return replace(content.retainedDuplicate()); - } - - /** Returns a new {@link Frame} which contains the specified {@code content}. */ - @Override - public Frame replace(ByteBuf content) { - return from(content); - } - - /** - * Returns the reference count of this object. If {@code 0}, it means this object has been - * deallocated. - */ - @Override - public int refCnt() { - return content.refCnt(); - } - - /** Increases the reference count by {@code 1}. */ - @Override - public Frame retain() { - content.retain(); - return this; - } - - /** Increases the reference count by the specified {@code increment}. */ - @Override - public Frame retain(int increment) { - content.retain(increment); - return this; - } - - /** - * Records the current access location of this object for debugging purposes. If this object is - * determined to be leaked, the information recorded by this operation will be provided to you via - * {@link ResourceLeakDetector}. This method is a shortcut to {@link #touch(Object) touch(null)}. - */ - @Override - public Frame touch() { - content.touch(); - return this; - } - - /** - * Records the current access location of this object with an additonal arbitrary information for - * debugging purposes. If this object is determined to be leaked, the information recorded by this - * operation will be provided to you via {@link ResourceLeakDetector}. - */ - @Override - public Frame touch(@Nullable Object hint) { - content.touch(hint); - return this; - } - - /** - * Decreases the reference count by {@code 1} and deallocates this object if the reference count - * reaches at {@code 0}. - * - * @return {@code true} if and only if the reference count became {@code 0} and this object has - * been deallocated - */ - @Override - public boolean release() { - if (content.release()) { - recycle(); - return true; - } - return false; - } - - /** - * Decreases the reference count by the specified {@code decrement} and deallocates this object if - * the reference count reaches at {@code 0}. - * - * @return {@code true} if and only if the reference count became {@code 0} and this object has - * been deallocated - */ - @Override - public boolean release(int decrement) { - if (content.release(decrement)) { - recycle(); - return true; - } - return false; - } - - /** - * Return {@link ByteBuffer} that is a {@link ByteBuffer#slice()} for the frame metadata - * - *

If no metadata is present, the ByteBuffer will have 0 capacity. - * - * @return ByteBuffer containing the content - */ - public ByteBuffer getMetadata() { - final ByteBuf metadata = FrameHeaderFlyweight.sliceFrameMetadata(content); - if (metadata.readableBytes() > 0) { - final ByteBuffer buffer = ByteBuffer.allocateDirect(metadata.readableBytes()); - metadata.readBytes(buffer); - buffer.flip(); - return buffer; - } else { - return NULL_BYTEBUFFER; - } - } - - /** - * Return {@link ByteBuffer} that is a {@link ByteBuffer#slice()} for the frame data - * - *

If no data is present, the ByteBuffer will have 0 capacity. - * - * @return ByteBuffer containing the data - */ - public ByteBuffer getData() { - final ByteBuf data = FrameHeaderFlyweight.sliceFrameData(content); - if (data.readableBytes() > 0) { - final ByteBuffer buffer = ByteBuffer.allocateDirect(data.readableBytes()); - data.readBytes(buffer); - buffer.flip(); - return buffer; - } else { - return NULL_BYTEBUFFER; - } - } - - /** - * Return frame stream identifier - * - * @return frame stream identifier - */ - public int getStreamId() { - return FrameHeaderFlyweight.streamId(content); - } - - /** - * Return frame {@link FrameType} - * - * @return frame type - */ - public FrameType getType() { - return FrameHeaderFlyweight.frameType(content); - } - - /** - * Return the flags field for the frame - * - * @return frame flags field value - */ - public int flags() { - return FrameHeaderFlyweight.flags(content); - } - - /** - * Acquire a free Frame backed by given ByteBuf - * - * @param content to use as backing buffer - * @return frame - */ - public static Frame from(final ByteBuf content) { - final Frame frame = RECYCLER.get(); - frame.content = content; - - return frame; - } - - /* TODO: - * - * fromRequest(type, id, payload) - * fromKeepalive(ByteBuf content) - * - */ - - // SETUP specific getters - public static class Setup { - - private Setup() {} - - public static Frame from( - int flags, - int keepaliveInterval, - int maxLifetime, - String metadataMimeType, - String dataMimeType, - Payload payload) { - final ByteBuf metadata = - payload.getMetadata() != null - ? Unpooled.wrappedBuffer(payload.getMetadata()) - : Unpooled.EMPTY_BUFFER; - final ByteBuf data = - payload.getData() != null - ? Unpooled.wrappedBuffer(payload.getData()) - : Unpooled.EMPTY_BUFFER; - - final Frame frame = RECYCLER.get(); - frame.content = - ByteBufAllocator.DEFAULT.buffer( - SetupFrameFlyweight.computeFrameLength( - flags, - metadataMimeType, - dataMimeType, - metadata.readableBytes(), - data.readableBytes())); - frame.content.writerIndex( - SetupFrameFlyweight.encode( - frame.content, - flags, - keepaliveInterval, - maxLifetime, - metadataMimeType, - dataMimeType, - metadata, - data)); - return frame; - } - - public static int getFlags(final Frame frame) { - ensureFrameType(FrameType.SETUP, frame); - final int flags = FrameHeaderFlyweight.flags(frame.content); - - return flags & SetupFrameFlyweight.VALID_FLAGS; - } - - public static int version(final Frame frame) { - ensureFrameType(FrameType.SETUP, frame); - return SetupFrameFlyweight.version(frame.content); - } - - public static int keepaliveInterval(final Frame frame) { - ensureFrameType(FrameType.SETUP, frame); - return SetupFrameFlyweight.keepaliveInterval(frame.content); - } - - public static int maxLifetime(final Frame frame) { - ensureFrameType(FrameType.SETUP, frame); - return SetupFrameFlyweight.maxLifetime(frame.content); - } - - public static String metadataMimeType(final Frame frame) { - ensureFrameType(FrameType.SETUP, frame); - return SetupFrameFlyweight.metadataMimeType(frame.content); - } - - public static String dataMimeType(final Frame frame) { - ensureFrameType(FrameType.SETUP, frame); - return SetupFrameFlyweight.dataMimeType(frame.content); - } - } - - public static class Error { - private static final Logger errorLogger = LoggerFactory.getLogger(Error.class); - - private Error() {} - - public static Frame from( - int streamId, final Throwable throwable, ByteBuf metadata, ByteBuf data) { - if (errorLogger.isDebugEnabled()) { - errorLogger.debug("an error occurred, creating error frame", throwable); - } - - final int code = ErrorFrameFlyweight.errorCodeFromException(throwable); - final Frame frame = RECYCLER.get(); - frame.content = - ByteBufAllocator.DEFAULT.buffer( - ErrorFrameFlyweight.computeFrameLength( - metadata.readableBytes(), data.readableBytes())); - frame.content.writerIndex( - ErrorFrameFlyweight.encode(frame.content, streamId, code, metadata, data)); - return frame; - } - - public static Frame from(int streamId, final Throwable throwable, ByteBuf metadata) { - String data = throwable.getMessage() == null ? "" : throwable.getMessage(); - byte[] bytes = data.getBytes(StandardCharsets.UTF_8); - final ByteBuf dataBuffer = Unpooled.wrappedBuffer(bytes); - - return from(streamId, throwable, metadata, dataBuffer); - } - - public static Frame from(int streamId, final Throwable throwable) { - return from(streamId, throwable, Unpooled.EMPTY_BUFFER); - } - - public static int errorCode(final Frame frame) { - ensureFrameType(FrameType.ERROR, frame); - return ErrorFrameFlyweight.errorCode(frame.content); - } - } - - public static class Lease { - private Lease() {} - - public static Frame from(int ttl, int numberOfRequests, ByteBuf metadata) { - final Frame frame = RECYCLER.get(); - frame.content = - ByteBufAllocator.DEFAULT.buffer( - LeaseFrameFlyweight.computeFrameLength(metadata.readableBytes())); - frame.content.writerIndex( - LeaseFrameFlyweight.encode(frame.content, ttl, numberOfRequests, metadata)); - return frame; - } - - public static int ttl(final Frame frame) { - ensureFrameType(FrameType.LEASE, frame); - return LeaseFrameFlyweight.ttl(frame.content); - } - - public static int numberOfRequests(final Frame frame) { - ensureFrameType(FrameType.LEASE, frame); - return LeaseFrameFlyweight.numRequests(frame.content); - } - } - - public static class RequestN { - private RequestN() {} - - public static Frame from(int streamId, long requestN) { - int v = requestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) requestN; - return from(streamId, v); - } - - public static Frame from(int streamId, int requestN) { - if (requestN < 1) { - throw new IllegalStateException("request n must be greater than 0"); - } - - final Frame frame = RECYCLER.get(); - frame.content = ByteBufAllocator.DEFAULT.buffer(RequestNFrameFlyweight.computeFrameLength()); - frame.content.writerIndex(RequestNFrameFlyweight.encode(frame.content, streamId, requestN)); - return frame; - } - - public static int requestN(final Frame frame) { - ensureFrameType(FrameType.REQUEST_N, frame); - return RequestNFrameFlyweight.requestN(frame.content); - } - } - - public static class Request { - private Request() {} - - public static Frame from(int streamId, FrameType type, Payload payload, long initialRequestN) { - int v = initialRequestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) initialRequestN; - return from(streamId, type, payload, v); - } - - public static Frame from(int streamId, FrameType type, Payload payload, int initialRequestN) { - if (initialRequestN < 1) { - throw new IllegalStateException("initial request n must be greater than 0"); - } - final ByteBuf metadata = - payload.getMetadata() != null - ? Unpooled.wrappedBuffer(payload.getMetadata()) - : Unpooled.EMPTY_BUFFER; - final ByteBuf data = - payload.getData() != null - ? Unpooled.wrappedBuffer(payload.getData()) - : Unpooled.EMPTY_BUFFER; - - final Frame frame = RECYCLER.get(); - frame.content = - ByteBufAllocator.DEFAULT.buffer( - RequestFrameFlyweight.computeFrameLength( - type, metadata.readableBytes(), data.readableBytes())); - - if (type.hasInitialRequestN()) { - frame.content.writerIndex( - RequestFrameFlyweight.encode( - frame.content, streamId, 0, type, initialRequestN, metadata, data)); - } else { - frame.content.writerIndex( - RequestFrameFlyweight.encode(frame.content, streamId, 0, type, metadata, data)); - } - - return frame; - } - - public static Frame from(int streamId, FrameType type, int flags) { - final Frame frame = RECYCLER.get(); - frame.content = - ByteBufAllocator.DEFAULT.buffer(RequestFrameFlyweight.computeFrameLength(type, 0, 0)); - frame.content.writerIndex( - RequestFrameFlyweight.encode( - frame.content, streamId, flags, type, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER)); - return frame; - } - - public static Frame from( - int streamId, - FrameType type, - ByteBuf metadata, - ByteBuf data, - int initialRequestN, - int flags) { - final Frame frame = RECYCLER.get(); - frame.content = - ByteBufAllocator.DEFAULT.buffer( - RequestFrameFlyweight.computeFrameLength( - type, metadata.readableBytes(), data.readableBytes())); - frame.content.writerIndex( - RequestFrameFlyweight.encode( - frame.content, streamId, flags, type, initialRequestN, metadata, data)); - return frame; - } - - public static int initialRequestN(final Frame frame) { - final FrameType type = frame.getType(); - int result; - - if (!type.isRequestType()) { - throw new AssertionError("expected request type, but saw " + type.name()); - } - - switch (frame.getType()) { - case REQUEST_RESPONSE: - result = 1; - break; - case FIRE_AND_FORGET: - result = 0; - break; - default: - result = RequestFrameFlyweight.initialRequestN(frame.content); - break; - } - - return result; - } - - public static boolean isRequestChannelComplete(final Frame frame) { - ensureFrameType(FrameType.REQUEST_CHANNEL, frame); - final int flags = FrameHeaderFlyweight.flags(frame.content); - - return (flags & FrameHeaderFlyweight.FLAGS_C) == FrameHeaderFlyweight.FLAGS_C; - } - } - - public static class PayloadFrame { - - private PayloadFrame() {} - - public static Frame from(int streamId, FrameType type) { - return from(streamId, type, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER, 0); - } - - public static Frame from(int streamId, FrameType type, Payload payload) { - return from(streamId, type, payload, 0); - } - - public static Frame from(int streamId, FrameType type, Payload payload, int flags) { - final ByteBuf metadata = - payload.getMetadata() != null - ? Unpooled.wrappedBuffer(payload.getMetadata()) - : Unpooled.EMPTY_BUFFER; - final ByteBuf data = - payload.getData() != null - ? Unpooled.wrappedBuffer(payload.getData()) - : Unpooled.EMPTY_BUFFER; - return from(streamId, type, metadata, data, flags); - } - - public static Frame from( - int streamId, FrameType type, ByteBuf metadata, ByteBuf data, int flags) { - final Frame frame = RECYCLER.get(); - frame.content = - ByteBufAllocator.DEFAULT.buffer( - FrameHeaderFlyweight.computeFrameHeaderLength( - type, metadata.readableBytes(), data.readableBytes())); - frame.content.writerIndex( - FrameHeaderFlyweight.encode(frame.content, streamId, flags, type, metadata, data)); - return frame; - } - } - - public static class Cancel { - - private Cancel() {} - - public static Frame from(int streamId) { - final Frame frame = RECYCLER.get(); - frame.content = - ByteBufAllocator.DEFAULT.buffer( - FrameHeaderFlyweight.computeFrameHeaderLength(FrameType.CANCEL, 0, 0)); - frame.content.writerIndex( - FrameHeaderFlyweight.encode( - frame.content, - streamId, - 0, - FrameType.CANCEL, - Unpooled.EMPTY_BUFFER, - Unpooled.EMPTY_BUFFER)); - return frame; - } - } - - public static class Keepalive { - - private Keepalive() {} - - public static Frame from(ByteBuf data, boolean respond) { - final Frame frame = RECYCLER.get(); - frame.content = - ByteBufAllocator.DEFAULT.buffer( - KeepaliveFrameFlyweight.computeFrameLength(data.readableBytes())); - - final int flags = respond ? KeepaliveFrameFlyweight.FLAGS_KEEPALIVE_R : 0; - frame.content.writerIndex(KeepaliveFrameFlyweight.encode(frame.content, flags, data)); - - return frame; - } - - public static boolean hasRespondFlag(final Frame frame) { - ensureFrameType(FrameType.KEEPALIVE, frame); - final int flags = FrameHeaderFlyweight.flags(frame.content); - - return (flags & KeepaliveFrameFlyweight.FLAGS_KEEPALIVE_R) - == KeepaliveFrameFlyweight.FLAGS_KEEPALIVE_R; - } - } - - public static void ensureFrameType(final FrameType frameType, final Frame frame) { - final FrameType typeInFrame = frame.getType(); - - if (typeInFrame != frameType) { - throw new AssertionError("expected " + frameType + ", but saw" + typeInFrame); - } - } - - @Override - public String toString() { - FrameType type = FrameType.UNDEFINED; - StringBuilder payload = new StringBuilder(); - long streamId = -1; - String additionalFlags = ""; - - try { - type = FrameHeaderFlyweight.frameType(content); - - ByteBuf metadata = FrameHeaderFlyweight.sliceFrameMetadata(content); - if (0 < metadata.readableBytes()) { - payload.append( - String.format("metadata: \"%s\" ", metadata.toString(StandardCharsets.UTF_8))); - } - - ByteBuf data = FrameHeaderFlyweight.sliceFrameData(content); - if (0 < data.readableBytes()) { - payload.append(String.format("data: \"%s\" ", data.toString(StandardCharsets.UTF_8))); - } - - streamId = FrameHeaderFlyweight.streamId(content); - - switch (type) { - case LEASE: - additionalFlags = - " Permits: " + Lease.numberOfRequests(this) + " TTL: " + Lease.ttl(this); - break; - case REQUEST_N: - additionalFlags = " RequestN: " + RequestN.requestN(this); - break; - case KEEPALIVE: - additionalFlags = " Respond flag: " + Keepalive.hasRespondFlag(this); - break; - case REQUEST_STREAM: - case REQUEST_CHANNEL: - additionalFlags = " Initial Request N: " + Request.initialRequestN(this); - break; - case ERROR: - additionalFlags = " Error code: " + Error.errorCode(this); - break; - case SETUP: - int version = Setup.version(this); - additionalFlags = - " Version: " - + VersionFlyweight.toString(version) - + " keep-alive interval: " - + Setup.keepaliveInterval(this) - + " max lifetime: " - + Setup.maxLifetime(this) - + " metadata mime type: " - + Setup.metadataMimeType(this) - + " data mime type: " - + Setup.dataMimeType(this); - break; - } - } catch (Exception e) { - logger.error("Error generating toString, ignored.", e); - } - return "Frame => Stream ID: " - + streamId - + " Type: " - + type - + (!additionalFlags.isEmpty() ? additionalFlags : "") - + " Payload: " - + payload; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/FrameType.java b/rsocket-core/src/main/java/io/rsocket/FrameType.java deleted file mode 100644 index 8cd8a0426..000000000 --- a/rsocket-core/src/main/java/io/rsocket/FrameType.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket; - -/** Types of {@link Frame} that can be sent. */ -public enum FrameType { - // blank type that is not defined - UNDEFINED(0x00), - // Connection - SETUP(0x01, Flags.CAN_HAVE_METADATA_AND_DATA), - LEASE(0x02, Flags.CAN_HAVE_METADATA), - KEEPALIVE(0x03, Flags.CAN_HAVE_DATA), - // Requester to start request - REQUEST_RESPONSE(0x04, Flags.CAN_HAVE_METADATA_AND_DATA | Flags.IS_REQUEST_TYPE), - FIRE_AND_FORGET(0x05, Flags.CAN_HAVE_METADATA_AND_DATA | Flags.IS_REQUEST_TYPE), - REQUEST_STREAM( - 0x06, Flags.CAN_HAVE_METADATA_AND_DATA | Flags.IS_REQUEST_TYPE | Flags.HAS_INITIAL_REQUEST_N), - REQUEST_CHANNEL( - 0x07, Flags.CAN_HAVE_METADATA_AND_DATA | Flags.IS_REQUEST_TYPE | Flags.HAS_INITIAL_REQUEST_N), - // Requester mid-stream - REQUEST_N(0x08), - CANCEL(0x09, Flags.CAN_HAVE_METADATA), - // Responder - PAYLOAD(0x0A, Flags.CAN_HAVE_METADATA_AND_DATA), - ERROR(0x0B, Flags.CAN_HAVE_METADATA_AND_DATA), - // Requester & Responder - METADATA_PUSH(0x0C, Flags.CAN_HAVE_METADATA), - // Resumption frames, not yet implemented - RESUME(0x0D), - RESUME_OK(0x0E), - // synthetic types from Responder for use by the rest of the machinery - NEXT(0xA0, Flags.CAN_HAVE_METADATA_AND_DATA), - COMPLETE(0xB0), - NEXT_COMPLETE(0xC0, Flags.CAN_HAVE_METADATA_AND_DATA), - EXT(0xFFFF, Flags.CAN_HAVE_METADATA_AND_DATA); - - private static class Flags { - private Flags() {} - - private static final int CAN_HAVE_DATA = 0b0001; - private static final int CAN_HAVE_METADATA = 0b0010; - private static final int CAN_HAVE_METADATA_AND_DATA = 0b0011; - private static final int IS_REQUEST_TYPE = 0b0100; - private static final int HAS_INITIAL_REQUEST_N = 0b1000; - } - - private static FrameType[] typesById; - - private final int id; - private final int flags; - - /** Index types by id for indexed lookup. */ - static { - int max = 0; - - for (FrameType t : values()) { - max = Math.max(t.id, max); - } - - typesById = new FrameType[max + 1]; - - for (FrameType t : values()) { - typesById[t.id] = t; - } - } - - FrameType(final int id) { - this(id, 0); - } - - FrameType(int id, int flags) { - this.id = id; - this.flags = flags; - } - - public int getEncodedType() { - return id; - } - - public boolean isRequestType() { - return Flags.IS_REQUEST_TYPE == (flags & Flags.IS_REQUEST_TYPE); - } - - public boolean hasInitialRequestN() { - return Flags.HAS_INITIAL_REQUEST_N == (flags & Flags.HAS_INITIAL_REQUEST_N); - } - - public boolean canHaveData() { - return Flags.CAN_HAVE_DATA == (flags & Flags.CAN_HAVE_DATA); - } - - public boolean canHaveMetadata() { - return Flags.CAN_HAVE_METADATA == (flags & Flags.CAN_HAVE_METADATA); - } - - // TODO: offset of metadata and data (simplify parsing) naming: endOfFrameHeaderOffset() - public int payloadOffset() { - return 0; - } - - public static FrameType from(int id) { - return typesById[id]; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/Payload.java b/rsocket-core/src/main/java/io/rsocket/Payload.java index 9ab7522cc..fc130528e 100644 --- a/rsocket-core/src/main/java/io/rsocket/Payload.java +++ b/rsocket-core/src/main/java/io/rsocket/Payload.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,13 +13,92 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket; +import io.netty.buffer.ByteBuf; +import io.netty.util.ReferenceCounted; +import io.netty.util.ResourceLeakDetector; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +/** Payload of a Frame . */ +public interface Payload extends ReferenceCounted { + /** + * Returns whether the payload has metadata, useful for tell if metadata is empty or not present. + * + * @return whether payload has non-null (possibly empty) metadata + */ + boolean hasMetadata(); + + /** + * Returns a slice Payload metadata. Always non-null, check {@link #hasMetadata()} to + * differentiate null from "". + * + * @return payload metadata. + */ + ByteBuf sliceMetadata(); + + /** + * Returns the Payload data. Always non-null. + * + * @return payload data. + */ + ByteBuf sliceData(); + + /** + * Returns the Payloads' data without slicing if possible. This is not safe and editing this could + * effect the payload. It is recommended to call sliceData(). + * + * @return data as a bytebuf or slice of the data + */ + ByteBuf data(); + + /** + * Returns the Payloads' metadata without slicing if possible. This is not safe and editing this + * could effect the payload. It is recommended to call sliceMetadata(). + * + * @return metadata as a bytebuf or slice of the metadata + */ + ByteBuf metadata(); + + /** Increases the reference count by {@code 1}. */ + @Override + Payload retain(); + + /** Increases the reference count by the specified {@code increment}. */ + @Override + Payload retain(int increment); + + /** + * Records the current access location of this object for debugging purposes. If this object is + * determined to be leaked, the information recorded by this operation will be provided to you via + * {@link ResourceLeakDetector}. This method is a shortcut to {@link #touch(Object) touch(null)}. + */ + @Override + Payload touch(); + + /** + * Records the current access location of this object with an additional arbitrary information for + * debugging purposes. If this object is determined to be leaked, the information recorded by this + * operation will be provided to you via {@link ResourceLeakDetector}. + */ + @Override + Payload touch(Object hint); + + default ByteBuffer getMetadata() { + return sliceMetadata().nioBuffer(); + } + + default ByteBuffer getData() { + return sliceData().nioBuffer(); + } -/** Payload of a {@link Frame}. */ -public interface Payload { - ByteBuffer getMetadata(); + default String getMetadataUtf8() { + return sliceMetadata().toString(StandardCharsets.UTF_8); + } - ByteBuffer getData(); + default String getDataUtf8() { + return sliceData().toString(StandardCharsets.UTF_8); + } } diff --git a/rsocket-core/src/main/java/io/rsocket/RSocket.java b/rsocket-core/src/main/java/io/rsocket/RSocket.java index 0f006d5ca..b05241365 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocket.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -33,7 +33,9 @@ public interface RSocket extends Availability, Closeable { * @return {@code Publisher} that completes when the passed {@code payload} is successfully * handled, otherwise errors. */ - Mono fireAndForget(Payload payload); + default Mono fireAndForget(Payload payload) { + return RSocketAdapter.fireAndForget(payload); + } /** * Request-Response interaction model of {@code RSocket}. @@ -42,7 +44,9 @@ public interface RSocket extends Availability, Closeable { * @return {@code Publisher} containing at most a single {@code Payload} representing the * response. */ - Mono requestResponse(Payload payload); + default Mono requestResponse(Payload payload) { + return RSocketAdapter.requestResponse(payload); + } /** * Request-Stream interaction model of {@code RSocket}. @@ -50,7 +54,9 @@ public interface RSocket extends Availability, Closeable { * @param payload Request payload. * @return {@code Publisher} containing the stream of {@code Payload}s representing the response. */ - Flux requestStream(Payload payload); + default Flux requestStream(Payload payload) { + return RSocketAdapter.requestStream(payload); + } /** * Request-Channel interaction model of {@code RSocket}. @@ -58,7 +64,9 @@ public interface RSocket extends Availability, Closeable { * @param payloads Stream of request payloads. * @return Stream of response payloads. */ - Flux requestChannel(Publisher payloads); + default Flux requestChannel(Publisher payloads) { + return RSocketAdapter.requestChannel(payloads); + } /** * Metadata-Push interaction model of {@code RSocket}. @@ -67,10 +75,25 @@ public interface RSocket extends Availability, Closeable { * @return {@code Publisher} that completes when the passed {@code payload} is successfully * handled, otherwise errors. */ - Mono metadataPush(Payload payload); + default Mono metadataPush(Payload payload) { + return RSocketAdapter.metadataPush(payload); + } @Override default double availability() { - return 0.0; + return isDisposed() ? 0.0 : 1.0; + } + + @Override + default void dispose() {} + + @Override + default boolean isDisposed() { + return false; + } + + @Override + default Mono onClose() { + return Mono.never(); } } diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketAdapter.java b/rsocket-core/src/main/java/io/rsocket/RSocketAdapter.java new file mode 100644 index 000000000..b5a64b8dd --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/RSocketAdapter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Package private class with default implementations for use in {@link RSocket}. The main purpose + * is to hide static {@link UnsupportedOperationException} declarations. + * + * @since 1.0.3 + */ +class RSocketAdapter { + + private static final Mono UNSUPPORTED_FIRE_AND_FORGET = + Mono.error(new UnsupportedInteractionException("Fire-and-Forget")); + + private static final Mono UNSUPPORTED_REQUEST_RESPONSE = + Mono.error(new UnsupportedInteractionException("Request-Response")); + + private static final Flux UNSUPPORTED_REQUEST_STREAM = + Flux.error(new UnsupportedInteractionException("Request-Stream")); + + private static final Flux UNSUPPORTED_REQUEST_CHANNEL = + Flux.error(new UnsupportedInteractionException("Request-Channel")); + + private static final Mono UNSUPPORTED_METADATA_PUSH = + Mono.error(new UnsupportedInteractionException("Metadata-Push")); + + static Mono fireAndForget(Payload payload) { + payload.release(); + return RSocketAdapter.UNSUPPORTED_FIRE_AND_FORGET; + } + + static Mono requestResponse(Payload payload) { + payload.release(); + return RSocketAdapter.UNSUPPORTED_REQUEST_RESPONSE; + } + + static Flux requestStream(Payload payload) { + payload.release(); + return RSocketAdapter.UNSUPPORTED_REQUEST_STREAM; + } + + static Flux requestChannel(Publisher payloads) { + return RSocketAdapter.UNSUPPORTED_REQUEST_CHANNEL; + } + + static Mono metadataPush(Payload payload) { + payload.release(); + return RSocketAdapter.UNSUPPORTED_METADATA_PUSH; + } + + private static class UnsupportedInteractionException extends RuntimeException { + + private static final long serialVersionUID = 5084623297446471999L; + + UnsupportedInteractionException(String interactionName) { + super(interactionName + " not implemented.", null, false, false); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java deleted file mode 100644 index 84a344da0..000000000 --- a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java +++ /dev/null @@ -1,499 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import io.netty.buffer.Unpooled; -import io.netty.util.collection.IntObjectHashMap; -import io.rsocket.exceptions.ConnectionException; -import io.rsocket.exceptions.Exceptions; -import io.rsocket.internal.LimitableRequestPublisher; -import io.rsocket.util.PayloadImpl; -import java.nio.channels.ClosedChannelException; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import javax.annotation.Nullable; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import reactor.core.Disposable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; -import reactor.core.publisher.UnicastProcessor; - -/** Client Side of a RSocket socket. Sends {@link Frame}s to a {@link RSocketServer} */ -class RSocketClient implements RSocket { - - private static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = - new ClosedChannelException(); - - private final DuplexConnection connection; - private final Consumer errorConsumer; - private final StreamIdSupplier streamIdSupplier; - private final MonoProcessor started; - private final IntObjectHashMap senders; - private final IntObjectHashMap> receivers; - private final AtomicInteger missedAckCounter; - - private @Nullable Disposable keepAliveSendSub; - - private volatile long timeLastTickSentMs; - - RSocketClient( - DuplexConnection connection, - Consumer errorConsumer, - StreamIdSupplier streamIdSupplier) { - this(connection, errorConsumer, streamIdSupplier, Duration.ZERO, Duration.ZERO, 0); - } - - RSocketClient( - DuplexConnection connection, - Consumer errorConsumer, - StreamIdSupplier streamIdSupplier, - Duration tickPeriod, - Duration ackTimeout, - int missedAcks) { - this.connection = connection; - this.errorConsumer = errorConsumer; - this.streamIdSupplier = streamIdSupplier; - this.started = MonoProcessor.create(); - this.senders = new IntObjectHashMap<>(256, 0.9f); - this.receivers = new IntObjectHashMap<>(256, 0.9f); - this.missedAckCounter = new AtomicInteger(); - - if (!Duration.ZERO.equals(tickPeriod)) { - long ackTimeoutMs = ackTimeout.toMillis(); - - this.keepAliveSendSub = - started - .thenMany(Flux.interval(tickPeriod)) - .doOnSubscribe(s -> timeLastTickSentMs = System.currentTimeMillis()) - .flatMap(i -> sendKeepAlive(ackTimeoutMs, missedAcks)) - .doOnError( - t -> { - errorConsumer.accept(t); - connection.close().subscribe(); - }) - .subscribe(); - } - - connection - .onClose() - .doFinally(signalType -> cleanup()) - .doOnError(errorConsumer::accept) - .subscribe(); - - connection - .receive() - .doOnSubscribe(subscription -> started.onComplete()) - .doOnNext(this::handleIncomingFrames) - .doOnError(errorConsumer) - .subscribe(); - } - - private Mono sendKeepAlive(long ackTimeoutMs, int missedAcks) { - long now = System.currentTimeMillis(); - if (now - timeLastTickSentMs > ackTimeoutMs) { - int count = missedAckCounter.incrementAndGet(); - if (count >= missedAcks) { - String message = - String.format( - "Missed %d keep-alive acks with a threshold of %d and a ack timeout of %d ms", - count, missedAcks, ackTimeoutMs); - return Mono.error(new ConnectionException(message)); - } - } - - return connection.sendOne(Frame.Keepalive.from(Unpooled.EMPTY_BUFFER, true)); - } - - @Override - public Mono fireAndForget(Payload payload) { - Mono defer = - Mono.defer( - () -> { - final int streamId = streamIdSupplier.nextStreamId(); - final Frame requestFrame = - Frame.Request.from(streamId, FrameType.FIRE_AND_FORGET, payload, 1); - return connection.sendOne(requestFrame); - }); - - return started.then(defer); - } - - @Override - public Mono requestResponse(Payload payload) { - return handleRequestResponse(payload); - } - - @Override - public Flux requestStream(Payload payload) { - return handleStreamResponse(Flux.just(payload), FrameType.REQUEST_STREAM); - } - - @Override - public Flux requestChannel(Publisher payloads) { - return handleStreamResponse(Flux.from(payloads), FrameType.REQUEST_CHANNEL); - } - - @Override - public Mono metadataPush(Payload payload) { - final Frame requestFrame = Frame.Request.from(0, FrameType.METADATA_PUSH, payload, 1); - return connection.sendOne(requestFrame); - } - - @Override - public double availability() { - return connection.availability(); - } - - @Override - public Mono close() { - return connection.close(); - } - - @Override - public Mono onClose() { - return connection.onClose(); - } - - private Mono handleRequestResponse(final Payload payload) { - return started.then( - Mono.defer( - () -> { - int streamId = streamIdSupplier.nextStreamId(); - final Frame requestFrame = - Frame.Request.from(streamId, FrameType.REQUEST_RESPONSE, payload, 1); - - MonoProcessor receiver = MonoProcessor.create(); - - synchronized (this) { - receivers.put(streamId, receiver); - } - - MonoProcessor subscribedRequest = - connection - .sendOne(requestFrame) - .doOnError( - t -> { - errorConsumer.accept(t); - receiver.cancel(); - }) - .toProcessor(); - subscribedRequest.subscribe(); - - return receiver - .doOnError( - t -> { - if (contains(streamId) - && connection.availability() > 0.0 - && !receiver.isTerminated()) { - connection - .sendOne(Frame.Error.from(streamId, t)) - .doOnError(errorConsumer::accept) - .subscribe(); - } - }) - .doOnCancel( - () -> { - if (contains(streamId) - && connection.availability() > 0.0 - && !receiver.isTerminated()) { - connection - .sendOne(Frame.Cancel.from(streamId)) - .doOnError(errorConsumer::accept) - .subscribe(); - } - subscribedRequest.cancel(); - }) - .doFinally(s -> removeReceiver(streamId)); - })); - } - - private Flux handleStreamResponse(Flux request, FrameType requestType) { - return started.thenMany( - Flux.defer( - new Supplier>() { - final UnicastProcessor receiver = UnicastProcessor.create(); - final int streamId = streamIdSupplier.nextStreamId(); - volatile @Nullable MonoProcessor subscribedRequests; - boolean firstRequest = true; - - boolean isValidToSendFrame() { - return contains(streamId) - && connection.availability() > 0.0 - && !receiver.isTerminated(); - } - - void sendOneFrame(Frame frame) { - if (isValidToSendFrame()) { - connection.sendOne(frame).doOnError(errorConsumer::accept).subscribe(); - } - } - - @Override - public Flux get() { - return receiver - .doOnRequest( - l -> { - boolean _firstRequest = false; - synchronized (RSocketClient.this) { - if (firstRequest) { - _firstRequest = true; - firstRequest = false; - } - } - - if (_firstRequest) { - Flux requestFrames = - request - .transform( - f -> { - LimitableRequestPublisher wrapped = - LimitableRequestPublisher.wrap(f); - // Need to set this to one for first the frame - wrapped.increaseRequestLimit(1); - synchronized (RSocketClient.this) { - senders.put(streamId, wrapped); - receivers.put(streamId, receiver); - } - - return wrapped; - }) - .map( - new Function() { - boolean firstPayload = true; - - @Override - public Frame apply(Payload payload) { - boolean _firstPayload = false; - synchronized (this) { - if (firstPayload) { - firstPayload = false; - _firstPayload = true; - } - } - - if (_firstPayload) { - return Frame.Request.from( - streamId, requestType, payload, l); - } else { - return Frame.PayloadFrame.from( - streamId, FrameType.NEXT, payload); - } - } - }) - .doOnComplete( - () -> { - if (FrameType.REQUEST_CHANNEL == requestType) { - sendOneFrame( - Frame.PayloadFrame.from( - streamId, FrameType.COMPLETE)); - } - }); - - subscribedRequests = - connection - .send(requestFrames) - .doOnError( - t -> { - errorConsumer.accept(t); - receiver.cancel(); - }) - .toProcessor(); - subscribedRequests.subscribe(); - } else { - sendOneFrame(Frame.RequestN.from(streamId, l)); - } - }) - .doOnError(t -> sendOneFrame(Frame.Error.from(streamId, t))) - .doOnCancel( - () -> { - sendOneFrame(Frame.Cancel.from(streamId)); - if (subscribedRequests != null) { - subscribedRequests.cancel(); - } - }) - .doFinally( - s -> { - removeReceiver(streamId); - removeSender(streamId); - }); - } - })); - } - - private boolean contains(int streamId) { - synchronized (RSocketClient.this) { - return receivers.containsKey(streamId); - } - } - - protected void cleanup() { - senders.forEach( - (integer, limitableRequestPublisher) -> - cleanUpLimitableRequestPublisher(limitableRequestPublisher)); - - receivers.forEach((integer, subscriber) -> cleanUpSubscriber(subscriber)); - - synchronized (this) { - senders.clear(); - receivers.clear(); - } - - if (null != keepAliveSendSub) { - keepAliveSendSub.dispose(); - } - } - - private synchronized void cleanUpLimitableRequestPublisher( - LimitableRequestPublisher limitableRequestPublisher) { - limitableRequestPublisher.cancel(); - } - - private synchronized void cleanUpSubscriber(Subscriber subscriber) { - subscriber.onError(CLOSED_CHANNEL_EXCEPTION); - } - - private void handleIncomingFrames(Frame frame) { - try { - int streamId = frame.getStreamId(); - FrameType type = frame.getType(); - if (streamId == 0) { - handleStreamZero(type, frame); - } else { - handleFrame(streamId, type, frame); - } - } finally { - frame.release(); - } - } - - private void handleStreamZero(FrameType type, Frame frame) { - switch (type) { - case ERROR: - throw Exceptions.from(frame); - case LEASE: - { - break; - } - case KEEPALIVE: - if (!Frame.Keepalive.hasRespondFlag(frame)) { - timeLastTickSentMs = System.currentTimeMillis(); - } - break; - default: - // Ignore unknown frames. Throwing an error will close the socket. - errorConsumer.accept( - new IllegalStateException( - "Client received supported frame on stream 0: " + frame.toString())); - } - } - - @SuppressWarnings("unchecked") - private void handleFrame(int streamId, FrameType type, Frame frame) { - Subscriber receiver; - synchronized (this) { - receiver = receivers.get(streamId); - } - if (receiver == null) { - handleMissingResponseProcessor(streamId, type, frame); - } else { - switch (type) { - case ERROR: - receiver.onError(Exceptions.from(frame)); - removeReceiver(streamId); - break; - case NEXT_COMPLETE: - receiver.onNext(new PayloadImpl(frame)); - receiver.onComplete(); - break; - case CANCEL: - { - LimitableRequestPublisher sender; - synchronized (this) { - sender = senders.remove(streamId); - removeReceiver(streamId); - } - if (sender != null) { - sender.cancel(); - } - break; - } - case NEXT: - receiver.onNext(new PayloadImpl(frame)); - break; - case REQUEST_N: - { - LimitableRequestPublisher sender; - synchronized (this) { - sender = senders.get(streamId); - } - if (sender != null) { - int n = Frame.RequestN.requestN(frame); - sender.increaseRequestLimit(n); - } - break; - } - case COMPLETE: - receiver.onComplete(); - synchronized (this) { - receivers.remove(streamId); - } - break; - default: - throw new IllegalStateException( - "Client received supported frame on stream " + streamId + ": " + frame.toString()); - } - } - } - - private void handleMissingResponseProcessor(int streamId, FrameType type, Frame frame) { - if (!streamIdSupplier.isBeforeOrCurrent(streamId)) { - if (type == FrameType.ERROR) { - // message for stream that has never existed, we have a problem with - // the overall connection and must tear down - String errorMessage = StandardCharsets.UTF_8.decode(frame.getData()).toString(); - - throw new IllegalStateException( - "Client received error for non-existent stream: " - + streamId - + " Message: " - + errorMessage); - } else { - throw new IllegalStateException( - "Client received message for non-existent stream: " - + streamId - + ", frame type: " - + type); - } - } - // receiving a frame after a given stream has been cancelled/completed, - // so ignore (cancellation is async so there is a race condition) - } - - private synchronized void removeReceiver(int streamId) { - receivers.remove(streamId); - } - - private synchronized void removeSender(int streamId) { - senders.remove(streamId); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketErrorException.java b/rsocket-core/src/main/java/io/rsocket/RSocketErrorException.java new file mode 100644 index 000000000..b43b14bae --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/RSocketErrorException.java @@ -0,0 +1,82 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket; + +import reactor.util.annotation.Nullable; + +/** + * Exception that represents an RSocket protocol error. + * + * @see ERROR + * Frame (0x0B) + */ +public class RSocketErrorException extends RuntimeException { + + private static final long serialVersionUID = -1628781753426267554L; + + private static final int MIN_ERROR_CODE = 0x00000001; + + private static final int MAX_ERROR_CODE = 0xFFFFFFFE; + + private final int errorCode; + + /** + * Constructor with a protocol error code and a message. + * + * @param errorCode the RSocket protocol error code + * @param message error explanation + */ + public RSocketErrorException(int errorCode, String message) { + this(errorCode, message, null); + } + + /** + * Alternative to {@link #RSocketErrorException(int, String)} with a root cause. + * + * @param errorCode the RSocket protocol error code + * @param message error explanation + * @param cause a root cause for the error + */ + public RSocketErrorException(int errorCode, String message, @Nullable Throwable cause) { + super(message, cause); + this.errorCode = errorCode; + if (errorCode > MAX_ERROR_CODE && errorCode < MIN_ERROR_CODE) { + throw new IllegalArgumentException( + "Allowed errorCode value should be in range [0x00000001-0xFFFFFFFE]", this); + } + } + + /** + * Return the RSocket error code + * represented by this exception + * + * @return the RSocket protocol error code + */ + public int errorCode() { + return errorCode; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + " (0x" + + Integer.toHexString(errorCode) + + "): " + + getMessage(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 4f05c5506..e23bcceb2 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -1,409 +1,571 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ - package io.rsocket; -import io.rsocket.exceptions.InvalidSetupException; -import io.rsocket.fragmentation.FragmentationDuplexConnection; -import io.rsocket.frame.SetupFrameFlyweight; -import io.rsocket.frame.VersionFlyweight; -import io.rsocket.internal.ClientServerInputMultiplexer; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.core.Resume; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.lease.LeaseStats; +import io.rsocket.lease.Leases; import io.rsocket.plugins.DuplexConnectionInterceptor; -import io.rsocket.plugins.PluginRegistry; -import io.rsocket.plugins.Plugins; import io.rsocket.plugins.RSocketInterceptor; +import io.rsocket.plugins.SocketAcceptorInterceptor; +import io.rsocket.resume.ClientResume; +import io.rsocket.resume.ResumableFramesStore; +import io.rsocket.resume.ResumeStrategy; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; -import io.rsocket.util.PayloadImpl; import java.time.Duration; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; -/** Factory for creating RSocket clients and servers. */ -public interface RSocketFactory { - /** Creates a factory that establishes client connections to other RSockets */ - static ClientRSocketFactory connect() { +/** + * Main entry point to create RSocket clients or servers as follows: + * + *

    + *
  • {@link ClientRSocketFactory} to connect as a client. Use {@link #connect()} for a default + * instance. + *
  • {@link ServerRSocketFactory} to start a server. Use {@link #receive()} for a default + * instance. + *
+ * + * @deprecated please use {@link RSocketConnector} and {@link RSocketServer}. + */ +@Deprecated +public final class RSocketFactory { + + /** + * Create a {@code ClientRSocketFactory} to connect to a remote RSocket endpoint. Internally + * delegates to {@link RSocketConnector}. + * + * @return the {@code ClientRSocketFactory} instance + */ + public static ClientRSocketFactory connect() { return new ClientRSocketFactory(); } - /** Creates a factory that receives server connections from client RSockets */ - static ServerRSocketFactory receive() { + /** + * Create a {@code ServerRSocketFactory} to accept connections from RSocket clients. Internally + * delegates to {@link RSocketServer}. + * + * @return the {@code ClientRSocketFactory} instance + */ + public static ServerRSocketFactory receive() { return new ServerRSocketFactory(); } - interface Start { + public interface Start { Mono start(); } - interface SetupPayload { - T setupPayload(Payload payload); - } - - interface Transport { - Start transport(Supplier t); - - default Start transport(T t) { - return transport(() -> t); - } - } - - interface Acceptor { - Transport acceptor(Supplier acceptor); + public interface ClientTransportAcceptor { + Start transport(Supplier transport); - default Transport acceptor(A acceptor) { - return acceptor(() -> acceptor); + default Start transport(ClientTransport transport) { + return transport(() -> transport); } } - interface Fragmentation< - R extends Acceptor, - T extends io.rsocket.transport.Transport, - A, - B extends Closeable> { - R fragment(int mtu); - } - - interface ErrorConsumer< - R extends Acceptor, - T extends io.rsocket.transport.Transport, - A, - B extends Closeable> { - R errorConsumer(Consumer errorConsumer); - } + public interface ServerTransportAcceptor { - interface KeepAlive { - T keepAlive(); + ServerTransport.ConnectionAcceptor toConnectionAcceptor(); - T keepAlive(Duration tickPeriod, Duration ackTimeout, int missedAcks); + Start transport(Supplier> transport); - T keepAliveTickPeriod(Duration tickPeriod); - - T keepAliveAckTimeout(Duration ackTimeout); - - T keepAliveMissedAcks(int missedAcks); + default Start transport(ServerTransport transport) { + return transport(() -> transport); + } } - interface MimeType { - T mimeType(String dataMineType, String metadataMimeType); - - T dataMineType(String dataMineType); + /** Factory to create and configure an RSocket client, and connect to a server. */ + public static class ClientRSocketFactory implements ClientTransportAcceptor { + private static final ClientResume CLIENT_RESUME = + new ClientResume(Duration.ofMinutes(2), Unpooled.EMPTY_BUFFER); - T metadataMimeType(String metadataMimeType); - } - - class ClientRSocketFactory - implements KeepAlive, - MimeType, - Acceptor, RSocket>, - Transport, - Fragmentation, RSocket>, - ErrorConsumer, RSocket>, - SetupPayload { + private final RSocketConnector connector; - private Supplier> acceptor = - () -> rSocket -> new AbstractRSocket() {}; + private Duration tickPeriod = Duration.ofSeconds(20); + private Duration ackTimeout = Duration.ofSeconds(30); + private int missedAcks = 3; - private Supplier transportClient; - private Consumer errorConsumer = Throwable::printStackTrace; - private int mtu = 0; - private PluginRegistry plugins = new PluginRegistry(Plugins.defaultPlugins()); - private int flags = SetupFrameFlyweight.FLAGS_STRICT_INTERPRETATION; + private Resume resume; - private Payload setupPayload = PayloadImpl.EMPTY; + public ClientRSocketFactory() { + this(RSocketConnector.create()); + } - private Duration tickPeriod = Duration.ZERO; - private Duration ackTimeout = Duration.ofSeconds(30); - private int missedAcks = 3; + public ClientRSocketFactory(RSocketConnector connector) { + this.connector = connector; + } - private String dataMineType = "application/binary"; - private String metadataMimeType = "application/binary"; + /** + * @deprecated this method is deprecated and deliberately has no effect anymore. Right now, in + * order configure the custom {@link ByteBufAllocator} it is recommended to use the + * following setup for Reactor Netty based transport:
+ * 1. For Client:
+ *
{@code
+     * TcpClient.create()
+     *          ...
+     *          .bootstrap(bootstrap -> bootstrap.option(ChannelOption.ALLOCATOR, clientAllocator))
+     * }
+ *
+ * 2. For server:
+ *
{@code
+     * TcpServer.create()
+     *          ...
+     *          .bootstrap(serverBootstrap -> serverBootstrap.childOption(ChannelOption.ALLOCATOR, serverAllocator))
+     * }
+ * Or in case of local transport, to use corresponding factory method {@code + * LocalClientTransport.creat(String, ByteBufAllocator)} + * @param allocator instance of {@link ByteBufAllocator} + * @return this factory instance + */ + public ClientRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { + return this; + } public ClientRSocketFactory addConnectionPlugin(DuplexConnectionInterceptor interceptor) { - plugins.addConnectionPlugin(interceptor); + connector.interceptors(registry -> registry.forConnection(interceptor)); return this; } + /** Deprecated. Use {@link #addRequesterPlugin(RSocketInterceptor)} instead */ + @Deprecated public ClientRSocketFactory addClientPlugin(RSocketInterceptor interceptor) { - plugins.addClientPlugin(interceptor); + return addRequesterPlugin(interceptor); + } + + public ClientRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor) { + connector.interceptors(registry -> registry.forRequester(interceptor)); return this; } + /** Deprecated. Use {@link #addResponderPlugin(RSocketInterceptor)} instead */ + @Deprecated public ClientRSocketFactory addServerPlugin(RSocketInterceptor interceptor) { - plugins.addServerPlugin(interceptor); + return addResponderPlugin(interceptor); + } + + public ClientRSocketFactory addResponderPlugin(RSocketInterceptor interceptor) { + connector.interceptors(registry -> registry.forResponder(interceptor)); return this; } - @Override + public ClientRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor) { + connector.interceptors(registry -> registry.forSocketAcceptor(interceptor)); + return this; + } + + /** + * Deprecated without replacement as Keep-Alive is not optional according to spec + * + * @return this ClientRSocketFactory + */ + @Deprecated public ClientRSocketFactory keepAlive() { - tickPeriod = Duration.ofSeconds(20); + connector.keepAlive(tickPeriod, ackTimeout.plus(tickPeriod.multipliedBy(missedAcks))); return this; } - @Override - public ClientRSocketFactory keepAlive( + public ClientTransportAcceptor keepAlive( Duration tickPeriod, Duration ackTimeout, int missedAcks) { this.tickPeriod = tickPeriod; this.ackTimeout = ackTimeout; this.missedAcks = missedAcks; + keepAlive(); return this; } - @Override public ClientRSocketFactory keepAliveTickPeriod(Duration tickPeriod) { this.tickPeriod = tickPeriod; + keepAlive(); return this; } - @Override public ClientRSocketFactory keepAliveAckTimeout(Duration ackTimeout) { this.ackTimeout = ackTimeout; + keepAlive(); return this; } - @Override public ClientRSocketFactory keepAliveMissedAcks(int missedAcks) { this.missedAcks = missedAcks; + keepAlive(); return this; } - @Override - public ClientRSocketFactory mimeType(String dataMineType, String metadataMimeType) { - this.dataMineType = dataMineType; - this.metadataMimeType = metadataMimeType; + public ClientRSocketFactory mimeType(String metadataMimeType, String dataMimeType) { + connector.metadataMimeType(metadataMimeType); + connector.dataMimeType(dataMimeType); return this; } - @Override - public ClientRSocketFactory dataMineType(String dataMineType) { - this.dataMineType = dataMineType; + public ClientRSocketFactory dataMimeType(String dataMimeType) { + connector.dataMimeType(dataMimeType); return this; } - @Override public ClientRSocketFactory metadataMimeType(String metadataMimeType) { - this.metadataMimeType = metadataMimeType; + connector.metadataMimeType(metadataMimeType); return this; } - @Override - public Start transport(Supplier t) { - return new ClientTransport().transport(t); + public ClientRSocketFactory lease(Supplier> supplier) { + connector.lease(supplier); + return this; } - protected class ClientTransport - implements Transport { - @Override - public Start transport( - Supplier transportClient) { - ClientRSocketFactory.this.transportClient = transportClient; - return new StartClient(); - } + public ClientRSocketFactory lease() { + connector.lease(Leases::new); + return this; } - @Override - public Transport acceptor( - Supplier> acceptor) { - this.acceptor = acceptor; - return new ClientTransport(); + /** @deprecated without a replacement and no longer used. */ + @Deprecated + public ClientRSocketFactory singleSubscriberRequester() { + return this; + } + + /** + * Enables a reconnectable, shared instance of {@code Mono} so every subscriber will + * observe the same RSocket instance up on connection establishment.
+ * For example: + * + *
{@code
+     * Mono sharedRSocketMono =
+     *   RSocketFactory
+     *                .connect()
+     *                .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
+     *                .transport(transport)
+     *                .start();
+     *
+     *  RSocket r1 = sharedRSocketMono.block();
+     *  RSocket r2 = sharedRSocketMono.block();
+     *
+     *  assert r1 == r2;
+     *
+     * }
+ * + * Apart of the shared behavior, if the connection is lost, the same {@code Mono} + * instance will transparently re-establish the connection for subsequent subscribers.
+ * For example: + * + *
{@code
+     * Mono sharedRSocketMono =
+     *   RSocketFactory
+     *                .connect()
+     *                .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
+     *                .transport(transport)
+     *                .start();
+     *
+     *  RSocket r1 = sharedRSocketMono.block();
+     *  RSocket r2 = sharedRSocketMono.block();
+     *
+     *  assert r1 == r2;
+     *
+     *  r1.dispose()
+     *
+     *  assert r2.isDisposed()
+     *
+     *  RSocket r3 = sharedRSocketMono.block();
+     *  RSocket r4 = sharedRSocketMono.block();
+     *
+     *
+     *  assert r1 != r3;
+     *  assert r4 == r3;
+     *
+     * }
+ * + * Note, having reconnect() enabled does not eliminate the need to accompany each + * individual request with the corresponding retry logic.
+ * For example: + * + *
{@code
+     * Mono sharedRSocketMono =
+     *   RSocketFactory
+     *                .connect()
+     *                .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
+     *                .transport(transport)
+     *                .start();
+     *
+     *  sharedRSocket.flatMap(rSocket -> rSocket.requestResponse(...))
+     *               .retryWhen(ownRetry)
+     *               .subscribe()
+     *
+     * }
+ * + * @param retrySpec a retry factory applied for {@link Mono#retryWhen(Retry)} + * @return a shared instance of {@code Mono}. + */ + public ClientRSocketFactory reconnect(Retry retrySpec) { + connector.reconnect(retrySpec); + return this; + } + + public ClientRSocketFactory resume() { + resume = resume != null ? resume : new Resume(); + connector.resume(resume); + return this; + } + + public ClientRSocketFactory resumeToken(Supplier supplier) { + resume(); + resume.token(supplier); + return this; + } + + public ClientRSocketFactory resumeStore( + Function storeFactory) { + resume(); + resume.storeFactory(storeFactory); + return this; + } + + public ClientRSocketFactory resumeSessionDuration(Duration sessionDuration) { + resume(); + resume.sessionDuration(sessionDuration); + return this; + } + + public ClientRSocketFactory resumeStreamTimeout(Duration streamTimeout) { + resume(); + resume.streamTimeout(streamTimeout); + return this; + } + + public ClientRSocketFactory resumeStrategy(Supplier strategy) { + resume(); + resume.retry( + Retry.from( + signals -> signals.flatMap(s -> strategy.get().apply(CLIENT_RESUME, s.failure())))); + return this; + } + + public ClientRSocketFactory resumeCleanupOnKeepAlive() { + resume(); + resume.cleanupStoreOnKeepAlive(); + return this; + } + + public Start transport(Supplier transport) { + return () -> connector.connect(transport); + } + + public ClientTransportAcceptor acceptor(Function acceptor) { + return acceptor(() -> acceptor); + } + + public ClientTransportAcceptor acceptor(Supplier> acceptorSupplier) { + return acceptor( + (setup, sendingSocket) -> { + acceptorSupplier.get().apply(sendingSocket); + return Mono.empty(); + }); + } + + public ClientTransportAcceptor acceptor(SocketAcceptor acceptor) { + connector.acceptor(acceptor); + return this; } - @Override public ClientRSocketFactory fragment(int mtu) { - this.mtu = mtu; + connector.fragment(mtu); return this; } - @Override + /** + * @deprecated this handler is deliberately no-ops and is deprecated with no replacement. In + * order to observe errors, it is recommended to add error handler using {@code doOnError} + * on the specific logical stream. In order to observe connection, or RSocket terminal + * errors, it is recommended to hook on {@link Closeable#onClose()} handler. + */ public ClientRSocketFactory errorConsumer(Consumer errorConsumer) { - this.errorConsumer = errorConsumer; return this; } - @Override public ClientRSocketFactory setupPayload(Payload payload) { - this.setupPayload = payload; - return this; - } - - protected class StartClient implements Start { - @Override - public Mono start() { - return transportClient - .get() - .connect() - .flatMap( - connection -> { - Frame setupFrame = - Frame.Setup.from( - flags, - (int) ackTimeout.toMillis(), - (int) ackTimeout.toMillis() * missedAcks, - dataMineType, - metadataMimeType, - setupPayload); - - if (mtu > 0) { - connection = new FragmentationDuplexConnection(connection, mtu); - } - - ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(connection, plugins); - - RSocketClient rSocketClient = - new RSocketClient( - multiplexer.asClientConnection(), - errorConsumer, - StreamIdSupplier.clientSupplier(), - tickPeriod, - ackTimeout, - missedAcks); - - Mono wrappedRSocketClient = - Mono.just(rSocketClient).map(plugins::applyClient); - - DuplexConnection finalConnection = connection; - return wrappedRSocketClient.flatMap( - wrappedClientRSocket -> { - RSocket unwrappedServerSocket = acceptor.get().apply(wrappedClientRSocket); - - Mono wrappedRSocketServer = - Mono.just(unwrappedServerSocket).map(plugins::applyServer); - - return wrappedRSocketServer - .doOnNext( - rSocket -> - new RSocketServer( - multiplexer.asServerConnection(), rSocket, errorConsumer)) - .then(finalConnection.sendOne(setupFrame)) - .then(wrappedRSocketClient); - }); - }); - } + connector.setupPayload(payload); + return this; + } + + public ClientRSocketFactory frameDecoder(PayloadDecoder payloadDecoder) { + connector.payloadDecoder(payloadDecoder); + return this; } } - class ServerRSocketFactory - implements Acceptor, - Fragmentation, - ErrorConsumer { + /** Factory to create, configure, and start an RSocket server. */ + public static class ServerRSocketFactory implements ServerTransportAcceptor { + private final RSocketServer server; - private Supplier acceptor; - private Supplier transportServer; - private Consumer errorConsumer = Throwable::printStackTrace; - private int mtu = 0; - private PluginRegistry plugins = new PluginRegistry(Plugins.defaultPlugins()); + private Resume resume; - private ServerRSocketFactory() {} + public ServerRSocketFactory() { + this(RSocketServer.create()); + } - public ServerRSocketFactory addConnectionPlugin(DuplexConnectionInterceptor interceptor) { - plugins.addConnectionPlugin(interceptor); + public ServerRSocketFactory(RSocketServer server) { + this.server = server; + } + + /** + * @deprecated this method is deprecated and deliberately has no effect anymore. Right now, in + * order configure the custom {@link ByteBufAllocator} it is recommended to use the + * following setup for Reactor Netty based transport:
+ * 1. For Client:
+ *
{@code
+     * TcpClient.create()
+     *          ...
+     *          .bootstrap(bootstrap -> bootstrap.option(ChannelOption.ALLOCATOR, clientAllocator))
+     * }
+ *
+ * 2. For server:
+ *
{@code
+     * TcpServer.create()
+     *          ...
+     *          .bootstrap(serverBootstrap -> serverBootstrap.childOption(ChannelOption.ALLOCATOR, serverAllocator))
+     * }
+ * Or in case of local transport, to use corresponding factory method {@code + * LocalClientTransport.creat(String, ByteBufAllocator)} + * @param allocator instance of {@link ByteBufAllocator} + * @return this factory instance + */ + @Deprecated + public ServerRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { return this; } + public ServerRSocketFactory addConnectionPlugin(DuplexConnectionInterceptor interceptor) { + server.interceptors(registry -> registry.forConnection(interceptor)); + return this; + } + /** Deprecated. Use {@link #addRequesterPlugin(RSocketInterceptor)} instead */ + @Deprecated public ServerRSocketFactory addClientPlugin(RSocketInterceptor interceptor) { - plugins.addClientPlugin(interceptor); + return addRequesterPlugin(interceptor); + } + + public ServerRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor) { + server.interceptors(registry -> registry.forRequester(interceptor)); return this; } + /** Deprecated. Use {@link #addResponderPlugin(RSocketInterceptor)} instead */ + @Deprecated public ServerRSocketFactory addServerPlugin(RSocketInterceptor interceptor) { - plugins.addServerPlugin(interceptor); + return addResponderPlugin(interceptor); + } + + public ServerRSocketFactory addResponderPlugin(RSocketInterceptor interceptor) { + server.interceptors(registry -> registry.forResponder(interceptor)); return this; } - @Override - public Transport acceptor( - Supplier acceptor) { - this.acceptor = acceptor; - return new ServerTransport(); + public ServerRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor) { + server.interceptors(registry -> registry.forSocketAcceptor(interceptor)); + return this; + } + + public ServerTransportAcceptor acceptor(SocketAcceptor acceptor) { + server.acceptor(acceptor); + return this; + } + + public ServerRSocketFactory frameDecoder(PayloadDecoder payloadDecoder) { + server.payloadDecoder(payloadDecoder); + return this; } - @Override public ServerRSocketFactory fragment(int mtu) { - this.mtu = mtu; + server.fragment(mtu); return this; } - @Override + /** + * @deprecated this handler is deliberately no-ops and is deprecated with no replacement. In + * order to observe errors, it is recommended to add error handler using {@code doOnError} + * on the specific logical stream. In order to observe connection, or RSocket terminal + * errors, it is recommended to hook on {@link Closeable#onClose()} handler. + */ public ServerRSocketFactory errorConsumer(Consumer errorConsumer) { - this.errorConsumer = errorConsumer; - return this; - } - - private class ServerTransport - implements Transport { - @Override - public Start transport(Supplier transportServer) { - ServerRSocketFactory.this.transportServer = transportServer; - return new ServerStart(); - } - } - - private class ServerStart implements Start { - @Override - public Mono start() { - return transportServer - .get() - .start( - connection -> { - if (mtu > 0) { - connection = new FragmentationDuplexConnection(connection, mtu); - } - - ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(connection, plugins); - - return multiplexer - .asStreamZeroConnection() - .receive() - .next() - .flatMap(setupFrame -> processSetupFrame(multiplexer, setupFrame)); - }); - } - - private Mono processSetupFrame( - ClientServerInputMultiplexer multiplexer, Frame setupFrame) { - int version = Frame.Setup.version(setupFrame); - if (version != SetupFrameFlyweight.CURRENT_VERSION) { - InvalidSetupException error = - new InvalidSetupException( - "Unsupported version " + VersionFlyweight.toString(version)); - return multiplexer - .asStreamZeroConnection() - .sendOne(Frame.Error.from(0, error)) - .then(multiplexer.close()); - } - - ConnectionSetupPayload setupPayload = ConnectionSetupPayload.create(setupFrame); - - RSocketClient rSocketClient = - new RSocketClient( - multiplexer.asServerConnection(), errorConsumer, StreamIdSupplier.serverSupplier()); - - Mono wrappedRSocketClient = Mono.just(rSocketClient).map(plugins::applyClient); - - return wrappedRSocketClient - .flatMap( - sender -> acceptor.get().accept(setupPayload, sender).map(plugins::applyServer)) - .map( - handler -> - new RSocketServer(multiplexer.asClientConnection(), handler, errorConsumer)) - .then(); - } + return this; + } + + public ServerRSocketFactory lease(Supplier> supplier) { + server.lease(supplier); + return this; + } + + public ServerRSocketFactory lease() { + server.lease(Leases::new); + return this; + } + + /** @deprecated without a replacement and no longer used. */ + @Deprecated + public ServerRSocketFactory singleSubscriberRequester() { + return this; + } + + public ServerRSocketFactory resume() { + resume = resume != null ? resume : new Resume(); + server.resume(resume); + return this; + } + + public ServerRSocketFactory resumeStore( + Function storeFactory) { + resume(); + resume.storeFactory(storeFactory); + return this; + } + + public ServerRSocketFactory resumeSessionDuration(Duration sessionDuration) { + resume(); + resume.sessionDuration(sessionDuration); + return this; + } + + public ServerRSocketFactory resumeStreamTimeout(Duration streamTimeout) { + resume(); + resume.streamTimeout(streamTimeout); + return this; + } + + public ServerRSocketFactory resumeCleanupOnKeepAlive() { + resume(); + resume.cleanupStoreOnKeepAlive(); + return this; + } + + @Override + public ServerTransport.ConnectionAcceptor toConnectionAcceptor() { + return server.asConnectionAcceptor(); + } + + @Override + public Start transport(Supplier> transport) { + return () -> server.bind(transport.get()); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java deleted file mode 100644 index 880a8fd9d..000000000 --- a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.util.collection.IntObjectHashMap; -import io.rsocket.Frame.Request; -import io.rsocket.exceptions.ApplicationException; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.internal.LimitableRequestPublisher; -import io.rsocket.util.PayloadImpl; -import java.util.function.Consumer; -import javax.annotation.Nullable; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.Disposable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.UnicastProcessor; - -/** Server side RSocket. Receives {@link Frame}s from a {@link RSocketClient} */ -class RSocketServer implements RSocket { - - private final DuplexConnection connection; - private final RSocket requestHandler; - private final Consumer errorConsumer; - - private final IntObjectHashMap sendingSubscriptions; - private final IntObjectHashMap> channelProcessors; - - private Disposable receiveDisposable; - - RSocketServer( - DuplexConnection connection, RSocket requestHandler, Consumer errorConsumer) { - this.connection = connection; - this.requestHandler = requestHandler; - this.errorConsumer = errorConsumer; - this.sendingSubscriptions = new IntObjectHashMap<>(); - this.channelProcessors = new IntObjectHashMap<>(); - this.receiveDisposable = - connection - .receive() - .flatMap(this::handleFrame) - .doOnError(errorConsumer::accept) - .then() - .subscribe(); - - this.connection - .onClose() - .doOnError(errorConsumer::accept) - .doFinally( - s -> { - cleanup(); - receiveDisposable.dispose(); - }) - .subscribe(); - } - - @Override - public Mono fireAndForget(Payload payload) { - try { - return requestHandler.fireAndForget(payload); - } catch (Throwable t) { - return Mono.error(t); - } - } - - @Override - public Mono requestResponse(Payload payload) { - try { - return requestHandler.requestResponse(payload); - } catch (Throwable t) { - return Mono.error(t); - } - } - - @Override - public Flux requestStream(Payload payload) { - try { - return requestHandler.requestStream(payload); - } catch (Throwable t) { - return Flux.error(t); - } - } - - @Override - public Flux requestChannel(Publisher payloads) { - try { - return requestHandler.requestChannel(payloads); - } catch (Throwable t) { - return Flux.error(t); - } - } - - @Override - public Mono metadataPush(Payload payload) { - try { - return requestHandler.metadataPush(payload); - } catch (Throwable t) { - return Mono.error(t); - } - } - - @Override - public Mono close() { - return connection.close(); - } - - @Override - public Mono onClose() { - return connection.onClose(); - } - - private void cleanup() { - cleanUpSendingSubscriptions(); - cleanUpChannelProcessors(); - - requestHandler.close().subscribe(); - } - - private synchronized void cleanUpSendingSubscriptions() { - sendingSubscriptions.values().forEach(Subscription::cancel); - sendingSubscriptions.clear(); - } - - private synchronized void cleanUpChannelProcessors() { - channelProcessors.values().forEach(Subscription::cancel); - channelProcessors.clear(); - } - - private Mono handleFrame(Frame frame) { - try { - int streamId = frame.getStreamId(); - Subscriber receiver; - switch (frame.getType()) { - case FIRE_AND_FORGET: - return handleFireAndForget(streamId, fireAndForget(new PayloadImpl(frame))); - case REQUEST_RESPONSE: - return handleRequestResponse(streamId, requestResponse(new PayloadImpl(frame))); - case CANCEL: - return handleCancelFrame(streamId); - case KEEPALIVE: - return handleKeepAliveFrame(frame); - case REQUEST_N: - return handleRequestN(streamId, frame); - case REQUEST_STREAM: - return handleStream(streamId, requestStream(new PayloadImpl(frame)), frame); - case REQUEST_CHANNEL: - return handleChannel(streamId, frame); - case PAYLOAD: - // TODO: Hook in receiving socket. - return Mono.empty(); - case METADATA_PUSH: - return metadataPush(new PayloadImpl(frame)); - case LEASE: - // Lease must not be received here as this is the server end of the socket which sends leases. - return Mono.empty(); - case NEXT: - receiver = getChannelProcessor(streamId); - if (receiver != null) { - receiver.onNext(new PayloadImpl(frame)); - } - return Mono.empty(); - case COMPLETE: - receiver = getChannelProcessor(streamId); - if (receiver != null) { - receiver.onComplete(); - } - return Mono.empty(); - case ERROR: - receiver = getChannelProcessor(streamId); - if (receiver != null) { - receiver.onError(new ApplicationException(new PayloadImpl(frame))); - } - return Mono.empty(); - case NEXT_COMPLETE: - receiver = getChannelProcessor(streamId); - if (receiver != null) { - receiver.onNext(new PayloadImpl(frame)); - receiver.onComplete(); - } - - return Mono.empty(); - - case SETUP: - return handleError( - streamId, new IllegalStateException("Setup frame received post setup.")); - default: - return handleError( - streamId, - new IllegalStateException( - "ServerRSocket: Unexpected frame type: " + frame.getType())); - } - } finally { - frame.release(); - } - } - - private Mono handleFireAndForget(int streamId, Mono result) { - return result - .doOnSubscribe(subscription -> addSubscription(streamId, subscription)) - .doOnError(errorConsumer) - .doFinally(signalType -> removeSubscription(streamId)) - .ignoreElement(); - } - - private Mono handleRequestResponse(int streamId, Mono response) { - Mono responseFrame = - response - .doOnSubscribe(subscription -> addSubscription(streamId, subscription)) - .map( - payload -> - Frame.PayloadFrame.from( - streamId, FrameType.NEXT_COMPLETE, payload, FrameHeaderFlyweight.FLAGS_C)) - .onErrorResume(t -> Mono.just(Frame.Error.from(streamId, t))) - .doFinally( - signalType -> { - removeSubscription(streamId); - }); - - return responseFrame.flatMap(connection::sendOne); - } - - private Mono handleStream(int streamId, Flux response, Frame firstFrame) { - int initialRequestN = Request.initialRequestN(firstFrame); - Flux responseFrames = - response - .map(payload -> Frame.PayloadFrame.from(streamId, FrameType.NEXT, payload)) - .transform( - frameFlux -> { - LimitableRequestPublisher frames = - LimitableRequestPublisher.wrap(frameFlux); - synchronized (RSocketServer.this) { - sendingSubscriptions.put(streamId, frames); - } - frames.increaseRequestLimit(initialRequestN); - return frames; - }) - .concatWith(Mono.just(Frame.PayloadFrame.from(streamId, FrameType.COMPLETE))) - .onErrorResume(t -> Mono.just(Frame.Error.from(streamId, t))) - .doFinally( - signalType -> { - removeSubscription(streamId); - }); - - return connection.send(responseFrames); - } - - private Mono handleChannel(int streamId, Frame firstFrame) { - UnicastProcessor frames = UnicastProcessor.create(); - addChannelProcessor(streamId, frames); - - Flux payloads = - frames - .doOnCancel( - () -> { - if (connection.availability() > 0.0) { - connection.sendOne(Frame.Cancel.from(streamId)).subscribe(null, errorConsumer); - } - }) - .doOnError( - t -> { - if (connection.availability() > 0.0) { - connection - .sendOne(Frame.Error.from(streamId, t)) - .subscribe(null, errorConsumer); - } - }) - .doOnRequest( - l -> { - if (connection.availability() > 0.0) { - connection - .sendOne(Frame.RequestN.from(streamId, l)) - .subscribe(null, errorConsumer); - } - }) - .doFinally( - signalType -> { - removeChannelProcessor(streamId); - }); - - return handleStream(streamId, requestChannel(payloads), firstFrame); - } - - private Mono handleKeepAliveFrame(Frame frame) { - if (Frame.Keepalive.hasRespondFlag(frame)) { - ByteBuf data = Unpooled.wrappedBuffer(frame.getData()); - return connection.sendOne(Frame.Keepalive.from(data, false)).doOnError(errorConsumer); - } - return Mono.empty(); - } - - private Mono handleCancelFrame(int streamId) { - Subscription subscription; - synchronized (this) { - subscription = sendingSubscriptions.remove(streamId); - } - - if (subscription != null) { - subscription.cancel(); - } - - return Mono.empty(); - } - - private Mono handleError(int streamId, Throwable t) { - errorConsumer.accept(t); - return connection.sendOne(Frame.Error.from(streamId, t)).doOnError(errorConsumer); - } - - private Mono handleRequestN(int streamId, Frame frame) { - final Subscription subscription = getSubscription(streamId); - if (subscription != null) { - int n = Frame.RequestN.requestN(frame); - subscription.request(n >= Integer.MAX_VALUE ? Long.MAX_VALUE : n); - } - return Mono.empty(); - } - - private synchronized void addSubscription(int streamId, Subscription subscription) { - sendingSubscriptions.put(streamId, subscription); - } - - private synchronized @Nullable Subscription getSubscription(int streamId) { - return sendingSubscriptions.get(streamId); - } - - private synchronized void removeSubscription(int streamId) { - sendingSubscriptions.remove(streamId); - } - - private synchronized void addChannelProcessor(int streamId, UnicastProcessor processor) { - channelProcessors.put(streamId, processor); - } - - private synchronized @Nullable UnicastProcessor getChannelProcessor(int streamId) { - return channelProcessors.get(streamId); - } - - private synchronized void removeChannelProcessor(int streamId) { - channelProcessors.remove(streamId); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/ResponderRSocket.java b/rsocket-core/src/main/java/io/rsocket/ResponderRSocket.java new file mode 100644 index 000000000..22697f130 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/ResponderRSocket.java @@ -0,0 +1,28 @@ +package io.rsocket; + +import java.util.function.BiFunction; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +/** + * Extends the {@link RSocket} that allows an implementer to peek at the first request payload of a + * channel. + * + * @deprecated as of 1.0 RC7 in favor of using {@link RSocket#requestChannel(Publisher)} with {@link + * Flux#switchOnFirst(BiFunction)} + */ +@Deprecated +public interface ResponderRSocket extends RSocket { + /** + * Implement this method to peak at the first payload of the incoming request stream without + * having to subscribe to Publish<Payload> payloads + * + * @param payload First payload in the stream - this is the same payload as the first payload in + * Publisher<Payload> payloads + * @param payloads Stream of request payloads. + * @return Stream of response payloads. + */ + default Flux requestChannel(Payload payload, Publisher payloads) { + return requestChannel(payloads); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/SocketAcceptor.java b/rsocket-core/src/main/java/io/rsocket/SocketAcceptor.java index b54ff8601..a42626e78 100644 --- a/rsocket-core/src/main/java/io/rsocket/SocketAcceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/SocketAcceptor.java @@ -1,40 +1,93 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket; import io.rsocket.exceptions.SetupException; +import java.util.function.Function; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** - * {@code RSocket} is a full duplex protocol where a client and server are identical in terms of - * both having the capability to initiate requests to their peer. This interface provides the - * contract where a server accepts a new {@code RSocket} for sending requests to the peer and - * returns a new {@code RSocket} that will be used to accept requests from it's peer. + * RSocket is a full duplex protocol where a client and server are identical in terms of both having + * the capability to initiate requests to their peer. This interface provides the contract where a + * client or server handles the {@code setup} for a new connection and creates a responder {@code + * RSocket} for accepting requests from the remote peer. */ public interface SocketAcceptor { /** - * Accepts a new {@code RSocket} used to send requests to the peer and returns another {@code - * RSocket} that is used for accepting requests from the peer. + * Handle the {@code SETUP} frame for a new connection and create a responder {@code RSocket} for + * handling requests from the remote peer. * - * @param setup Setup as sent by the client. - * @param sendingSocket Socket used to send requests to the peer. - * @return Socket to accept requests from the peer. + * @param setup the {@code setup} received from a client in a server scenario, or in a client + * scenario this is the setup about to be sent to the server. + * @param sendingSocket socket for sending requests to the remote peer. + * @return {@code RSocket} to accept requests with. * @throws SetupException If the acceptor needs to reject the setup of this socket. */ Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket); + + /** Create a {@code SocketAcceptor} that handles requests with the given {@code RSocket}. */ + static SocketAcceptor with(RSocket rsocket) { + return (setup, sendingRSocket) -> Mono.just(rsocket); + } + + /** Create a {@code SocketAcceptor} for fire-and-forget interactions with the given handler. */ + static SocketAcceptor forFireAndForget(Function> handler) { + return with( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + return handler.apply(payload); + } + }); + } + + /** Create a {@code SocketAcceptor} for request-response interactions with the given handler. */ + static SocketAcceptor forRequestResponse(Function> handler) { + return with( + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + return handler.apply(payload); + } + }); + } + + /** Create a {@code SocketAcceptor} for request-stream interactions with the given handler. */ + static SocketAcceptor forRequestStream(Function> handler) { + return with( + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + return handler.apply(payload); + } + }); + } + + /** Create a {@code SocketAcceptor} for request-channel interactions with the given handler. */ + static SocketAcceptor forRequestChannel(Function, Flux> handler) { + return with( + new RSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + return handler.apply(payloads); + } + }); + } } diff --git a/rsocket-core/src/main/java/io/rsocket/StreamIdSupplier.java b/rsocket-core/src/main/java/io/rsocket/StreamIdSupplier.java deleted file mode 100644 index 07607ce6e..000000000 --- a/rsocket-core/src/main/java/io/rsocket/StreamIdSupplier.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -final class StreamIdSupplier { - - private int streamId; - - private StreamIdSupplier(int streamId) { - this.streamId = streamId; - } - - synchronized int nextStreamId() { - streamId += 2; - return streamId; - } - - synchronized boolean isBeforeOrCurrent(int streamId) { - return this.streamId >= streamId && streamId > 0; - } - - static StreamIdSupplier clientSupplier() { - return new StreamIdSupplier(-1); - } - - static StreamIdSupplier serverSupplier() { - return new StreamIdSupplier(0); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/client/package-info.java b/rsocket-core/src/main/java/io/rsocket/client/package-info.java deleted file mode 100644 index 97d283054..000000000 --- a/rsocket-core/src/main/java/io/rsocket/client/package-info.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@javax.annotation.ParametersAreNonnullByDefault -package io.rsocket.client; diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java new file mode 100644 index 000000000..9b5647c6f --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java @@ -0,0 +1,119 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.SetupFrameCodec; + +/** + * Default implementation of {@link ConnectionSetupPayload}. Primarily for internal use within + * RSocket Java but may be created in an application, e.g. for testing purposes. + */ +public class DefaultConnectionSetupPayload extends ConnectionSetupPayload { + + private final ByteBuf setupFrame; + + public DefaultConnectionSetupPayload(ByteBuf setupFrame) { + this.setupFrame = setupFrame; + } + + @Override + public boolean hasMetadata() { + return FrameHeaderCodec.hasMetadata(setupFrame); + } + + @Override + public ByteBuf sliceMetadata() { + final ByteBuf metadata = SetupFrameCodec.metadata(setupFrame); + return metadata == null ? Unpooled.EMPTY_BUFFER : metadata; + } + + @Override + public ByteBuf sliceData() { + return SetupFrameCodec.data(setupFrame); + } + + @Override + public ByteBuf data() { + return sliceData(); + } + + @Override + public ByteBuf metadata() { + return sliceMetadata(); + } + + @Override + public String metadataMimeType() { + return SetupFrameCodec.metadataMimeType(setupFrame); + } + + @Override + public String dataMimeType() { + return SetupFrameCodec.dataMimeType(setupFrame); + } + + @Override + public int keepAliveInterval() { + return SetupFrameCodec.keepAliveInterval(setupFrame); + } + + @Override + public int keepAliveMaxLifetime() { + return SetupFrameCodec.keepAliveMaxLifetime(setupFrame); + } + + @Override + public int getFlags() { + return FrameHeaderCodec.flags(setupFrame); + } + + @Override + public boolean willClientHonorLease() { + return SetupFrameCodec.honorLease(setupFrame); + } + + @Override + public boolean isResumeEnabled() { + return SetupFrameCodec.resumeEnabled(setupFrame); + } + + @Override + public ByteBuf resumeToken() { + return SetupFrameCodec.resumeToken(setupFrame); + } + + @Override + public ConnectionSetupPayload touch() { + setupFrame.touch(); + return this; + } + + @Override + public ConnectionSetupPayload touch(Object hint) { + setupFrame.touch(hint); + return this; + } + + @Override + protected void deallocate() { + setupFrame.release(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/PayloadValidationUtils.java b/rsocket-core/src/main/java/io/rsocket/core/PayloadValidationUtils.java new file mode 100644 index 000000000..5e62105c9 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/PayloadValidationUtils.java @@ -0,0 +1,61 @@ +package io.rsocket.core; + +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + +import io.rsocket.Payload; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameLengthCodec; + +final class PayloadValidationUtils { + static final String INVALID_PAYLOAD_ERROR_MESSAGE = + "The payload is too big to send as a single frame with a 24-bit encoded length. Consider enabling fragmentation via RSocketFactory."; + + static boolean isValid(int mtu, Payload payload, int maxFrameLength) { + if (mtu > 0) { + return true; + } + + if (payload.hasMetadata()) { + return ((FrameHeaderCodec.size() + + FrameLengthCodec.FRAME_LENGTH_SIZE + + FrameHeaderCodec.size() + + payload.data().readableBytes() + + payload.metadata().readableBytes()) + <= maxFrameLength); + } else { + return ((FrameHeaderCodec.size() + + payload.data().readableBytes() + + FrameLengthCodec.FRAME_LENGTH_SIZE) + <= maxFrameLength); + } + } + + static void assertValidateSetup(int maxFrameLength, int maxInboundPayloadSize, int mtu) { + + if (maxFrameLength > FRAME_LENGTH_MASK) { + throw new IllegalArgumentException( + "Configured maxFrameLength[" + + maxFrameLength + + "] exceeds maxFrameLength limit " + + FRAME_LENGTH_MASK); + } + + if (maxFrameLength > maxInboundPayloadSize) { + throw new IllegalArgumentException( + "Configured maxFrameLength[" + + maxFrameLength + + "] exceeds maxPayloadSize[" + + maxInboundPayloadSize + + "]"); + } + + if (mtu != 0 && mtu > maxFrameLength) { + throw new IllegalArgumentException( + "Configured maximumTransmissionUnit[" + + mtu + + "] exceeds configured maxFrameLength[" + + maxFrameLength + + "]"); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java new file mode 100644 index 000000000..eab70cc30 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -0,0 +1,662 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.core; + +import static io.rsocket.core.PayloadValidationUtils.assertValidateSetup; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.DuplexConnection; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.fragmentation.FragmentationDuplexConnection; +import io.rsocket.fragmentation.ReassemblyDuplexConnection; +import io.rsocket.frame.SetupFrameCodec; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.ClientServerInputMultiplexer; +import io.rsocket.keepalive.KeepAliveHandler; +import io.rsocket.lease.LeaseStats; +import io.rsocket.lease.Leases; +import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.plugins.InitializingInterceptorRegistry; +import io.rsocket.plugins.InterceptorRegistry; +import io.rsocket.resume.ClientRSocketSession; +import io.rsocket.transport.ClientTransport; +import io.rsocket.util.DefaultPayload; +import io.rsocket.util.EmptyPayload; +import java.time.Duration; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; +import reactor.core.Disposable; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.util.annotation.Nullable; +import reactor.util.function.Tuples; +import reactor.util.retry.Retry; + +/** + * The main class to use to establish a connection to an RSocket server. + * + *

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

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

To customize connection settings before connecting: + * + *

{@code
+ * Mono rocketMono =
+ *         RSocketConnector.create()
+ *                 .metadataMimeType("message/x.rsocket.composite-metadata.v0")
+ *                 .dataMimeType("application/cbor")
+ *                 .connect(TcpClientTransport.create("localhost", 7000));
+ * }
+ */ +public class RSocketConnector { + private static final String CLIENT_TAG = "client"; + + private static final BiConsumer INVALIDATE_FUNCTION = + (r, i) -> r.onClose().subscribe(null, __ -> i.invalidate(), i::invalidate); + + private Mono setupPayloadMono = Mono.empty(); + private String metadataMimeType = "application/binary"; + private String dataMimeType = "application/binary"; + private Duration keepAliveInterval = Duration.ofSeconds(20); + private Duration keepAliveMaxLifeTime = Duration.ofSeconds(90); + + @Nullable private SocketAcceptor acceptor; + private InitializingInterceptorRegistry interceptors = new InitializingInterceptorRegistry(); + + private Retry retrySpec; + private Resume resume; + private Supplier> leasesSupplier; + + private int mtu = 0; + private int maxInboundPayloadSize = Integer.MAX_VALUE; + private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; + + private RSocketConnector() {} + + /** + * Static factory method to create an {@code RSocketConnector} instance and customize default + * settings before connecting. To connect only, use {@link #connectWith(ClientTransport)}. + */ + public static RSocketConnector create() { + return new RSocketConnector(); + } + + /** + * Static factory method to connect with default settings, effectively a shortcut for: + * + *
+   * RSocketConnector.create().connectWith(transport);
+   * 
+ * + * @param transport the transport of choice to connect with + * @return a {@code Mono} with the connected RSocket + */ + public static Mono connectWith(ClientTransport transport) { + return RSocketConnector.create().connect(() -> transport); + } + + /** + * Provide a {@code Mono} from which to obtain the {@code Payload} for the initial SETUP frame. + * Data and metadata should be formatted according to the MIME types specified via {@link + * #dataMimeType(String)} and {@link #metadataMimeType(String)}. + * + * @param setupPayloadMono the payload with data and/or metadata for the {@code SETUP} frame. + * @return the same instance for method chaining + * @since 1.0.2 + * @see
SETUP + * Frame + */ + public RSocketConnector setupPayload(Mono setupPayloadMono) { + this.setupPayloadMono = setupPayloadMono; + return this; + } + + /** + * Variant of {@link #setupPayload(Mono)} that accepts a {@code Payload} instance. + * + *

Note: if the given payload is {@link io.rsocket.util.ByteBufPayload}, it is copied to a + * {@link DefaultPayload} and released immediately. This ensures it can re-used to obtain a + * connection more than once. + * + * @param payload the payload with data and/or metadata for the {@code SETUP} frame. + * @return the same instance for method chaining + * @see SETUP + * Frame + */ + public RSocketConnector setupPayload(Payload payload) { + if (payload instanceof DefaultPayload) { + this.setupPayloadMono = Mono.just(payload); + } else { + this.setupPayloadMono = Mono.just(DefaultPayload.create(Objects.requireNonNull(payload))); + payload.release(); + } + return this; + } + + /** + * Set the MIME type to use for formatting payload data on the established connection. This is set + * in the initial {@code SETUP} frame sent to the server. + * + *

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

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

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

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

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

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

By default these are set to 20 seconds and 90 seconds respectively. + * + * @param interval how frequently to emit KEEPALIVE frames + * @param maxLifeTime how long to allow between {@code KEEPALIVE} frames from the remote end + * before assuming that connectivity is lost; the value should be generous and allow for + * multiple missed {@code KEEPALIVE} frames. + * @return the same instance for method chaining + * @see SETUP + * Frame + */ + public RSocketConnector keepAlive(Duration interval, Duration maxLifeTime) { + if (!interval.negated().isNegative()) { + throw new IllegalArgumentException("`interval` for keepAlive must be > 0"); + } + if (!maxLifeTime.negated().isNegative()) { + throw new IllegalArgumentException("`maxLifeTime` for keepAlive must be > 0"); + } + this.keepAliveInterval = interval; + this.keepAliveMaxLifeTime = maxLifeTime; + return this; + } + + /** + * Configure interception at one of the following levels: + * + *

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

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

{@code
+   * Mono rsocketMono =
+   *     RSocketConnector.create()
+   *             .acceptor((setup, sendingRSocket) -> Mono.just(new RSocket() {...}))
+   *             .connect(transport);
+   * }
+ * + *

A shortcut example with just the handling RSocket: + * + *

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

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

{@code
+   * Mono rsocketMono =
+   *     RSocketConnector.create()
+   *             .acceptor(SocketAcceptor.forRequestResponse(payload -> ...))
+   *             .connect(transport);
+   * }
+ * + *

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

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

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

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

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

{@code
+   * Mono rocketMono =
+   *   RSocketConnector.create()
+   *           .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
+   *           .connect(transport);
+   *
+   *  rsocketMono.flatMap(rsocket -> rsocket.requestResponse(...))
+   *           .retryWhen(Retry.fixedDelay(1, Duration.ofSeconds(5)))
+   *           .subscribe()
+   * }
+ * + *

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

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

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

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

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

Example usage: + * + *

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

By default this is not enabled. + * + * @param supplier supplier for a {@link Leases} + * @return the same instance for method chaining + * @see Lease + * Semantics + */ + public RSocketConnector lease(Supplier> supplier) { + this.leasesSupplier = supplier; + return this; + } + + /** + * When this is set, frames reassembler control maximum payload size which can be reassembled. + * + *

By default this is not set in which case maximum reassembled payloads size is not + * controlled. + * + * @param maxInboundPayloadSize the threshold size for reassembly, must no be less than 64 bytes. + * Please note, {@code maxInboundPayloadSize} must always be greater or equal to {@link + * io.rsocket.transport.Transport#maxFrameLength()}, otherwise inbound frame can exceed the + * {@code maxInboundPayloadSize} + * @return the same instance for method chaining + * @see Fragmentation + * and Reassembly + */ + public RSocketConnector maxInboundPayloadSize(int maxInboundPayloadSize) { + this.maxInboundPayloadSize = + ReassemblyDuplexConnection.assertInboundPayloadSize(maxInboundPayloadSize); + return this; + } + + /** + * When this is set, frames larger than the given maximum transmission unit (mtu) size value are + * broken down into fragments to fit that size. + * + *

By default this is not set in which case payloads are sent whole up to the maximum frame + * size of 16,777,215 bytes. + * + * @param mtu the threshold size for fragmentation, must be no less than 64 + * @return the same instance for method chaining + * @see Fragmentation + * and Reassembly + */ + public RSocketConnector fragment(int mtu) { + this.mtu = FragmentationDuplexConnection.assertMtu(mtu); + return this; + } + + /** + * Configure the {@code PayloadDecoder} used to create {@link Payload}'s from incoming raw frame + * buffers. The following decoders are available: + * + *

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

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

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

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

// TODO: when to use? + * + * @param transportSupplier supplier for the transport to connect with + * @return a {@code Mono} with the connected RSocket + */ + public Mono connect(Supplier transportSupplier) { + + return Mono.fromSupplier(transportSupplier) + .flatMap( + ct -> { + int maxFrameLength = ct.maxFrameLength(); + + Mono connectionMono = + Mono.fromCallable( + () -> { + assertValidateSetup(maxFrameLength, maxInboundPayloadSize, mtu); + return ct; + }) + .flatMap(transport -> transport.connect()) + .map( + connection -> + mtu > 0 + ? new FragmentationDuplexConnection( + connection, mtu, maxInboundPayloadSize, "client") + : new ReassemblyDuplexConnection( + connection, maxInboundPayloadSize)); + + return connectionMono + .flatMap( + connection -> + setupPayloadMono + .defaultIfEmpty(EmptyPayload.INSTANCE) + .map(setupPayload -> Tuples.of(connection, setupPayload)) + .doOnError(ex -> connection.dispose()) + .doOnCancel(connection::dispose)) + .flatMap( + tuple -> { + DuplexConnection connection = tuple.getT1(); + Payload setupPayload = tuple.getT2(); + ByteBuf resumeToken; + KeepAliveHandler keepAliveHandler; + DuplexConnection wrappedConnection; + + if (resume != null) { + resumeToken = resume.getTokenSupplier().get(); + ClientRSocketSession session = + new ClientRSocketSession( + connection, + resume.getSessionDuration(), + resume.getRetry(), + resume.getStoreFactory(CLIENT_TAG).apply(resumeToken), + resume.getStreamTimeout(), + resume.isCleanupStoreOnKeepAlive()) + .continueWith(connectionMono) + .resumeToken(resumeToken); + keepAliveHandler = + new KeepAliveHandler.ResumableKeepAliveHandler( + session.resumableConnection()); + wrappedConnection = session.resumableConnection(); + } else { + resumeToken = Unpooled.EMPTY_BUFFER; + keepAliveHandler = + new KeepAliveHandler.DefaultKeepAliveHandler(connection); + wrappedConnection = connection; + } + + ClientServerInputMultiplexer multiplexer = + new ClientServerInputMultiplexer(wrappedConnection, interceptors, true); + + boolean leaseEnabled = leasesSupplier != null; + Leases leases = leaseEnabled ? leasesSupplier.get() : null; + RequesterLeaseHandler requesterLeaseHandler = + leaseEnabled + ? new RequesterLeaseHandler.Impl(CLIENT_TAG, leases.receiver()) + : RequesterLeaseHandler.None; + + RSocket rSocketRequester = + new RSocketRequester( + multiplexer.asClientConnection(), + payloadDecoder, + StreamIdSupplier.clientSupplier(), + mtu, + maxFrameLength, + (int) keepAliveInterval.toMillis(), + (int) keepAliveMaxLifeTime.toMillis(), + keepAliveHandler, + requesterLeaseHandler, + Schedulers.single(Schedulers.parallel())); + + RSocket wrappedRSocketRequester = + interceptors.initRequester(rSocketRequester); + + ByteBuf setupFrame = + SetupFrameCodec.encode( + wrappedConnection.alloc(), + leaseEnabled, + (int) keepAliveInterval.toMillis(), + (int) keepAliveMaxLifeTime.toMillis(), + resumeToken, + metadataMimeType, + dataMimeType, + setupPayload); + + SocketAcceptor acceptor = + this.acceptor != null + ? this.acceptor + : SocketAcceptor.with(new RSocket() {}); + + ConnectionSetupPayload setup = + new DefaultConnectionSetupPayload(setupFrame); + + return interceptors + .initSocketAcceptor(acceptor) + .accept(setup, wrappedRSocketRequester) + .flatMap( + rSocketHandler -> { + RSocket wrappedRSocketHandler = + interceptors.initResponder(rSocketHandler); + + ResponderLeaseHandler responderLeaseHandler = + leaseEnabled + ? new ResponderLeaseHandler.Impl<>( + CLIENT_TAG, + wrappedConnection.alloc(), + leases.sender(), + leases.stats()) + : ResponderLeaseHandler.None; + + RSocket rSocketResponder = + new RSocketResponder( + multiplexer.asServerConnection(), + wrappedRSocketHandler, + payloadDecoder, + responderLeaseHandler, + mtu, + maxFrameLength); + + return wrappedConnection + .sendOne(setupFrame.retain()) + .thenReturn(wrappedRSocketRequester); + }) + .doFinally(signalType -> setup.release()); + }); + }) + .as( + source -> { + if (retrySpec != null) { + return new ReconnectMono<>( + source.retryWhen(retrySpec), Disposable::dispose, INVALIDATE_FUNCTION); + } else { + return source; + } + }); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java new file mode 100644 index 000000000..d1a37e7e8 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -0,0 +1,817 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; +import static io.rsocket.keepalive.KeepAliveSupport.ClientKeepAliveSupport; +import static io.rsocket.keepalive.KeepAliveSupport.KeepAlive; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.IllegalReferenceCountException; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; +import io.netty.util.collection.IntObjectMap; +import io.rsocket.DuplexConnection; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.exceptions.ConnectionErrorException; +import io.rsocket.exceptions.Exceptions; +import io.rsocket.frame.CancelFrameCodec; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.MetadataPushFrameCodec; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; +import io.rsocket.frame.RequestNFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.SynchronizedIntObjectHashMap; +import io.rsocket.internal.UnboundedProcessor; +import io.rsocket.keepalive.KeepAliveFramesAcceptor; +import io.rsocket.keepalive.KeepAliveHandler; +import io.rsocket.keepalive.KeepAliveSupport; +import io.rsocket.lease.RequesterLeaseHandler; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.Consumer; +import java.util.function.Supplier; +import org.reactivestreams.Processor; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.SignalType; +import reactor.core.publisher.UnicastProcessor; +import reactor.core.scheduler.Scheduler; +import reactor.util.annotation.Nullable; +import reactor.util.concurrent.Queues; + +/** + * Requester Side of a RSocket socket. Sends {@link ByteBuf}s to a {@link RSocketResponder} of peer + */ +class RSocketRequester implements RSocket { + private static final Logger LOGGER = LoggerFactory.getLogger(RSocketRequester.class); + + private static final Exception CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException(); + private static final Consumer DROPPED_ELEMENTS_CONSUMER = + referenceCounted -> { + if (referenceCounted.refCnt() > 0) { + try { + referenceCounted.release(); + } catch (IllegalReferenceCountException e) { + // ignored + } + } + }; + + static { + CLOSED_CHANNEL_EXCEPTION.setStackTrace(new StackTraceElement[0]); + } + + private volatile Throwable terminationError; + + private static final AtomicReferenceFieldUpdater TERMINATION_ERROR = + AtomicReferenceFieldUpdater.newUpdater( + RSocketRequester.class, Throwable.class, "terminationError"); + + private final DuplexConnection connection; + private final PayloadDecoder payloadDecoder; + private final StreamIdSupplier streamIdSupplier; + private final IntObjectMap senders; + private final IntObjectMap> receivers; + private final UnboundedProcessor sendProcessor; + private final int mtu; + private final int maxFrameLength; + private final RequesterLeaseHandler leaseHandler; + private final ByteBufAllocator allocator; + private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; + private final MonoProcessor onClose; + private final Scheduler serialScheduler; + + RSocketRequester( + DuplexConnection connection, + PayloadDecoder payloadDecoder, + StreamIdSupplier streamIdSupplier, + int mtu, + int maxFrameLength, + int keepAliveTickPeriod, + int keepAliveAckTimeout, + @Nullable KeepAliveHandler keepAliveHandler, + RequesterLeaseHandler leaseHandler, + Scheduler serialScheduler) { + this.connection = connection; + this.allocator = connection.alloc(); + this.payloadDecoder = payloadDecoder; + this.streamIdSupplier = streamIdSupplier; + this.mtu = mtu; + this.maxFrameLength = maxFrameLength; + this.leaseHandler = leaseHandler; + this.senders = new SynchronizedIntObjectHashMap<>(); + this.receivers = new SynchronizedIntObjectHashMap<>(); + this.onClose = MonoProcessor.create(); + this.serialScheduler = serialScheduler; + + // DO NOT Change the order here. The Send processor must be subscribed to before receiving + this.sendProcessor = new UnboundedProcessor<>(); + + connection.onClose().subscribe(null, this::tryTerminateOnConnectionError, this::tryShutdown); + connection.send(sendProcessor).subscribe(null, this::handleSendProcessorError); + + connection.receive().subscribe(this::handleIncomingFrames, e -> {}); + + if (keepAliveTickPeriod != 0 && keepAliveHandler != null) { + KeepAliveSupport keepAliveSupport = + new ClientKeepAliveSupport(this.allocator, keepAliveTickPeriod, keepAliveAckTimeout); + this.keepAliveFramesAcceptor = + keepAliveHandler.start( + keepAliveSupport, sendProcessor::onNextPrioritized, this::tryTerminateOnKeepAlive); + } else { + keepAliveFramesAcceptor = null; + } + } + + @Override + public Mono fireAndForget(Payload payload) { + return handleFireAndForget(payload); + } + + @Override + public Mono requestResponse(Payload payload) { + return handleRequestResponse(payload); + } + + @Override + public Flux requestStream(Payload payload) { + return handleRequestStream(payload); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return handleChannel(Flux.from(payloads)); + } + + @Override + public Mono metadataPush(Payload payload) { + return handleMetadataPush(payload); + } + + @Override + public double availability() { + return Math.min(connection.availability(), leaseHandler.availability()); + } + + @Override + public void dispose() { + tryShutdown(); + } + + @Override + public boolean isDisposed() { + return terminationError != null; + } + + @Override + public Mono onClose() { + return onClose; + } + + private Mono handleFireAndForget(Payload payload) { + if (payload.refCnt() <= 0) { + return Mono.error(new IllegalReferenceCountException()); + } + + if (isDisposed()) { + payload.release(); + final Throwable t = terminationError; + return Mono.error(t); + } + + if (!PayloadValidationUtils.isValid(this.mtu, payload, maxFrameLength)) { + payload.release(); + return Mono.error(new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE)); + } + + final AtomicBoolean once = new AtomicBoolean(); + + return Mono.defer( + () -> { + if (once.getAndSet(true)) { + return Mono.error( + new IllegalStateException("FireAndForgetMono allows only a single subscriber")); + } + + if (isDisposed()) { + payload.release(); + final Throwable t = terminationError; + return Mono.error(t); + } + + RequesterLeaseHandler lh = leaseHandler; + if (!lh.useLease()) { + payload.release(); + return Mono.error(lh.leaseError()); + } + + final int streamId = streamIdSupplier.nextStreamId(receivers); + final ByteBuf requestFrame = + RequestFireAndForgetFrameCodec.encodeReleasingPayload( + allocator, streamId, payload); + + sendProcessor.onNext(requestFrame); + + return Mono.empty(); + }) + .subscribeOn(serialScheduler); + } + + private Mono handleRequestResponse(final Payload payload) { + if (payload.refCnt() <= 0) { + return Mono.error(new IllegalReferenceCountException()); + } + + if (isDisposed()) { + payload.release(); + final Throwable t = terminationError; + return Mono.error(t); + } + + if (!PayloadValidationUtils.isValid(this.mtu, payload, maxFrameLength)) { + payload.release(); + return Mono.error(new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE)); + } + + final UnboundedProcessor sendProcessor = this.sendProcessor; + final UnicastProcessor receiver = UnicastProcessor.create(Queues.one().get()); + return Mono.fromDirect( + new RequestOperator( + receiver.next(), "RequestResponseMono allows only a single subscriber") { + + @Override + void hookOnFirstRequest(long n) { + if (isDisposed()) { + payload.release(); + final Throwable t = terminationError; + receiver.onError(t); + return; + } + + RequesterLeaseHandler lh = leaseHandler; + if (!lh.useLease()) { + payload.release(); + receiver.onError(lh.leaseError()); + return; + } + + int streamId = streamIdSupplier.nextStreamId(receivers); + this.streamId = streamId; + + ByteBuf requestResponseFrame = + RequestResponseFrameCodec.encodeReleasingPayload(allocator, streamId, payload); + + receivers.put(streamId, receiver); + sendProcessor.onNext(requestResponseFrame); + } + + @Override + void hookOnCancel() { + if (receivers.remove(streamId, receiver)) { + sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); + } else { + if (this.firstRequest) { + payload.release(); + } + } + } + + @Override + public void hookOnTerminal(SignalType signalType) { + receivers.remove(streamId, receiver); + } + }) + .subscribeOn(serialScheduler) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); + } + + private Flux handleRequestStream(final Payload payload) { + if (payload.refCnt() <= 0) { + return Flux.error(new IllegalReferenceCountException()); + } + + if (isDisposed()) { + payload.release(); + final Throwable t = terminationError; + return Flux.error(t); + } + + if (!PayloadValidationUtils.isValid(this.mtu, payload, maxFrameLength)) { + payload.release(); + return Flux.error(new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE)); + } + + final UnboundedProcessor sendProcessor = this.sendProcessor; + final UnicastProcessor receiver = UnicastProcessor.create(Queues.one().get()); + + return Flux.from( + new RequestOperator(receiver, "RequestStreamFlux allows only a single subscriber") { + + @Override + void hookOnFirstRequest(long n) { + if (isDisposed()) { + payload.release(); + final Throwable t = terminationError; + receiver.onError(t); + return; + } + + RequesterLeaseHandler lh = leaseHandler; + if (!lh.useLease()) { + payload.release(); + receiver.onError(lh.leaseError()); + return; + } + + int streamId = streamIdSupplier.nextStreamId(receivers); + this.streamId = streamId; + + ByteBuf requestStreamFrame = + RequestStreamFrameCodec.encodeReleasingPayload(allocator, streamId, n, payload); + + receivers.put(streamId, receiver); + + sendProcessor.onNext(requestStreamFrame); + } + + @Override + void hookOnRemainingRequests(long n) { + if (receiver.isDisposed()) { + return; + } + + sendProcessor.onNext(RequestNFrameCodec.encode(allocator, streamId, n)); + } + + @Override + void hookOnCancel() { + if (receivers.remove(streamId, receiver)) { + sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); + } else { + if (this.firstRequest) { + payload.release(); + } + } + } + + @Override + void hookOnTerminal(SignalType signalType) { + receivers.remove(streamId); + } + }) + .subscribeOn(serialScheduler, false) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); + } + + private Flux handleChannel(Flux request) { + if (isDisposed()) { + final Throwable t = terminationError; + return Flux.error(t); + } + + return request + .switchOnFirst( + (s, flux) -> { + Payload payload = s.get(); + if (payload != null) { + if (payload.refCnt() <= 0) { + return Mono.error(new IllegalReferenceCountException()); + } + + if (!PayloadValidationUtils.isValid(mtu, payload, maxFrameLength)) { + payload.release(); + final IllegalArgumentException t = + new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); + return Mono.error(t); + } + return handleChannel(payload, flux); + } else { + return flux; + } + }, + false) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); + } + + private Flux handleChannel(Payload initialPayload, Flux inboundFlux) { + final UnboundedProcessor sendProcessor = this.sendProcessor; + + final UnicastProcessor receiver = UnicastProcessor.create(Queues.one().get()); + + return Flux.from( + new RequestOperator( + receiver, "RequestStreamFlux allows only a " + "single subscriber") { + + final BaseSubscriber upstreamSubscriber = + new BaseSubscriber() { + + boolean first = true; + + @Override + protected void hookOnSubscribe(Subscription subscription) { + // noops + } + + @Override + protected void hookOnNext(Payload payload) { + if (first) { + // need to skip first since we have already sent it + // no need to release it since it was released earlier on the + // request + // establishment + // phase + first = false; + request(1); + return; + } + if (!PayloadValidationUtils.isValid(mtu, payload, maxFrameLength)) { + payload.release(); + cancel(); + final IllegalArgumentException t = + new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); + // no need to send any errors. + sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); + receiver.onError(t); + return; + } + final ByteBuf frame = + PayloadFrameCodec.encodeNextReleasingPayload( + allocator, streamId, payload); + + sendProcessor.onNext(frame); + } + + @Override + protected void hookOnComplete() { + ByteBuf frame = PayloadFrameCodec.encodeComplete(allocator, streamId); + sendProcessor.onNext(frame); + } + + @Override + protected void hookOnError(Throwable t) { + ByteBuf frame = ErrorFrameCodec.encode(allocator, streamId, t); + sendProcessor.onNext(frame); + receiver.onError(t); + } + + @Override + protected void hookFinally(SignalType type) { + senders.remove(streamId, this); + } + }; + + @Override + void hookOnFirstRequest(long n) { + if (isDisposed()) { + initialPayload.release(); + final Throwable t = terminationError; + upstreamSubscriber.cancel(); + receiver.onError(t); + return; + } + + RequesterLeaseHandler lh = leaseHandler; + if (!lh.useLease()) { + initialPayload.release(); + receiver.onError(lh.leaseError()); + return; + } + + final int streamId = streamIdSupplier.nextStreamId(receivers); + this.streamId = streamId; + + final ByteBuf frame = + RequestChannelFrameCodec.encodeReleasingPayload( + allocator, streamId, false, n, initialPayload); + + senders.put(streamId, upstreamSubscriber); + receivers.put(streamId, receiver); + + inboundFlux + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER) + .subscribe(upstreamSubscriber); + + sendProcessor.onNext(frame); + } + + @Override + void hookOnRemainingRequests(long n) { + if (receiver.isDisposed()) { + return; + } + + sendProcessor.onNext(RequestNFrameCodec.encode(allocator, streamId, n)); + } + + @Override + void hookOnCancel() { + senders.remove(streamId, upstreamSubscriber); + if (receivers.remove(streamId, receiver)) { + sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); + } + } + + @Override + void hookOnTerminal(SignalType signalType) { + if (signalType == SignalType.ON_ERROR) { + upstreamSubscriber.cancel(); + } + receivers.remove(streamId, receiver); + } + + @Override + public void cancel() { + upstreamSubscriber.cancel(); + super.cancel(); + } + }) + .subscribeOn(serialScheduler, false); + } + + private Mono handleMetadataPush(Payload payload) { + if (payload.refCnt() <= 0) { + return Mono.error(new IllegalReferenceCountException()); + } + + if (isDisposed()) { + Throwable err = this.terminationError; + payload.release(); + return Mono.error(err); + } + + if (!PayloadValidationUtils.isValid(this.mtu, payload, maxFrameLength)) { + payload.release(); + return Mono.error(new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE)); + } + + final AtomicBoolean once = new AtomicBoolean(); + + return Mono.defer( + () -> { + if (once.getAndSet(true)) { + return Mono.error( + new IllegalStateException("MetadataPushMono allows only a single subscriber")); + } + + if (isDisposed()) { + payload.release(); + final Throwable t = terminationError; + return Mono.error(t); + } + + ByteBuf metadataPushFrame = + MetadataPushFrameCodec.encodeReleasingPayload(allocator, payload); + + sendProcessor.onNextPrioritized(metadataPushFrame); + + return Mono.empty(); + }); + } + + private void handleIncomingFrames(ByteBuf frame) { + try { + int streamId = FrameHeaderCodec.streamId(frame); + FrameType type = FrameHeaderCodec.frameType(frame); + if (streamId == 0) { + handleStreamZero(type, frame); + } else { + handleFrame(streamId, type, frame); + } + frame.release(); + } catch (Throwable t) { + ReferenceCountUtil.safeRelease(frame); + throw reactor.core.Exceptions.propagate(t); + } + } + + private void handleStreamZero(FrameType type, ByteBuf frame) { + switch (type) { + case ERROR: + tryTerminateOnZeroError(frame); + break; + case LEASE: + leaseHandler.receive(frame); + break; + case KEEPALIVE: + if (keepAliveFramesAcceptor != null) { + keepAliveFramesAcceptor.receive(frame); + } + break; + default: + // Ignore unknown frames. Throwing an error will close the socket. + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Requester received unsupported frame on stream 0: " + frame.toString()); + } + } + } + + private void handleFrame(int streamId, FrameType type, ByteBuf frame) { + Subscriber receiver = receivers.get(streamId); + switch (type) { + case NEXT: + if (receiver == null) { + handleMissingResponseProcessor(streamId, type, frame); + return; + } + receiver.onNext(payloadDecoder.apply(frame)); + break; + case NEXT_COMPLETE: + if (receiver == null) { + handleMissingResponseProcessor(streamId, type, frame); + return; + } + receiver.onNext(payloadDecoder.apply(frame)); + receiver.onComplete(); + break; + case COMPLETE: + if (receiver == null) { + handleMissingResponseProcessor(streamId, type, frame); + return; + } + receiver.onComplete(); + receivers.remove(streamId); + break; + case ERROR: + if (receiver == null) { + handleMissingResponseProcessor(streamId, type, frame); + return; + } + + // FIXME: when https://github.com/reactor/reactor-core/issues/2176 is resolved + // This is workaround to handle specific Reactor related case when + // onError call may not return normally + try { + receiver.onError(Exceptions.from(streamId, frame)); + } catch (RuntimeException e) { + if (reactor.core.Exceptions.isBubbling(e) + || reactor.core.Exceptions.isErrorCallbackNotImplemented(e)) { + if (LOGGER.isDebugEnabled()) { + Throwable unwrapped = reactor.core.Exceptions.unwrap(e); + LOGGER.debug("Unhandled dropped exception", unwrapped); + } + } + } + + receivers.remove(streamId); + break; + case CANCEL: + { + Subscription sender = senders.remove(streamId); + if (sender != null) { + sender.cancel(); + } + break; + } + case REQUEST_N: + { + Subscription sender = senders.get(streamId); + if (sender != null) { + long n = RequestNFrameCodec.requestN(frame); + sender.request(n); + } + break; + } + default: + throw new IllegalStateException( + "Requester received unsupported frame on stream " + streamId + ": " + frame.toString()); + } + } + + private void handleMissingResponseProcessor(int streamId, FrameType type, ByteBuf frame) { + if (!streamIdSupplier.isBeforeOrCurrent(streamId)) { + if (type == FrameType.ERROR) { + // message for stream that has never existed, we have a problem with + // the overall connection and must tear down + String errorMessage = ErrorFrameCodec.dataUtf8(frame); + + throw new IllegalStateException( + "Client received error for non-existent stream: " + + streamId + + " Message: " + + errorMessage); + } else { + throw new IllegalStateException( + "Client received message for non-existent stream: " + + streamId + + ", frame type: " + + type); + } + } + // receiving a frame after a given stream has been cancelled/completed, + // so ignore (cancellation is async so there is a race condition) + } + + private void tryTerminateOnKeepAlive(KeepAlive keepAlive) { + tryTerminate( + () -> + new ConnectionErrorException( + String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()))); + } + + private void tryTerminateOnConnectionError(Throwable e) { + tryTerminate(() -> e); + } + + private void tryTerminateOnZeroError(ByteBuf errorFrame) { + tryTerminate(() -> Exceptions.from(0, errorFrame)); + } + + private void tryTerminate(Supplier errorSupplier) { + if (terminationError == null) { + Throwable e = errorSupplier.get(); + if (TERMINATION_ERROR.compareAndSet(this, null, e)) { + serialScheduler.schedule(() -> terminate(e)); + } + } + } + + private void tryShutdown() { + if (terminationError == null) { + if (TERMINATION_ERROR.compareAndSet(this, null, CLOSED_CHANNEL_EXCEPTION)) { + serialScheduler.schedule(() -> terminate(CLOSED_CHANNEL_EXCEPTION)); + } + } + } + + private void terminate(Throwable e) { + if (keepAliveFramesAcceptor != null) { + keepAliveFramesAcceptor.dispose(); + } + connection.dispose(); + leaseHandler.dispose(); + + // Iterate explicitly to handle collisions with concurrent removals + final IntObjectMap> receivers = this.receivers; + // copy to avoid collection modification from the foreach loop + final Collection> receiversCopy = + new ArrayList<>(receivers.values()); + for (Processor handler : receiversCopy) { + try { + handler.onError(e); + } catch (Throwable ex) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Dropped exception", ex); + } + } + } + // Iterate explicitly to handle collisions with concurrent removals + final IntObjectMap senders = this.senders; + // copy to avoid collection modification from the foreach loop + final Collection sendersCopy = new ArrayList<>(senders.values()); + for (Subscription subscription : sendersCopy) { + try { + subscription.cancel(); + } catch (Throwable ex) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Dropped exception", ex); + } + } + } + + senders.clear(); + receivers.clear(); + sendProcessor.dispose(); + if (e == CLOSED_CHANNEL_EXCEPTION) { + onClose.onComplete(); + } else { + onClose.onError(e); + } + } + + private void handleSendProcessorError(Throwable t) { + connection.dispose(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java new file mode 100644 index 000000000..edb01ba16 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -0,0 +1,632 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.IllegalReferenceCountException; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; +import io.netty.util.collection.IntObjectMap; +import io.rsocket.DuplexConnection; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.frame.CancelFrameCodec; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestNFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.SynchronizedIntObjectHashMap; +import io.rsocket.internal.UnboundedProcessor; +import io.rsocket.lease.ResponderLeaseHandler; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.Consumer; +import java.util.function.LongConsumer; +import java.util.function.Supplier; +import org.reactivestreams.Processor; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.Exceptions; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; +import reactor.core.publisher.UnicastProcessor; +import reactor.util.annotation.Nullable; +import reactor.util.concurrent.Queues; + +/** Responder side of RSocket. Receives {@link ByteBuf}s from a peer's {@link RSocketRequester} */ +class RSocketResponder implements RSocket { + private static final Logger LOGGER = LoggerFactory.getLogger(RSocketResponder.class); + + private static final Consumer DROPPED_ELEMENTS_CONSUMER = + referenceCounted -> { + if (referenceCounted.refCnt() > 0) { + try { + referenceCounted.release(); + } catch (IllegalReferenceCountException e) { + // ignored + } + } + }; + private static final Exception CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException(); + + private final DuplexConnection connection; + private final RSocket requestHandler; + + @SuppressWarnings("deprecation") + private final io.rsocket.ResponderRSocket responderRSocket; + + private final PayloadDecoder payloadDecoder; + private final ResponderLeaseHandler leaseHandler; + private final Disposable leaseHandlerDisposable; + + private volatile Throwable terminationError; + private static final AtomicReferenceFieldUpdater TERMINATION_ERROR = + AtomicReferenceFieldUpdater.newUpdater( + RSocketResponder.class, Throwable.class, "terminationError"); + + private final int mtu; + private final int maxFrameLength; + + private final IntObjectMap sendingSubscriptions; + private final IntObjectMap> channelProcessors; + + private final UnboundedProcessor sendProcessor; + private final ByteBufAllocator allocator; + + RSocketResponder( + DuplexConnection connection, + RSocket requestHandler, + PayloadDecoder payloadDecoder, + ResponderLeaseHandler leaseHandler, + int mtu, + int maxFrameLength) { + this.connection = connection; + this.allocator = connection.alloc(); + this.mtu = mtu; + this.maxFrameLength = maxFrameLength; + + this.requestHandler = requestHandler; + this.responderRSocket = + (requestHandler instanceof io.rsocket.ResponderRSocket) + ? (io.rsocket.ResponderRSocket) requestHandler + : null; + + this.payloadDecoder = payloadDecoder; + this.leaseHandler = leaseHandler; + this.sendingSubscriptions = new SynchronizedIntObjectHashMap<>(); + this.channelProcessors = new SynchronizedIntObjectHashMap<>(); + + // DO NOT Change the order here. The Send processor must be subscribed to before receiving + // connections + this.sendProcessor = new UnboundedProcessor<>(); + + connection.send(sendProcessor).subscribe(null, this::handleSendProcessorError); + + connection.receive().subscribe(this::handleFrame, e -> {}); + leaseHandlerDisposable = leaseHandler.send(sendProcessor::onNextPrioritized); + + this.connection + .onClose() + .subscribe(null, this::tryTerminateOnConnectionError, this::tryTerminateOnConnectionClose); + } + + private void handleSendProcessorError(Throwable t) { + cleanUpSendingSubscriptions(); + cleanUpChannelProcessors(t); + } + + private void tryTerminateOnConnectionError(Throwable e) { + tryTerminate(() -> e); + } + + private void tryTerminateOnConnectionClose() { + tryTerminate(() -> CLOSED_CHANNEL_EXCEPTION); + } + + private void tryTerminate(Supplier errorSupplier) { + if (terminationError == null) { + Throwable e = errorSupplier.get(); + if (TERMINATION_ERROR.compareAndSet(this, null, e)) { + cleanup(e); + } + } + } + + @Override + public Mono fireAndForget(Payload payload) { + try { + if (leaseHandler.useLease()) { + return requestHandler.fireAndForget(payload); + } else { + payload.release(); + return Mono.error(leaseHandler.leaseError()); + } + } catch (Throwable t) { + return Mono.error(t); + } + } + + @Override + public Mono requestResponse(Payload payload) { + try { + if (leaseHandler.useLease()) { + return requestHandler.requestResponse(payload); + } else { + payload.release(); + return Mono.error(leaseHandler.leaseError()); + } + } catch (Throwable t) { + return Mono.error(t); + } + } + + @Override + public Flux requestStream(Payload payload) { + try { + if (leaseHandler.useLease()) { + return requestHandler.requestStream(payload); + } else { + payload.release(); + return Flux.error(leaseHandler.leaseError()); + } + } catch (Throwable t) { + return Flux.error(t); + } + } + + @Override + public Flux requestChannel(Publisher payloads) { + try { + if (leaseHandler.useLease()) { + return requestHandler.requestChannel(payloads); + } else { + return Flux.error(leaseHandler.leaseError()); + } + } catch (Throwable t) { + return Flux.error(t); + } + } + + private Flux requestChannel(Payload payload, Publisher payloads) { + try { + if (leaseHandler.useLease()) { + return responderRSocket.requestChannel(payload, payloads); + } else { + payload.release(); + return Flux.error(leaseHandler.leaseError()); + } + } catch (Throwable t) { + return Flux.error(t); + } + } + + @Override + public Mono metadataPush(Payload payload) { + try { + return requestHandler.metadataPush(payload); + } catch (Throwable t) { + return Mono.error(t); + } + } + + @Override + public void dispose() { + tryTerminate(() -> new CancellationException("Disposed")); + } + + @Override + public boolean isDisposed() { + return connection.isDisposed(); + } + + @Override + public Mono onClose() { + return connection.onClose(); + } + + private void cleanup(Throwable e) { + cleanUpSendingSubscriptions(); + cleanUpChannelProcessors(e); + + connection.dispose(); + leaseHandlerDisposable.dispose(); + requestHandler.dispose(); + sendProcessor.dispose(); + } + + private synchronized void cleanUpSendingSubscriptions() { + // Iterate explicitly to handle collisions with concurrent removals + final IntObjectMap sendingSubscriptions = this.sendingSubscriptions; + final Collection sendingSubscriptionsCopy = + new ArrayList<>(sendingSubscriptions.values()); + for (Subscription subscription : sendingSubscriptionsCopy) { + try { + subscription.cancel(); + } catch (Throwable ex) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Dropped exception", ex); + } + } + } + sendingSubscriptions.clear(); + } + + private synchronized void cleanUpChannelProcessors(Throwable e) { + // Iterate explicitly to handle collisions with concurrent removals + for (IntObjectMap.PrimitiveEntry> entry : + channelProcessors.entries()) { + try { + entry.value().onError(e); + } catch (Throwable ex) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Dropped exception", ex); + } + } + } + channelProcessors.clear(); + } + + private void handleFrame(ByteBuf frame) { + try { + int streamId = FrameHeaderCodec.streamId(frame); + Subscriber receiver; + FrameType frameType = FrameHeaderCodec.frameType(frame); + switch (frameType) { + case REQUEST_FNF: + handleFireAndForget(streamId, fireAndForget(payloadDecoder.apply(frame))); + break; + case REQUEST_RESPONSE: + handleRequestResponse(streamId, requestResponse(payloadDecoder.apply(frame))); + break; + case CANCEL: + handleCancelFrame(streamId); + break; + case REQUEST_N: + handleRequestN(streamId, frame); + break; + case REQUEST_STREAM: + long streamInitialRequestN = RequestStreamFrameCodec.initialRequestN(frame); + Payload streamPayload = payloadDecoder.apply(frame); + handleStream(streamId, requestStream(streamPayload), streamInitialRequestN, null); + break; + case REQUEST_CHANNEL: + long channelInitialRequestN = RequestChannelFrameCodec.initialRequestN(frame); + Payload channelPayload = payloadDecoder.apply(frame); + handleChannel(streamId, channelPayload, channelInitialRequestN); + break; + case METADATA_PUSH: + handleMetadataPush(metadataPush(payloadDecoder.apply(frame))); + break; + case PAYLOAD: + // TODO: Hook in receiving socket. + break; + case NEXT: + receiver = channelProcessors.get(streamId); + if (receiver != null) { + receiver.onNext(payloadDecoder.apply(frame)); + } + break; + case COMPLETE: + receiver = channelProcessors.get(streamId); + if (receiver != null) { + receiver.onComplete(); + } + break; + case ERROR: + receiver = channelProcessors.get(streamId); + if (receiver != null) { + // FIXME: when https://github.com/reactor/reactor-core/issues/2176 is resolved + // This is workaround to handle specific Reactor related case when + // onError call may not return normally + try { + receiver.onError(io.rsocket.exceptions.Exceptions.from(streamId, frame)); + } catch (RuntimeException e) { + if (reactor.core.Exceptions.isBubbling(e) + || reactor.core.Exceptions.isErrorCallbackNotImplemented(e)) { + if (LOGGER.isDebugEnabled()) { + Throwable unwrapped = reactor.core.Exceptions.unwrap(e); + LOGGER.debug("Unhandled dropped exception", unwrapped); + } + } + } + } + break; + case NEXT_COMPLETE: + receiver = channelProcessors.get(streamId); + if (receiver != null) { + receiver.onNext(payloadDecoder.apply(frame)); + receiver.onComplete(); + } + break; + case SETUP: + handleError(streamId, new IllegalStateException("Setup frame received post setup.")); + break; + case LEASE: + default: + handleError( + streamId, + new IllegalStateException("ServerRSocket: Unexpected frame type: " + frameType)); + break; + } + ReferenceCountUtil.safeRelease(frame); + } catch (Throwable t) { + ReferenceCountUtil.safeRelease(frame); + throw Exceptions.propagate(t); + } + } + + private void handleFireAndForget(int streamId, Mono result) { + result.subscribe( + new BaseSubscriber() { + @Override + protected void hookOnSubscribe(Subscription subscription) { + sendingSubscriptions.put(streamId, subscription); + subscription.request(Long.MAX_VALUE); + } + + @Override + protected void hookOnError(Throwable throwable) {} + + @Override + protected void hookFinally(SignalType type) { + sendingSubscriptions.remove(streamId); + } + }); + } + + private void handleRequestResponse(int streamId, Mono response) { + final BaseSubscriber subscriber = + new BaseSubscriber() { + private boolean isEmpty = true; + + @Override + protected void hookOnNext(Payload payload) { + isEmpty = false; + + if (!PayloadValidationUtils.isValid(mtu, payload, maxFrameLength)) { + payload.release(); + cancel(); + sendingSubscriptions.remove(streamId, this); + handleError(streamId, new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE)); + return; + } + + ByteBuf byteBuf = + PayloadFrameCodec.encodeNextCompleteReleasingPayload(allocator, streamId, payload); + sendProcessor.onNext(byteBuf); + sendingSubscriptions.remove(streamId, this); + } + + @Override + protected void hookOnError(Throwable throwable) { + if (sendingSubscriptions.remove(streamId, this)) { + handleError(streamId, throwable); + } + } + + @Override + protected void hookOnComplete() { + if (isEmpty && sendingSubscriptions.remove(streamId, this)) { + sendProcessor.onNext(PayloadFrameCodec.encodeComplete(allocator, streamId)); + } + } + }; + + sendingSubscriptions.put(streamId, subscriber); + response.doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER).subscribe(subscriber); + } + + private void handleStream( + int streamId, + Flux response, + long initialRequestN, + @Nullable UnicastProcessor requestChannel) { + final BaseSubscriber subscriber = + new BaseSubscriber() { + + @Override + protected void hookOnSubscribe(Subscription s) { + s.request(initialRequestN); + } + + @Override + protected void hookOnNext(Payload payload) { + try { + if (!PayloadValidationUtils.isValid(mtu, payload, maxFrameLength)) { + payload.release(); + final IllegalArgumentException t = + new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); + + cancelStream(t); + return; + } + + ByteBuf byteBuf = + PayloadFrameCodec.encodeNextReleasingPayload(allocator, streamId, payload); + sendProcessor.onNext(byteBuf); + } catch (Throwable e) { + cancelStream(e); + } + } + + private void cancelStream(Throwable t) { + // Cancel the output stream and send an ERROR frame but do not dispose the + // requestChannel (i.e. close the connection) since the spec allows to leave + // the channel in half-closed state. + // specifically for requestChannel case so when Payload is invalid we will not be + // sending CancelFrame and ErrorFrame + // Note: CancelFrame is redundant and due to spec + // (https://github.com/rsocket/rsocket/blob/master/Protocol.md#request-channel) + // Upon receiving an ERROR[APPLICATION_ERROR|REJECTED|CANCELED|INVALID], the stream + // is terminated on both Requester and Responder. + // Upon sending an ERROR[APPLICATION_ERROR|REJECTED|CANCELED|INVALID], the stream is + // terminated on both the Requester and Responder. + if (requestChannel != null) { + channelProcessors.remove(streamId, requestChannel); + } + cancel(); + handleError(streamId, t); + } + + @Override + protected void hookOnComplete() { + if (sendingSubscriptions.remove(streamId, this)) { + sendProcessor.onNext(PayloadFrameCodec.encodeComplete(allocator, streamId)); + } + } + + @Override + protected void hookOnError(Throwable throwable) { + if (sendingSubscriptions.remove(streamId, this)) { + // specifically for requestChannel case so when Payload is invalid we will not be + // sending CancelFrame and ErrorFrame + // Note: CancelFrame is redundant and due to spec + // (https://github.com/rsocket/rsocket/blob/master/Protocol.md#request-channel) + // Upon receiving an ERROR[APPLICATION_ERROR|REJECTED|CANCELED|INVALID], the stream + // is terminated on both Requester and Responder. + // Upon sending an ERROR[APPLICATION_ERROR|REJECTED|CANCELED|INVALID], the stream is + // terminated on both the Requester and Responder. + if (requestChannel != null && !requestChannel.isDisposed()) { + if (channelProcessors.remove(streamId, requestChannel)) { + try { + requestChannel.dispose(); + } catch (Throwable e) { + // ignore to ensure it does not blows up if it racing with async + // cancel + } + } + } + + handleError(streamId, throwable); + } + } + }; + + sendingSubscriptions.put(streamId, subscriber); + response.doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER).subscribe(subscriber); + } + + private void handleChannel(int streamId, Payload payload, long initialRequestN) { + UnicastProcessor frames = UnicastProcessor.create(Queues.one().get()); + channelProcessors.put(streamId, frames); + + Flux payloads = + frames + .doOnRequest( + new LongConsumer() { + boolean first = true; + + @Override + public void accept(long l) { + long n; + if (first) { + first = false; + n = l - 1L; + } else { + n = l; + } + if (n > 0) { + sendProcessor.onNext(RequestNFrameCodec.encode(allocator, streamId, n)); + } + } + }) + .doFinally( + signalType -> { + if (channelProcessors.remove(streamId, frames)) { + if (signalType == SignalType.CANCEL) { + sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); + } else if (signalType == SignalType.ON_ERROR) { + Subscription subscription = sendingSubscriptions.remove(streamId); + if (subscription != null) { + subscription.cancel(); + } + } + } + }) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); + + // not chained, as the payload should be enqueued in the Unicast processor before this method + // returns + // and any later payload can be processed + frames.onNext(payload); + + if (responderRSocket != null) { + handleStream(streamId, requestChannel(payload, payloads), initialRequestN, frames); + } else { + handleStream(streamId, requestChannel(payloads), initialRequestN, frames); + } + } + + private void handleMetadataPush(Mono result) { + result.subscribe( + new BaseSubscriber() { + @Override + protected void hookOnSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + protected void hookOnError(Throwable throwable) {} + }); + } + + private void handleCancelFrame(int streamId) { + Subscription subscription = sendingSubscriptions.remove(streamId); + Processor processor = channelProcessors.remove(streamId); + + if (processor != null) { + try { + processor.onError(new CancellationException("Disposed")); + } catch (Exception e) { + // ignore + } + } + + if (subscription != null) { + subscription.cancel(); + } + } + + private void handleError(int streamId, Throwable t) { + sendProcessor.onNext(ErrorFrameCodec.encode(allocator, streamId, t)); + } + + private void handleRequestN(int streamId, ByteBuf frame) { + Subscription subscription = sendingSubscriptions.get(streamId); + + if (subscription != null) { + long n = RequestNFrameCodec.requestN(frame); + subscription.request(n); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java new file mode 100644 index 000000000..610636f02 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -0,0 +1,491 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import static io.rsocket.core.PayloadValidationUtils.assertValidateSetup; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + +import io.netty.buffer.ByteBuf; +import io.rsocket.Closeable; +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.DuplexConnection; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.exceptions.InvalidSetupException; +import io.rsocket.exceptions.RejectedSetupException; +import io.rsocket.fragmentation.FragmentationDuplexConnection; +import io.rsocket.fragmentation.ReassemblyDuplexConnection; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.SetupFrameCodec; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.ClientServerInputMultiplexer; +import io.rsocket.lease.Leases; +import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.plugins.InitializingInterceptorRegistry; +import io.rsocket.plugins.InterceptorRegistry; +import io.rsocket.resume.SessionManager; +import io.rsocket.transport.ServerTransport; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +/** + * The main class for starting an RSocket server. + * + *

For example: + * + *

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

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

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

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

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

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

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

By default, {@code new RSocket(){}} is used for handling which rejects requests from the + * client with {@link UnsupportedOperationException}. + * + * @param acceptor the acceptor to handle incoming connections and requests with + * @return the same instance for method chaining + */ + public RSocketServer acceptor(SocketAcceptor acceptor) { + Objects.requireNonNull(acceptor); + this.acceptor = acceptor; + return this; + } + + /** + * Configure interception at one of the following levels: + * + *

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

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

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

Example usage: + * + *

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

By default this is not enabled. + * + * @param supplier supplier for a {@link Leases} + * @return the same instance for method chaining + * @return the same instance for method chaining + * @see Lease + * Semantics + */ + public RSocketServer lease(Supplier> supplier) { + this.leasesSupplier = supplier; + return this; + } + + /** + * When this is set, frames reassembler control maximum payload size which can be reassembled. + * + *

By default this is not set in which case maximum reassembled payloads size is not + * controlled. + * + * @param maxInboundPayloadSize the threshold size for reassembly, must no be less than 64 bytes. + * Please note, {@code maxInboundPayloadSize} must always be greater or equal to {@link + * io.rsocket.transport.Transport#maxFrameLength()}, otherwise inbound frame can exceed the + * {@code maxInboundPayloadSize} + * @return the same instance for method chaining + * @see Fragmentation + * and Reassembly + */ + public RSocketServer maxInboundPayloadSize(int maxInboundPayloadSize) { + this.maxInboundPayloadSize = + ReassemblyDuplexConnection.assertInboundPayloadSize(maxInboundPayloadSize); + return this; + } + + /** + * When this is set, frames larger than the given maximum transmission unit (mtu) size value are + * fragmented. + * + *

By default this is not set in which case payloads are sent whole up to the maximum frame + * size of 16,777,215 bytes. + * + * @param mtu the threshold size for fragmentation, must be no less than 64 + * @return the same instance for method chaining + * @see Fragmentation + * and Reassembly + */ + public RSocketServer fragment(int mtu) { + this.mtu = FragmentationDuplexConnection.assertMtu(mtu); + return this; + } + + /** + * Configure the {@code PayloadDecoder} used to create {@link Payload}'s from incoming raw frame + * buffers. The following decoders are available: + * + *

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

By default this is set to {@link PayloadDecoder#DEFAULT} in which case data and metadata are + * copied and do not need to be tracked and released. + * + * @param decoder the decoder to use + * @return the same instance for method chaining + */ + public RSocketServer payloadDecoder(PayloadDecoder decoder) { + Objects.requireNonNull(decoder); + this.payloadDecoder = decoder; + return this; + } + + /** + * Start the server on the given transport. + * + *

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

    + *
  • {@link io.rsocket.transport.netty.client.TcpServerTransport TcpServerTransport} via + * {@code rsocket-transport-netty}. + *
  • {@link io.rsocket.transport.netty.client.WebsocketServerTransport + * WebsocketServerTransport} via {@code rsocket-transport-netty}. + *
  • {@link io.rsocket.transport.local.LocalServerTransport LocalServerTransport} via {@code + * rsocket-transport-local} + *
+ * + * @param transport the transport of choice to connect with + * @param the type of {@code Closeable} for the given transport + * @return a {@code Mono} with a {@code Closeable} that can be used to obtain information about + * the server, stop it, or be notified of when it is stopped. + */ + public Mono bind(ServerTransport transport) { + return Mono.defer( + new Supplier>() { + final ServerSetup serverSetup = serverSetup(); + + @Override + public Mono get() { + int maxFrameLength = transport.maxFrameLength(); + assertValidateSetup(maxFrameLength, maxInboundPayloadSize, mtu); + return transport + .start(duplexConnection -> acceptor(serverSetup, duplexConnection, maxFrameLength)) + .doOnNext(c -> c.onClose().doFinally(v -> serverSetup.dispose()).subscribe()); + } + }); + } + + /** + * Start the server on the given transport. Effectively is a shortcut for {@code + * .bind(ServerTransport).block()} + */ + public T bindNow(ServerTransport transport) { + return bind(transport).block(); + } + /** + * An alternative to {@link #bind(ServerTransport)} that is useful for installing RSocket on a + * server that is started independently. + * + * @see io.rsocket.examples.transport.ws.WebSocketHeadersSample + */ + public ServerTransport.ConnectionAcceptor asConnectionAcceptor() { + return asConnectionAcceptor(FRAME_LENGTH_MASK); + } + + /** + * An alternative to {@link #bind(ServerTransport)} that is useful for installing RSocket on a + * server that is started independently. + * + * @see io.rsocket.examples.transport.ws.WebSocketHeadersSample + */ + public ServerTransport.ConnectionAcceptor asConnectionAcceptor(int maxFrameLength) { + assertValidateSetup(maxFrameLength, maxInboundPayloadSize, mtu); + return new ServerTransport.ConnectionAcceptor() { + private final ServerSetup serverSetup = serverSetup(); + + @Override + public Mono apply(DuplexConnection connection) { + return acceptor(serverSetup, connection, maxFrameLength); + } + }; + } + + private Mono acceptor( + ServerSetup serverSetup, DuplexConnection connection, int maxFrameLength) { + connection = + mtu > 0 + ? new FragmentationDuplexConnection(connection, mtu, maxInboundPayloadSize, "server") + : new ReassemblyDuplexConnection(connection, maxInboundPayloadSize); + + ClientServerInputMultiplexer multiplexer = + new ClientServerInputMultiplexer(connection, interceptors, false); + + return multiplexer + .asSetupConnection() + .receive() + .next() + .flatMap(startFrame -> accept(serverSetup, startFrame, multiplexer, maxFrameLength)); + } + + private Mono acceptResume( + ServerSetup serverSetup, ByteBuf resumeFrame, ClientServerInputMultiplexer multiplexer) { + return serverSetup.acceptRSocketResume(resumeFrame, multiplexer); + } + + private Mono accept( + ServerSetup serverSetup, + ByteBuf startFrame, + ClientServerInputMultiplexer multiplexer, + int maxFrameLength) { + switch (FrameHeaderCodec.frameType(startFrame)) { + case SETUP: + return acceptSetup(serverSetup, startFrame, multiplexer, maxFrameLength); + case RESUME: + return acceptResume(serverSetup, startFrame, multiplexer); + default: + return serverSetup + .sendError( + multiplexer, + new InvalidSetupException( + "invalid setup frame: " + FrameHeaderCodec.frameType(startFrame))) + .doFinally( + signalType -> { + startFrame.release(); + multiplexer.dispose(); + }); + } + } + + private Mono acceptSetup( + ServerSetup serverSetup, + ByteBuf setupFrame, + ClientServerInputMultiplexer multiplexer, + int maxFrameLength) { + + if (!SetupFrameCodec.isSupportedVersion(setupFrame)) { + return serverSetup + .sendError( + multiplexer, + new InvalidSetupException( + "Unsupported version: " + SetupFrameCodec.humanReadableVersion(setupFrame))) + .doFinally( + signalType -> { + setupFrame.release(); + multiplexer.dispose(); + }); + } + + boolean leaseEnabled = leasesSupplier != null; + if (SetupFrameCodec.honorLease(setupFrame) && !leaseEnabled) { + return serverSetup + .sendError(multiplexer, new InvalidSetupException("lease is not supported")) + .doFinally( + signalType -> { + setupFrame.release(); + multiplexer.dispose(); + }); + } + + return serverSetup.acceptRSocketSetup( + setupFrame, + multiplexer, + (keepAliveHandler, wrappedMultiplexer) -> { + ConnectionSetupPayload setupPayload = new DefaultConnectionSetupPayload(setupFrame); + + Leases leases = leaseEnabled ? leasesSupplier.get() : null; + RequesterLeaseHandler requesterLeaseHandler = + leaseEnabled + ? new RequesterLeaseHandler.Impl(SERVER_TAG, leases.receiver()) + : RequesterLeaseHandler.None; + + RSocket rSocketRequester = + new RSocketRequester( + wrappedMultiplexer.asServerConnection(), + payloadDecoder, + StreamIdSupplier.serverSupplier(), + mtu, + maxFrameLength, + setupPayload.keepAliveInterval(), + setupPayload.keepAliveMaxLifetime(), + keepAliveHandler, + requesterLeaseHandler, + Schedulers.single(Schedulers.parallel())); + + RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); + + return interceptors + .initSocketAcceptor(acceptor) + .accept(setupPayload, wrappedRSocketRequester) + .onErrorResume( + err -> + serverSetup + .sendError(multiplexer, rejectedSetupError(err)) + .then(Mono.error(err))) + .doOnNext( + rSocketHandler -> { + RSocket wrappedRSocketHandler = interceptors.initResponder(rSocketHandler); + DuplexConnection connection = wrappedMultiplexer.asClientConnection(); + + ResponderLeaseHandler responderLeaseHandler = + leaseEnabled + ? new ResponderLeaseHandler.Impl<>( + SERVER_TAG, connection.alloc(), leases.sender(), leases.stats()) + : ResponderLeaseHandler.None; + + RSocket rSocketResponder = + new RSocketResponder( + connection, + wrappedRSocketHandler, + payloadDecoder, + responderLeaseHandler, + mtu, + maxFrameLength); + }) + .doFinally(signalType -> setupPayload.release()) + .then(); + }); + } + + private ServerSetup serverSetup() { + return resume != null ? createSetup() : new ServerSetup.DefaultServerSetup(); + } + + ServerSetup createSetup() { + return new ServerSetup.ResumableServerSetup( + new SessionManager(), + resume.getSessionDuration(), + resume.getStreamTimeout(), + resume.getStoreFactory(SERVER_TAG), + resume.isCleanupStoreOnKeepAlive()); + } + + private Exception rejectedSetupError(Throwable err) { + String msg = err.getMessage(); + return new RejectedSetupException(msg == null ? "rejected by server acceptor" : msg); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/ReconnectMono.java b/rsocket-core/src/main/java/io/rsocket/core/ReconnectMono.java new file mode 100644 index 000000000..44e4ffa81 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/ReconnectMono.java @@ -0,0 +1,271 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import java.time.Duration; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Disposable; +import reactor.core.Scannable; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +final class ReconnectMono extends Mono implements Invalidatable, Disposable, Scannable { + + final Mono source; + final BiConsumer onValueReceived; + final Consumer onValueExpired; + final ResolvingInner resolvingInner; + + ReconnectMono( + Mono source, + Consumer onValueExpired, + BiConsumer onValueReceived) { + this.source = source; + this.onValueExpired = onValueExpired; + this.onValueReceived = onValueReceived; + this.resolvingInner = new ResolvingInner<>(this); + } + + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) return source; + if (key == Attr.PREFETCH) return Integer.MAX_VALUE; + + final boolean isDisposed = isDisposed(); + if (key == Attr.TERMINATED) return isDisposed; + if (key == Attr.ERROR) return this.resolvingInner.t; + + return null; + } + + @Override + public void invalidate() { + this.resolvingInner.invalidate(); + } + + @Override + public void dispose() { + this.resolvingInner.terminate( + new CancellationException("ReconnectMono has already been disposed")); + } + + @Override + public boolean isDisposed() { + return this.resolvingInner.isDisposed(); + } + + @Override + @SuppressWarnings("uncheked") + public void subscribe(CoreSubscriber actual) { + final ResolvingOperator.MonoDeferredResolutionOperator inner = + new ResolvingOperator.MonoDeferredResolutionOperator<>(this.resolvingInner, actual); + actual.onSubscribe(inner); + + this.resolvingInner.observe(inner); + } + + /** + * Block the calling thread indefinitely, waiting for the completion of this {@code + * ReconnectMono}. If the {@link ReconnectMono} is completed with an error a RuntimeException that + * wraps the error is thrown. + * + * @return the value of this {@code ReconnectMono} + */ + @Override + @Nullable + public T block() { + return block(null); + } + + /** + * Block the calling thread for the specified time, waiting for the completion of this {@code + * ReconnectMono}. If the {@link ReconnectMono} is completed with an error a RuntimeException that + * wraps the error is thrown. + * + * @param timeout the timeout value as a {@link Duration} + * @return the value of this {@code ReconnectMono} or {@code null} if the timeout is reached and + * the {@code ReconnectMono} has not completed + */ + @Override + @Nullable + @SuppressWarnings("uncheked") + public T block(@Nullable Duration timeout) { + return this.resolvingInner.block(timeout); + } + + /** + * Subscriber that subscribes to the source {@link Mono} to receive its value.
+ * Note that the source is not expected to complete empty, and if this happens, execution will + * terminate with an {@code IllegalStateException}. + */ + static final class ReconnectMainSubscriber implements CoreSubscriber { + + final ResolvingInner parent; + + volatile Subscription s; + + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater S = + AtomicReferenceFieldUpdater.newUpdater( + ReconnectMainSubscriber.class, Subscription.class, "s"); + + volatile int wip; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(ReconnectMainSubscriber.class, "wip"); + + T value; + + ReconnectMainSubscriber(ResolvingInner parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Subscription s) { + if (Operators.setOnce(S, this, s)) { + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onComplete() { + final Subscription s = this.s; + final T value = this.value; + + if (s == Operators.cancelledSubscription() || !S.compareAndSet(this, s, null)) { + this.doFinally(); + return; + } + + final ResolvingInner p = this.parent; + if (value == null) { + p.terminate(new IllegalStateException("Source completed empty")); + } else { + p.complete(value); + } + } + + @Override + public void onError(Throwable t) { + final Subscription s = this.s; + + if (s == Operators.cancelledSubscription() + || S.getAndSet(this, Operators.cancelledSubscription()) + == Operators.cancelledSubscription()) { + this.doFinally(); + Operators.onErrorDropped(t, Context.empty()); + return; + } + + this.doFinally(); + // terminate upstream which means retryBackoff has exhausted + this.parent.terminate(t); + } + + @Override + public void onNext(T value) { + if (this.s == Operators.cancelledSubscription()) { + this.parent.doOnValueExpired(value); + return; + } + + this.value = value; + // volatile write and check on racing + this.doFinally(); + } + + void dispose() { + if (Operators.terminate(S, this)) { + this.doFinally(); + } + } + + final void doFinally() { + if (WIP.getAndIncrement(this) != 0) { + return; + } + + int m = 1; + T value; + + for (; ; ) { + value = this.value; + if (value != null && this.s == Operators.cancelledSubscription()) { + this.value = null; + this.parent.doOnValueExpired(value); + return; + } + + m = WIP.addAndGet(this, -m); + if (m == 0) { + return; + } + } + } + } + + static final class ResolvingInner extends ResolvingOperator implements Scannable { + + final ReconnectMono parent; + final ReconnectMainSubscriber mainSubscriber; + + ResolvingInner(ReconnectMono parent) { + this.parent = parent; + this.mainSubscriber = new ReconnectMainSubscriber<>(this); + } + + @Override + protected void doOnValueExpired(T value) { + this.parent.onValueExpired.accept(value); + } + + @Override + protected void doOnValueResolved(T value) { + this.parent.onValueReceived.accept(value, this.parent); + } + + @Override + protected void doOnDispose() { + this.mainSubscriber.dispose(); + } + + @Override + protected void doSubscribe() { + this.parent.source.subscribe(this.mainSubscriber); + } + + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) return this.parent; + return null; + } + } +} + +interface Invalidatable { + + void invalidate(); +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java b/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java new file mode 100644 index 000000000..09eeadb6c --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java @@ -0,0 +1,222 @@ +package io.rsocket.core; + +import io.rsocket.Payload; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.CorePublisher; +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.core.publisher.Operators; +import reactor.core.publisher.SignalType; +import reactor.util.context.Context; + +/** + * This is a support class for handling of request input, intended for use with {@link + * Operators#lift}. It ensures serial execution of cancellation vs first request signals and also + * provides hooks for separate handling of first vs subsequent {@link Subscription#request} + * invocations. + */ +abstract class RequestOperator + implements CoreSubscriber, + CorePublisher, + Fuseable.QueueSubscription, + Fuseable { + + final CorePublisher source; + final String errorMessageOnSecondSubscription; + + CoreSubscriber actual; + + Subscription s; + Fuseable.QueueSubscription qs; + + int streamId; + boolean firstRequest = true; + + volatile int wip; + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(RequestOperator.class, "wip"); + + RequestOperator(CorePublisher source, String errorMessageOnSecondSubscription) { + this.source = source; + this.errorMessageOnSecondSubscription = errorMessageOnSecondSubscription; + WIP.lazySet(this, -1); + } + + @Override + public void subscribe(Subscriber actual) { + subscribe(Operators.toCoreSubscriber(actual)); + } + + @Override + public void subscribe(CoreSubscriber actual) { + if (this.wip == -1 && WIP.compareAndSet(this, -1, 0)) { + this.actual = actual; + source.subscribe(this); + actual.onSubscribe(this); + } else { + Operators.error(actual, new IllegalStateException(this.errorMessageOnSecondSubscription)); + } + } + + /** + * Optional hook executed exactly once on the first {@link Subscription#request) invocation + * and right after the {@link Subscription#request} was propagated to the upstream subscription. + * + *

Note: this hook may not be invoked if cancellation happened before this invocation + */ + void hookOnFirstRequest(long n) {} + + /** + * Optional hook executed after the {@link Subscription#request} was propagated to the upstream + * subscription and excludes the first {@link Subscription#request} invocation. + */ + void hookOnRemainingRequests(long n) {} + + /** Optional hook executed after this {@link Subscription} cancelling. */ + void hookOnCancel() {} + + /** + * Optional hook executed after {@link org.reactivestreams.Subscriber} termination events + * (onError, onComplete). + * + * @param signalType the type of termination event that triggered the hook ({@link + * SignalType#ON_ERROR} or {@link SignalType#ON_COMPLETE}) + */ + void hookOnTerminal(SignalType signalType) {} + + @Override + public Context currentContext() { + return actual.currentContext(); + } + + @Override + public void request(long n) { + if (!firstRequest) { + try { + this.hookOnRemainingRequests(n); + } catch (Throwable throwable) { + onError(throwable); + } + return; + } + + if (WIP.getAndIncrement(this) != 0) { + return; + } + + this.firstRequest = false; + int missed = 1; + + boolean firstLoop = true; + for (; ; ) { + if (firstLoop) { + firstLoop = false; + try { + // since in all the scenarios where RequestOperator is used, the + // CorePublisher is either UnicastProcessor or UnicastProcessor.next() + // we are free to propagate unbounded demand to that publisher right after + // the first request happens. UnicastProcessor is only there to allow sending signals from + // the + // connection to a real subscriber and does not have to check the real demand + // For more info see + // https://github.com/rsocket/rsocket/blob/master/Protocol.md#handling-the-unexpected + this.s.request(Long.MAX_VALUE); + this.hookOnFirstRequest(n); + } catch (Throwable throwable) { + onError(throwable); + return; + } + } else { + try { + this.hookOnCancel(); + } catch (Throwable throwable) { + onError(throwable); + } + return; + } + + missed = WIP.addAndGet(this, -missed); + if (missed == 0) { + return; + } + } + } + + @Override + public void cancel() { + this.s.cancel(); + + if (WIP.getAndIncrement(this) != 0) { + return; + } + + hookOnCancel(); + } + + @Override + @SuppressWarnings("unchecked") + public void onSubscribe(Subscription s) { + if (Operators.validate(this.s, s)) { + this.s = s; + if (s instanceof Fuseable.QueueSubscription) { + this.qs = (Fuseable.QueueSubscription) s; + } + } + } + + @Override + public void onNext(Payload t) { + this.actual.onNext(t); + } + + @Override + public void onError(Throwable t) { + this.actual.onError(t); + try { + this.hookOnTerminal(SignalType.ON_ERROR); + } catch (Throwable throwable) { + Operators.onErrorDropped(throwable, currentContext()); + } + } + + @Override + public void onComplete() { + this.actual.onComplete(); + try { + this.hookOnTerminal(SignalType.ON_COMPLETE); + } catch (Throwable throwable) { + Operators.onErrorDropped(throwable, currentContext()); + } + } + + @Override + public int requestFusion(int requestedMode) { + if (this.qs != null) { + return this.qs.requestFusion(requestedMode); + } else { + return Fuseable.NONE; + } + } + + @Override + public Payload poll() { + return this.qs.poll(); + } + + @Override + public int size() { + return this.qs.size(); + } + + @Override + public boolean isEmpty() { + return this.qs.isEmpty(); + } + + @Override + public void clear() { + this.qs.clear(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java b/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java new file mode 100644 index 000000000..979743fb1 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/ResolvingOperator.java @@ -0,0 +1,605 @@ +package io.rsocket.core; + +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; + +class ResolvingOperator implements Disposable { + + static final CancellationException ON_DISPOSE = new CancellationException("Disposed"); + + volatile int wip; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(ResolvingOperator.class, "wip"); + + volatile BiConsumer[] subscribers; + + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater SUBSCRIBERS = + AtomicReferenceFieldUpdater.newUpdater( + ResolvingOperator.class, BiConsumer[].class, "subscribers"); + + @SuppressWarnings("unchecked") + static final BiConsumer[] EMPTY_UNSUBSCRIBED = new BiConsumer[0]; + + @SuppressWarnings("unchecked") + static final BiConsumer[] EMPTY_SUBSCRIBED = new BiConsumer[0]; + + @SuppressWarnings("unchecked") + static final BiConsumer[] READY = new BiConsumer[0]; + + @SuppressWarnings("unchecked") + static final BiConsumer[] TERMINATED = new BiConsumer[0]; + + static final int ADDED_STATE = 0; + static final int READY_STATE = 1; + static final int TERMINATED_STATE = 2; + + T value; + Throwable t; + + public ResolvingOperator() { + + SUBSCRIBERS.lazySet(this, EMPTY_UNSUBSCRIBED); + } + + @Override + public final void dispose() { + this.terminate(ON_DISPOSE); + } + + @Override + public final boolean isDisposed() { + return this.subscribers == TERMINATED; + } + + public final boolean isPending() { + BiConsumer[] state = this.subscribers; + return state != READY && state != TERMINATED; + } + + @Nullable + public final T valueIfResolved() { + if (this.subscribers == READY) { + T value = this.value; + if (value != null) { + return value; + } + } + + return null; + } + + final void observe(BiConsumer actual) { + for (; ; ) { + final int state = this.add(actual); + + T value = this.value; + + if (state == READY_STATE) { + if (value != null) { + actual.accept(value, null); + return; + } + // value == null means racing between invalidate and this subscriber + // thus, we have to loop again + continue; + } else if (state == TERMINATED_STATE) { + actual.accept(null, this.t); + return; + } + + return; + } + } + + /** + * Block the calling thread for the specified time, waiting for the completion of this {@code + * ReconnectMono}. If the {@link ResolvingOperator} is completed with an error a RuntimeException + * that wraps the error is thrown. + * + * @param timeout the timeout value as a {@link Duration} + * @return the value of this {@link ResolvingOperator} or {@code null} if the timeout is reached + * and the {@link ResolvingOperator} has not completed + * @throws RuntimeException if terminated with error + * @throws IllegalStateException if timed out or {@link Thread} was interrupted with {@link + * InterruptedException} + */ + @Nullable + @SuppressWarnings({"uncheked", "BusyWait"}) + public T block(@Nullable Duration timeout) { + try { + BiConsumer[] subscribers = this.subscribers; + 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 + subscribers = this.subscribers; + } + } + + if (subscribers == TERMINATED) { + RuntimeException re = Exceptions.propagate(this.t); + re = Exceptions.addSuppressed(re, new Exception("Terminated with an error")); + throw re; + } + + // connect once + if (subscribers == EMPTY_UNSUBSCRIBED + && SUBSCRIBERS.compareAndSet(this, EMPTY_UNSUBSCRIBED, EMPTY_SUBSCRIBED)) { + this.doSubscribe(); + } + + long delay; + if (null == timeout) { + delay = 0L; + } else { + delay = System.nanoTime() + timeout.toNanos(); + } + for (; ; ) { + subscribers = this.subscribers; + + 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 + subscribers = this.subscribers; + } + } + if (subscribers == TERMINATED) { + RuntimeException re = Exceptions.propagate(this.t); + re = Exceptions.addSuppressed(re, new Exception("Terminated with an error")); + throw re; + } + if (timeout != null && delay < System.nanoTime()) { + 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) { + Thread.currentThread().interrupt(); + + throw new IllegalStateException("Thread Interruption on Mono blocking read"); + } + } + + @SuppressWarnings("unchecked") + final void terminate(Throwable t) { + if (isDisposed()) { + Operators.onErrorDropped(t, Context.empty()); + return; + } + + // writes happens before volatile write + this.t = t; + + final BiConsumer[] subscribers = SUBSCRIBERS.getAndSet(this, TERMINATED); + if (subscribers == TERMINATED) { + Operators.onErrorDropped(t, Context.empty()); + return; + } + + this.doOnDispose(); + + this.doFinally(); + + for (BiConsumer consumer : subscribers) { + consumer.accept(null, t); + } + } + + final void complete(T value) { + BiConsumer[] subscribers = this.subscribers; + if (subscribers == TERMINATED) { + this.doOnValueExpired(value); + return; + } + + this.value = value; + + for (; ; ) { + // ensures TERMINATE is going to be replaced with READY + if (SUBSCRIBERS.compareAndSet(this, subscribers, READY)) { + break; + } + + subscribers = this.subscribers; + + if (subscribers == TERMINATED) { + this.doFinally(); + return; + } + } + + this.doOnValueResolved(value); + + for (BiConsumer consumer : subscribers) { + consumer.accept(value, null); + } + } + + protected void doOnValueResolved(T value) { + // no ops + } + + final void doFinally() { + if (WIP.getAndIncrement(this) != 0) { + return; + } + + int m = 1; + T value; + + for (; ; ) { + value = this.value; + if (value != null && isDisposed()) { + this.value = null; + this.doOnValueExpired(value); + return; + } + + m = WIP.addAndGet(this, -m); + if (m == 0) { + return; + } + } + } + + final void invalidate() { + if (this.subscribers == TERMINATED) { + return; + } + + final BiConsumer[] subscribers = this.subscribers; + + if (subscribers == READY) { + // guarded section to ensure we expire value exactly once if there is racing + if (WIP.getAndIncrement(this) != 0) { + return; + } + + final T value = this.value; + if (value != null) { + this.value = null; + this.doOnValueExpired(value); + } + + int m = 1; + for (; ; ) { + if (isDisposed()) { + return; + } + + m = WIP.addAndGet(this, -m); + if (m == 0) { + break; + } + } + + SUBSCRIBERS.compareAndSet(this, READY, EMPTY_UNSUBSCRIBED); + } + } + + protected void doOnValueExpired(T value) { + // no ops + } + + protected void doOnDispose() { + // no ops + } + + final int add(BiConsumer ps) { + for (; ; ) { + BiConsumer[] a = this.subscribers; + + if (a == TERMINATED) { + return TERMINATED_STATE; + } + + if (a == READY) { + return READY_STATE; + } + + int n = a.length; + @SuppressWarnings("unchecked") + BiConsumer[] b = new BiConsumer[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = ps; + + if (SUBSCRIBERS.compareAndSet(this, a, b)) { + if (a == EMPTY_UNSUBSCRIBED) { + this.doSubscribe(); + } + return ADDED_STATE; + } + } + } + + protected void doSubscribe() { + // no ops + } + + @SuppressWarnings("unchecked") + final void remove(BiConsumer ps) { + for (; ; ) { + BiConsumer[] a = this.subscribers; + int n = a.length; + if (n == 0) { + return; + } + + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i] == ps) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + + BiConsumer[] b; + + if (n == 1) { + b = EMPTY_SUBSCRIBED; + } else { + b = new BiConsumer[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + if (SUBSCRIBERS.compareAndSet(this, a, b)) { + return; + } + } + } + + 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/core/Resume.java b/rsocket-core/src/main/java/io/rsocket/core/Resume.java new file mode 100644 index 000000000..48133af98 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/Resume.java @@ -0,0 +1,177 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.core; + +import io.netty.buffer.ByteBuf; +import io.rsocket.frame.ResumeFrameCodec; +import io.rsocket.resume.InMemoryResumableFramesStore; +import io.rsocket.resume.ResumableFramesStore; +import java.time.Duration; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.util.retry.Retry; + +/** + * Simple holder of configuration settings for the RSocket Resume capability. This can be used to + * configure an {@link RSocketConnector} or an {@link RSocketServer} except for {@link + * #retry(Retry)} and {@link #token(Supplier)} which apply only to the client side. + */ +public class Resume { + private static final Logger logger = LoggerFactory.getLogger(Resume.class); + + private Duration sessionDuration = Duration.ofMinutes(2); + + /* Storage */ + private boolean cleanupStoreOnKeepAlive; + private Function storeFactory; + private Duration streamTimeout = Duration.ofSeconds(10); + + /* Client only */ + private Supplier tokenSupplier = ResumeFrameCodec::generateResumeToken; + private Retry retry = + Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(1)) + .maxBackoff(Duration.ofSeconds(16)) + .jitter(1.0) + .doBeforeRetry(signal -> logger.debug("Connection error", signal.failure())); + + public Resume() {} + + /** + * The maximum time for a client to keep trying to reconnect. During this time client and server + * continue to store unsent frames to keep the session warm and ready to resume. + * + *

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

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

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

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

By default this is set to: + * + *

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

By default this is {@code ResumeFrameFlyweight::generateResumeToken}. + * + * @param supplier a custom generator for a resume identification token + * @return the same instance for method chaining + */ + public Resume token(Supplier supplier) { + this.tokenSupplier = supplier; + return this; + } + + // Package private accessors + + Duration getSessionDuration() { + return sessionDuration; + } + + boolean isCleanupStoreOnKeepAlive() { + return cleanupStoreOnKeepAlive; + } + + Function getStoreFactory(String tag) { + return storeFactory != null + ? storeFactory + : token -> new InMemoryResumableFramesStore(tag, 100_000); + } + + Duration getStreamTimeout() { + return streamTimeout; + } + + Retry getRetry() { + return retry; + } + + Supplier getTokenSupplier() { + return tokenSupplier; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java new file mode 100644 index 000000000..337d17c64 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java @@ -0,0 +1,158 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import static io.rsocket.keepalive.KeepAliveHandler.*; + +import io.netty.buffer.ByteBuf; +import io.rsocket.DuplexConnection; +import io.rsocket.exceptions.RejectedResumeException; +import io.rsocket.exceptions.UnsupportedSetupException; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.ResumeFrameCodec; +import io.rsocket.frame.SetupFrameCodec; +import io.rsocket.internal.ClientServerInputMultiplexer; +import io.rsocket.keepalive.KeepAliveHandler; +import io.rsocket.resume.*; +import java.time.Duration; +import java.util.function.BiFunction; +import java.util.function.Function; +import reactor.core.publisher.Mono; + +abstract class ServerSetup { + + abstract Mono acceptRSocketSetup( + ByteBuf frame, + ClientServerInputMultiplexer multiplexer, + BiFunction> then); + + abstract Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer); + + void dispose() {} + + Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { + DuplexConnection duplexConnection = multiplexer.asSetupConnection(); + return duplexConnection + .sendOne(ErrorFrameCodec.encode(duplexConnection.alloc(), 0, exception)) + .onErrorResume(err -> Mono.empty()); + } + + static class DefaultServerSetup extends ServerSetup { + + @Override + public Mono acceptRSocketSetup( + ByteBuf frame, + ClientServerInputMultiplexer multiplexer, + BiFunction> then) { + + if (SetupFrameCodec.resumeEnabled(frame)) { + return sendError(multiplexer, new UnsupportedSetupException("resume not supported")) + .doFinally( + signalType -> { + frame.release(); + multiplexer.dispose(); + }); + } else { + return then.apply(new DefaultKeepAliveHandler(multiplexer), multiplexer); + } + } + + @Override + public Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer) { + + return sendError(multiplexer, new RejectedResumeException("resume not supported")) + .doFinally( + signalType -> { + frame.release(); + multiplexer.dispose(); + }); + } + } + + static class ResumableServerSetup extends ServerSetup { + private final SessionManager sessionManager; + private final Duration resumeSessionDuration; + private final Duration resumeStreamTimeout; + private final Function resumeStoreFactory; + private final boolean cleanupStoreOnKeepAlive; + + ResumableServerSetup( + SessionManager sessionManager, + Duration resumeSessionDuration, + Duration resumeStreamTimeout, + Function resumeStoreFactory, + boolean cleanupStoreOnKeepAlive) { + this.sessionManager = sessionManager; + this.resumeSessionDuration = resumeSessionDuration; + this.resumeStreamTimeout = resumeStreamTimeout; + this.resumeStoreFactory = resumeStoreFactory; + this.cleanupStoreOnKeepAlive = cleanupStoreOnKeepAlive; + } + + @Override + public Mono acceptRSocketSetup( + ByteBuf frame, + ClientServerInputMultiplexer multiplexer, + BiFunction> then) { + + if (SetupFrameCodec.resumeEnabled(frame)) { + ByteBuf resumeToken = SetupFrameCodec.resumeToken(frame); + + ResumableDuplexConnection connection = + sessionManager + .save( + new ServerRSocketSession( + multiplexer.asClientServerConnection(), + resumeSessionDuration, + resumeStreamTimeout, + resumeStoreFactory, + resumeToken, + cleanupStoreOnKeepAlive)) + .resumableConnection(); + return then.apply( + new ResumableKeepAliveHandler(connection), + new ClientServerInputMultiplexer(connection)); + } else { + return then.apply(new DefaultKeepAliveHandler(multiplexer), multiplexer); + } + } + + @Override + public Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer) { + ServerRSocketSession session = sessionManager.get(ResumeFrameCodec.token(frame)); + if (session != null) { + return session + .continueWith(multiplexer.asClientServerConnection()) + .resumeWith(frame) + .onClose() + .then(); + } else { + return sendError(multiplexer, new RejectedResumeException("unknown resume token")) + .doFinally( + s -> { + frame.release(); + multiplexer.dispose(); + }); + } + } + + @Override + public void dispose() { + sessionManager.dispose(); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java b/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java new file mode 100644 index 000000000..15d39c993 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.core; + +import io.netty.util.collection.IntObjectMap; + +/** This API is not thread-safe and must be strictly used in serialized fashion */ +final class StreamIdSupplier { + private static final int MASK = 0x7FFFFFFF; + + private long streamId; + + // Visible for testing + StreamIdSupplier(int streamId) { + this.streamId = streamId; + } + + static StreamIdSupplier clientSupplier() { + return new StreamIdSupplier(-1); + } + + static StreamIdSupplier serverSupplier() { + return new StreamIdSupplier(0); + } + + /** + * This methods provides new stream id and ensures there is no intersections with already running + * streams. This methods is not thread-safe. + * + * @param streamIds currently running streams store + * @return next stream id + */ + int nextStreamId(IntObjectMap streamIds) { + int streamId; + do { + this.streamId += 2; + streamId = (int) (this.streamId & MASK); + } while (streamId == 0 || streamIds.containsKey(streamId)); + return streamId; + } + + boolean isBeforeOrCurrent(int streamId) { + return this.streamId >= streamId && streamId > 0; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/package-info.java b/rsocket-core/src/main/java/io/rsocket/core/package-info.java new file mode 100644 index 000000000..29db3f205 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Contains {@link io.rsocket.core.RSocketConnector RSocketConnector} and {@link + * io.rsocket.core.RSocketServer RSocketServer}, the main classes for connecting to or starting an + * RSocket server. + * + *

This package also contains a package private classes that implement support for the main + * RSocket interactions. + */ +@NonNullApi +package io.rsocket.core; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java new file mode 100644 index 000000000..cd0d46754 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; + +/** + * Application layer logic generating a Reactive Streams {@code onError} event. + * + * @see Error + * Codes + */ +public final class ApplicationErrorException extends RSocketException { + + private static final long serialVersionUID = 7873267740343446585L; + + /** + * Constructs a new exception with the specified message. + * + * @param message the message + */ + public ApplicationErrorException(String message) { + this(message, null); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message the message + * @param cause the cause of this exception + */ + public ApplicationErrorException(String message, @Nullable Throwable cause) { + super(ErrorFrameCodec.APPLICATION_ERROR, message, cause); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationException.java deleted file mode 100644 index 6a620596e..000000000 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationException.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.exceptions; - -import io.rsocket.Payload; -import java.nio.ByteBuffer; - -public class ApplicationException extends RuntimeException { - - private static final long serialVersionUID = -8801579369150844447L; - private final Payload payload; - - public ApplicationException(Payload payload) { - this.payload = payload; - } - - public ByteBuffer getErrorMetadata() { - return payload.getMetadata(); - } - - public ByteBuffer getErrorData() { - return payload.getData(); - } - - //@Override - //public synchronized Throwable fillInStackTrace() { - // return this; - //} -} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java new file mode 100644 index 000000000..d51ba0fb7 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; + +/** + * The Responder canceled the request but may have started processing it (similar to REJECTED but + * doesn't guarantee lack of side-effects). + * + * @see Error + * Codes + */ +public final class CanceledException extends RSocketException { + + private static final long serialVersionUID = 5074789326089722770L; + + /** + * Constructs a new exception with the specified message. + * + * @param message the message + */ + public CanceledException(String message) { + this(message, null); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message the message + * @param cause the cause of this exception + */ + public CanceledException(String message, @Nullable Throwable cause) { + super(ErrorFrameCodec.CANCELED, message, cause); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java new file mode 100644 index 000000000..80324aa90 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; + +/** + * The connection is being terminated. Sender or Receiver of this frame MUST wait for outstanding + * streams to terminate before closing the connection. New requests MAY not be accepted. + * + * @see Error + * Codes + */ +public final class ConnectionCloseException extends RSocketException { + + private static final long serialVersionUID = -2214953527482377471L; + + /** + * Constructs a new exception with the specified message. + * + * @param message the message + */ + public ConnectionCloseException(String message) { + this(message, null); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message the message + * @param cause the cause of this exception + */ + public ConnectionCloseException(String message, @Nullable Throwable cause) { + super(ErrorFrameCodec.CONNECTION_CLOSE, message, cause); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java new file mode 100644 index 000000000..b44714f7e --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; + +/** + * The connection is being terminated. Sender or Receiver of this frame MAY close the connection + * immediately without waiting for outstanding streams to terminate. + * + * @see Error + * Codes + */ +public final class ConnectionErrorException extends RSocketException implements Retryable { + + private static final long serialVersionUID = 512325887785119744L; + + /** + * Constructs a new exception with the specified message. + * + * @param message the message + */ + public ConnectionErrorException(String message) { + this(message, null); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message the message + * @param cause the cause of this exception + */ + public ConnectionErrorException(String message, @Nullable Throwable cause) { + super(ErrorFrameCodec.CONNECTION_ERROR, message, cause); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java new file mode 100644 index 000000000..079b561f9 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; + +public class CustomRSocketException extends RSocketException { + private static final long serialVersionUID = 7873267740343446585L; + + /** + * Constructs a new exception with the specified message. + * + * @param errorCode customizable error code. Should be in range [0x00000301-0xFFFFFFFE] + * @param message the message + * @throws IllegalArgumentException if {@code errorCode} is out of allowed range + */ + public CustomRSocketException(int errorCode, String message) { + this(errorCode, message, null); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param errorCode customizable error code. Should be in range [0x00000301-0xFFFFFFFE] + * @param message the message + * @param cause the cause of this exception + * @throws IllegalArgumentException if {@code errorCode} is out of allowed range + */ + public CustomRSocketException(int errorCode, String message, @Nullable Throwable cause) { + super(errorCode, message, cause); + if (errorCode > ErrorFrameCodec.MAX_USER_ALLOWED_ERROR_CODE + && errorCode < ErrorFrameCodec.MIN_USER_ALLOWED_ERROR_CODE) { + throw new IllegalArgumentException( + "Allowed errorCode value should be in range [0x00000301-0xFFFFFFFE]", this); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java b/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java index 31ca46846..5c6eee614 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,49 +13,83 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.exceptions; -import static io.rsocket.frame.ErrorFrameFlyweight.*; +import static io.rsocket.frame.ErrorFrameCodec.APPLICATION_ERROR; +import static io.rsocket.frame.ErrorFrameCodec.CANCELED; +import static io.rsocket.frame.ErrorFrameCodec.CONNECTION_CLOSE; +import static io.rsocket.frame.ErrorFrameCodec.CONNECTION_ERROR; +import static io.rsocket.frame.ErrorFrameCodec.INVALID; +import static io.rsocket.frame.ErrorFrameCodec.INVALID_SETUP; +import static io.rsocket.frame.ErrorFrameCodec.MAX_USER_ALLOWED_ERROR_CODE; +import static io.rsocket.frame.ErrorFrameCodec.MIN_USER_ALLOWED_ERROR_CODE; +import static io.rsocket.frame.ErrorFrameCodec.REJECTED; +import static io.rsocket.frame.ErrorFrameCodec.REJECTED_RESUME; +import static io.rsocket.frame.ErrorFrameCodec.REJECTED_SETUP; +import static io.rsocket.frame.ErrorFrameCodec.UNSUPPORTED_SETUP; -import io.rsocket.Frame; -import io.rsocket.util.PayloadImpl; -import java.nio.charset.StandardCharsets; +import io.netty.buffer.ByteBuf; +import io.rsocket.RSocketErrorException; +import io.rsocket.frame.ErrorFrameCodec; +import java.util.Objects; -public class Exceptions { +/** Utility class that generates an exception from a frame. */ +public final class Exceptions { private Exceptions() {} - public static RuntimeException from(Frame frame) { - final int errorCode = Frame.Error.errorCode(frame); - - RuntimeException ex; - switch (errorCode) { - case APPLICATION_ERROR: - ex = new ApplicationException(new PayloadImpl(frame)); - break; - case CONNECTION_ERROR: - ex = new ConnectionException(StandardCharsets.UTF_8.decode(frame.getData()).toString()); - break; - case INVALID: - ex = new InvalidRequestException(StandardCharsets.UTF_8.decode(frame.getData()).toString()); - break; - case INVALID_SETUP: - ex = new InvalidSetupException(StandardCharsets.UTF_8.decode(frame.getData()).toString()); - break; - case REJECTED: - ex = new RejectedException(StandardCharsets.UTF_8.decode(frame.getData()).toString()); - break; - case REJECTED_SETUP: - ex = new RejectedSetupException(StandardCharsets.UTF_8.decode(frame.getData()).toString()); - break; - case UNSUPPORTED_SETUP: - ex = - new UnsupportedSetupException( - StandardCharsets.UTF_8.decode(frame.getData()).toString()); - break; - default: - ex = new InvalidRequestException("Invalid Error frame"); + /** + * Create a {@link RSocketErrorException} from a Frame that matches the error code it contains. + * + * @param frame the frame to retrieve the error code and message from + * @return a {@link RSocketErrorException} that matches the error code in the Frame + * @throws NullPointerException if {@code frame} is {@code null} + */ + public static RuntimeException from(int streamId, ByteBuf frame) { + Objects.requireNonNull(frame, "frame must not be null"); + + int errorCode = ErrorFrameCodec.errorCode(frame); + String message = ErrorFrameCodec.dataUtf8(frame); + + if (streamId == 0) { + switch (errorCode) { + case INVALID_SETUP: + return new InvalidSetupException(message); + case UNSUPPORTED_SETUP: + return new UnsupportedSetupException(message); + case REJECTED_SETUP: + return new RejectedSetupException(message); + case REJECTED_RESUME: + return new RejectedResumeException(message); + case CONNECTION_ERROR: + return new ConnectionErrorException(message); + case CONNECTION_CLOSE: + return new ConnectionCloseException(message); + default: + return new IllegalArgumentException( + String.format("Invalid Error frame in Stream ID 0: 0x%08X '%s'", errorCode, message)); + } + } else { + switch (errorCode) { + case APPLICATION_ERROR: + return new ApplicationErrorException(message); + case REJECTED: + return new RejectedException(message); + case CANCELED: + return new CanceledException(message); + case INVALID: + return new InvalidException(message); + default: + if (errorCode >= MIN_USER_ALLOWED_ERROR_CODE + || errorCode <= MAX_USER_ALLOWED_ERROR_CODE) { + return new CustomRSocketException(errorCode, message); + } + return new IllegalArgumentException( + String.format( + "Invalid Error frame in Stream ID %d: 0x%08X '%s'", + streamId, errorCode, message)); + } } - return ex; } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java new file mode 100644 index 000000000..a1b77b8dd --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; + +/** + * The request is invalid. + * + * @see Error + * Codes + */ +public final class InvalidException extends RSocketException { + + private static final long serialVersionUID = 8279420324864928243L; + + /** + * Constructs a new exception with the specified message. + * + * @param message the message + */ + public InvalidException(String message) { + this(message, null); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message the message + * @param cause the cause of this exception + */ + public InvalidException(String message, @Nullable Throwable cause) { + super(ErrorFrameCodec.INVALID, message, cause); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java index 87889e2e6..b0889c5a6 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,10 +13,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.exceptions; -public class InvalidSetupException extends SetupException { +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; + +/** + * The Setup frame is invalid for the server (it could be that the client is too recent for the old + * server). + * + * @see Error + * Codes + */ +public final class InvalidSetupException extends SetupException { + + private static final long serialVersionUID = -6816210006610385251L; + + /** + * Constructs a new exception with the specified message. + * + * @param message the message + */ public InvalidSetupException(String message) { - super(message); + this(message, null); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message the message + * @param cause the cause of this exception + */ + public InvalidSetupException(String message, @Nullable Throwable cause) { + super(ErrorFrameCodec.INVALID_SETUP, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RSocketException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RSocketException.java new file mode 100644 index 000000000..2b137282f --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RSocketException.java @@ -0,0 +1,63 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +import io.rsocket.RSocketErrorException; +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; + +/** + * The root of the RSocket exception hierarchy. + * + * @deprecated please use {@link RSocketErrorException} instead + */ +@Deprecated +public abstract class RSocketException extends RSocketErrorException { + + private static final long serialVersionUID = 2912815394105575423L; + + /** + * Constructs a new exception with the specified message and error code 0x201 (Application error). + * + * @param message the message + */ + public RSocketException(String message) { + this(message, null); + } + + /** + * Constructs a new exception with the specified message and cause and error code 0x201 + * (Application error). + * + * @param message the message + * @param cause the cause of this exception + */ + public RSocketException(String message, @Nullable Throwable cause) { + super(ErrorFrameCodec.APPLICATION_ERROR, message, cause); + } + + /** + * Constructs a new exception with the specified error code, message and cause. + * + * @param errorCode the RSocket protocol error code + * @param message the message + * @param cause the cause of this exception + */ + public RSocketException(int errorCode, String message, @Nullable Throwable cause) { + super(errorCode, message, cause); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java index e19ce6bd2..baed84e1b 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,10 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.exceptions; -public class RejectedException extends RuntimeException implements Retryable { +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; + +/** + * Despite being a valid request, the Responder decided to reject it. The Responder guarantees that + * it didn't process the request. The reason for the rejection is explained in the Error Data + * section. + * + * @see Error + * Codes + */ +public class RejectedException extends RSocketException implements Retryable { + + private static final long serialVersionUID = 3926231092835143715L; + + /** + * Constructs a new exception with the specified message. + * + * @param message the message + */ public RejectedException(String message) { - super(message); + this(message, null); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message the message + * @param cause the cause of this exception + */ + public RejectedException(String message, @Nullable Throwable cause) { + super(ErrorFrameCodec.REJECTED, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java new file mode 100644 index 000000000..8a99fcffb --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; + +/** + * The server rejected the resume, it can specify the reason in the payload. + * + * @see Error + * Codes + */ +public final class RejectedResumeException extends RSocketException { + + private static final long serialVersionUID = -873684362478544811L; + + /** + * Constructs a new exception with the specified message. + * + * @param message the message + */ + public RejectedResumeException(String message) { + this(message, null); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message the message + * @param cause the cause of this exception + */ + public RejectedResumeException(String message, @Nullable Throwable cause) { + super(ErrorFrameCodec.REJECTED_RESUME, message, cause); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java index 2d4a201c1..c09a27e32 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,10 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.exceptions; -public class RejectedSetupException extends SetupException implements Retryable { +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; + +/** + * The server rejected the setup, it can specify the reason in the payload. + * + * @see Error + * Codes + */ +public final class RejectedSetupException extends SetupException implements Retryable { + + private static final long serialVersionUID = 8757401529926371738L; + + /** + * Constructs a new exception with the specified message. + * + * @param message the message + */ public RejectedSetupException(String message) { - super(message); + this(message, null); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message the message + * @param cause the cause of this exception + */ + public RejectedSetupException(String message, @Nullable Throwable cause) { + super(ErrorFrameCodec.REJECTED_SETUP, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/Retryable.java b/rsocket-core/src/main/java/io/rsocket/exceptions/Retryable.java index 86611bd61..e61fe4f97 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/Retryable.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/Retryable.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,7 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.exceptions; -/** Marker interface only */ +/** + * Indicates that an exception is retryable. This interface is a marker and the strategy for + * retrying and operation that causes a {@link Retryable} to be thrown is not specified. + */ public interface Retryable {} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java index a2fb37109..ed979c9e6 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,10 +13,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.exceptions; -public abstract class SetupException extends RuntimeException { +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; + +/** The root of the setup exception hierarchy. */ +public abstract class SetupException extends RSocketException { + + private static final long serialVersionUID = -2928269501877732756L; + + /** + * Constructs a new exception with the specified message. + * + * @param message the message + * @deprecated please use {@link #SetupException(int, String, Throwable)} + */ + @Deprecated public SetupException(String message) { - super(message); + this(message, null); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message the message + * @param cause the cause of this exception + * @deprecated please use {@link #SetupException(int, String, Throwable)} + */ + @Deprecated + public SetupException(String message, @Nullable Throwable cause) { + this(ErrorFrameCodec.INVALID_SETUP, message, cause); + } + + /** + * Constructs a new exception with the specified error code, message and cause. + * + * @param errorCode the RSocket protocol code + * @param message the message + * @param cause the cause of this exception + */ + public SetupException(int errorCode, String message, @Nullable Throwable cause) { + super(errorCode, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java index 9b9f07fc9..7429ccd98 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,10 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.exceptions; -public class UnsupportedSetupException extends SetupException { +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; + +/** + * Some (or all) of the parameters specified by the client are unsupported by the server. + * + * @see Error + * Codes + */ +public final class UnsupportedSetupException extends SetupException { + + private static final long serialVersionUID = -1892507835635323415L; + + /** + * Constructs a new exception with the specified message. + * + * @param message the message + */ public UnsupportedSetupException(String message) { - super(message); + this(message, null); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message the message + * @param cause the cause of this exception + */ + public UnsupportedSetupException(String message, @Nullable Throwable cause) { + super(ErrorFrameCodec.UNSUPPORTED_SETUP, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/package-info.java b/rsocket-core/src/main/java/io/rsocket/exceptions/package-info.java index 5a29ce4ab..969aedded 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/package-info.java @@ -1,18 +1,26 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -@javax.annotation.ParametersAreNonnullByDefault +/** + * A hierarchy of exceptions that represent RSocket protocol error codes. + * + * @see Error + * Codes + */ +@NonNullApi package io.rsocket.exceptions; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java index 38c46b62c..84338d1df 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java @@ -1,117 +1,128 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket.fragmentation; -import io.netty.util.collection.IntObjectHashMap; +import static io.rsocket.fragmentation.FrameFragmenter.fragmentFrame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import io.rsocket.DuplexConnection; -import io.rsocket.Frame; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import java.util.Objects; import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -/** Fragments and Re-assembles frames. MTU is number of bytes per fragment. The default is 1024 */ -public class FragmentationDuplexConnection implements DuplexConnection { - - private final DuplexConnection source; - private final IntObjectHashMap frameReassemblers = new IntObjectHashMap<>(); - private final FrameFragmenter frameFragmenter; - - public FragmentationDuplexConnection(DuplexConnection source, int mtu) { - this.source = source; - this.frameFragmenter = new FrameFragmenter(mtu); - } - - public static int getDefaultMTU() { - if (Boolean.getBoolean("io.rsocket.fragmentation.enable")) { - return Integer.getInteger("io.rsocket.fragmentation.mtu", 1024); - } - - return 0; - } - - @Override - public double availability() { - return source.availability(); +/** + * A {@link DuplexConnection} implementation that fragments and reassembles {@link ByteBuf}s. + * + * @see Fragmentation + * and Reassembly + */ +public final class FragmentationDuplexConnection extends ReassemblyDuplexConnection + implements DuplexConnection { + + public static final int MIN_MTU_SIZE = 64; + + private static final Logger logger = LoggerFactory.getLogger(FragmentationDuplexConnection.class); + + final DuplexConnection delegate; + final int mtu; + final String type; + + /** + * Class constructor. + * + * @param delegate the underlying connection + * @param mtu the fragment size, greater than {@link #MIN_MTU_SIZE} + * @param maxInboundPayloadSize the maximum payload size, which can be reassembled from multiple + * fragments + * @param type a label to use for logging purposes + */ + public FragmentationDuplexConnection( + DuplexConnection delegate, int mtu, int maxInboundPayloadSize, String type) { + super(delegate, maxInboundPayloadSize); + + Objects.requireNonNull(delegate, "delegate must not be null"); + this.delegate = delegate; + this.mtu = assertMtu(mtu); + this.type = type; } - @Override - public Mono send(Publisher frames) { - return Flux.from(frames).concatMap(this::sendOne).then(); + private boolean shouldFragment(FrameType frameType, int readableBytes) { + return frameType.isFragmentable() && readableBytes > mtu; } - @Override - public Mono sendOne(Frame frame) { - if (frameFragmenter.shouldFragment(frame)) { - return source.send(frameFragmenter.fragment(frame)); + public static int assertMtu(int mtu) { + if (mtu > 0 && mtu < MIN_MTU_SIZE || mtu < 0) { + String msg = + String.format( + "The smallest allowed mtu size is %d bytes, provided: %d", MIN_MTU_SIZE, mtu); + throw new IllegalArgumentException(msg); } else { - return source.sendOne(frame); + return mtu; } } @Override - public Flux receive() { - return source - .receive() - .concatMap( - frame -> { - if (FrameHeaderFlyweight.FLAGS_F == (frame.flags() & FrameHeaderFlyweight.FLAGS_F)) { - FrameReassembler frameReassembler = getFrameReassembler(frame); - frameReassembler.append(frame); - return Mono.empty(); - } else if (frameReassemblersContain(frame.getStreamId())) { - FrameReassembler frameReassembler = removeFrameReassembler(frame.getStreamId()); - frameReassembler.append(frame); - Frame reassembled = frameReassembler.reassemble(); - return Mono.just(reassembled); - } else { - return Mono.just(frame); - } - }); + public Mono send(Publisher frames) { + return delegate.send( + Flux.from(frames) + .concatMap( + frame -> { + FrameType frameType = FrameHeaderCodec.frameType(frame); + int readableBytes = frame.readableBytes(); + if (!shouldFragment(frameType, readableBytes)) { + return Flux.just(frame); + } + + return logFragments(Flux.from(fragmentFrame(alloc(), mtu, frame, frameType))); + })); } @Override - public Mono close() { - return source.close(); - } - - private synchronized FrameReassembler getFrameReassembler(Frame frame) { - return frameReassemblers.computeIfAbsent(frame.getStreamId(), s -> new FrameReassembler(frame)); - } - - private synchronized FrameReassembler removeFrameReassembler(int streamId) { - return frameReassemblers.remove(streamId); - } - - private synchronized boolean frameReassemblersContain(int streamId) { - return frameReassemblers.containsKey(streamId); + public Mono sendOne(ByteBuf frame) { + FrameType frameType = FrameHeaderCodec.frameType(frame); + int readableBytes = frame.readableBytes(); + if (!shouldFragment(frameType, readableBytes)) { + return delegate.sendOne(frame); + } + Flux fragments = Flux.from(fragmentFrame(alloc(), mtu, frame, frameType)); + fragments = logFragments(fragments); + return delegate.send(fragments); } - @Override - public Mono onClose() { - return source - .onClose() - .doFinally( - s -> { - synchronized (FragmentationDuplexConnection.this) { - frameReassemblers.values().forEach(FrameReassembler::dispose); - - frameReassemblers.clear(); - } - }); + protected Flux logFragments(Flux fragments) { + if (logger.isDebugEnabled()) { + fragments = + fragments.doOnNext( + byteBuf -> { + logger.debug( + "{} - stream id {} - frame type {} - \n {}", + type, + FrameHeaderCodec.streamId(byteBuf), + FrameHeaderCodec.frameType(byteBuf), + ByteBufUtil.prettyHexDump(byteBuf)); + }); + } + return fragments; } } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java index 402380b03..fcb6198a3 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java @@ -1,111 +1,235 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket.fragmentation; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; -import io.rsocket.Frame; -import io.rsocket.FrameType; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.netty.util.ReferenceCountUtil; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; +import java.util.function.Consumer; +import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import reactor.core.publisher.SynchronousSink; -public class FrameFragmenter { - private final int mtu; +/** + * The implementation of the RSocket fragmentation behavior. + * + * @see Fragmentation + * and Reassembly + */ +final class FrameFragmenter { + static Publisher fragmentFrame( + ByteBufAllocator allocator, int mtu, final ByteBuf frame, FrameType frameType) { + ByteBuf metadata = getMetadata(frame, frameType); + ByteBuf data = getData(frame, frameType); + int streamId = FrameHeaderCodec.streamId(frame); + return Flux.generate( + new Consumer>() { + boolean first = true; - public FrameFragmenter(int mtu) { - this.mtu = mtu; - } + @Override + public void accept(SynchronousSink sink) { + ByteBuf byteBuf; + if (first) { + first = false; + byteBuf = + encodeFirstFragment( + allocator, mtu, frame, frameType, streamId, metadata, data); + } else { + byteBuf = encodeFollowsFragment(allocator, mtu, streamId, metadata, data); + } - public boolean shouldFragment(Frame frame) { - return isFragmentableFrame(frame.getType()) - && FrameHeaderFlyweight.payloadLength(frame.content()) > mtu; + sink.next(byteBuf); + if (!metadata.isReadable() && !data.isReadable()) { + sink.complete(); + } + } + }) + .doFinally(signalType -> ReferenceCountUtil.safeRelease(frame)); } - private boolean isFragmentableFrame(FrameType type) { - switch (type) { - case FIRE_AND_FORGET: + static ByteBuf encodeFirstFragment( + ByteBufAllocator allocator, + int mtu, + ByteBuf frame, + FrameType frameType, + int streamId, + ByteBuf metadata, + ByteBuf data) { + // subtract the header bytes + int remaining = mtu - FrameHeaderCodec.size(); + + // substract the initial request n + switch (frameType) { case REQUEST_STREAM: case REQUEST_CHANNEL: + remaining -= Integer.BYTES; + break; + default: + } + + ByteBuf metadataFragment = null; + if (metadata.isReadable()) { + // subtract the metadata frame length + remaining -= 3; + int r = Math.min(remaining, metadata.readableBytes()); + remaining -= r; + metadataFragment = metadata.readRetainedSlice(r); + } + + ByteBuf dataFragment = Unpooled.EMPTY_BUFFER; + if (remaining > 0 && data.isReadable()) { + int r = Math.min(remaining, data.readableBytes()); + dataFragment = data.readRetainedSlice(r); + } + + switch (frameType) { + case REQUEST_FNF: + return RequestFireAndForgetFrameCodec.encode( + allocator, streamId, true, metadataFragment, dataFragment); + case REQUEST_STREAM: + return RequestStreamFrameCodec.encode( + allocator, + streamId, + true, + RequestStreamFrameCodec.initialRequestN(frame), + metadataFragment, + dataFragment); case REQUEST_RESPONSE: + return RequestResponseFrameCodec.encode( + allocator, streamId, true, metadataFragment, dataFragment); + case REQUEST_CHANNEL: + return RequestChannelFrameCodec.encode( + allocator, + streamId, + true, + false, + RequestChannelFrameCodec.initialRequestN(frame), + metadataFragment, + dataFragment); + // Payload and synthetic types case PAYLOAD: + return PayloadFrameCodec.encode( + allocator, streamId, true, false, false, metadataFragment, dataFragment); + case NEXT: + return PayloadFrameCodec.encode( + allocator, streamId, true, false, true, metadataFragment, dataFragment); case NEXT_COMPLETE: - case METADATA_PUSH: - return true; + return PayloadFrameCodec.encode( + allocator, streamId, true, true, true, metadataFragment, dataFragment); + case COMPLETE: + return PayloadFrameCodec.encode( + allocator, streamId, true, true, false, metadataFragment, dataFragment); default: - return false; + throw new IllegalStateException("unsupported fragment type: " + frameType); } } - public Flux fragment(Frame frame) { - final FrameType frameType = frame.getType(); - final int streamId = frame.getStreamId(); - final int flags = frame.flags() & ~FrameHeaderFlyweight.FLAGS_F & ~FrameHeaderFlyweight.FLAGS_M; - final ByteBuf metadata = FrameHeaderFlyweight.sliceFrameMetadata(frame.content()); - final ByteBuf data = FrameHeaderFlyweight.sliceFrameData(frame.content()); - frame.retain(); + static ByteBuf encodeFollowsFragment( + ByteBufAllocator allocator, int mtu, int streamId, ByteBuf metadata, ByteBuf data) { + // subtract the header bytes + int remaining = mtu - FrameHeaderCodec.size(); - return Flux.generate( - sink -> { - final int metadataLength = metadata.readableBytes(); - final int dataLength = data.readableBytes(); - - if (metadataLength > mtu) { - sink.next( - Frame.PayloadFrame.from( - streamId, - frameType, - metadata.readSlice(mtu), - Unpooled.EMPTY_BUFFER, - flags | FrameHeaderFlyweight.FLAGS_M | FrameHeaderFlyweight.FLAGS_F)); - } else if (metadataLength > 0) { - if (dataLength > mtu - metadataLength) { - sink.next( - Frame.PayloadFrame.from( - streamId, - frameType, - metadata.readSlice(metadataLength), - data.readSlice(mtu - metadataLength), - flags | FrameHeaderFlyweight.FLAGS_M | FrameHeaderFlyweight.FLAGS_F)); - } else { - sink.next( - Frame.PayloadFrame.from( - streamId, - frameType, - metadata.readSlice(metadataLength), - data.readSlice(dataLength), - flags | FrameHeaderFlyweight.FLAGS_M)); - frame.release(); - sink.complete(); - } - } else if (dataLength > mtu) { - sink.next( - Frame.PayloadFrame.from( - streamId, - frameType, - Unpooled.EMPTY_BUFFER, - data.readSlice(mtu), - flags | FrameHeaderFlyweight.FLAGS_F)); - } else { - sink.next( - Frame.PayloadFrame.from( - streamId, frameType, Unpooled.EMPTY_BUFFER, data.readSlice(dataLength), flags)); - frame.release(); - sink.complete(); - } - }); + ByteBuf metadataFragment = null; + if (metadata.isReadable()) { + // subtract the metadata frame length + remaining -= 3; + int r = Math.min(remaining, metadata.readableBytes()); + remaining -= r; + metadataFragment = metadata.readRetainedSlice(r); + } + + ByteBuf dataFragment = Unpooled.EMPTY_BUFFER; + if (remaining > 0 && data.isReadable()) { + int r = Math.min(remaining, data.readableBytes()); + dataFragment = data.readRetainedSlice(r); + } + + boolean follows = data.isReadable() || metadata.isReadable(); + return PayloadFrameCodec.encode( + allocator, streamId, follows, false, true, metadataFragment, dataFragment); + } + + static ByteBuf getMetadata(ByteBuf frame, FrameType frameType) { + boolean hasMetadata = FrameHeaderCodec.hasMetadata(frame); + if (hasMetadata) { + ByteBuf metadata; + switch (frameType) { + case REQUEST_FNF: + metadata = RequestFireAndForgetFrameCodec.metadata(frame); + break; + case REQUEST_STREAM: + metadata = RequestStreamFrameCodec.metadata(frame); + break; + case REQUEST_RESPONSE: + metadata = RequestResponseFrameCodec.metadata(frame); + break; + case REQUEST_CHANNEL: + metadata = RequestChannelFrameCodec.metadata(frame); + break; + // Payload and synthetic types + case PAYLOAD: + case NEXT: + case NEXT_COMPLETE: + case COMPLETE: + metadata = PayloadFrameCodec.metadata(frame); + break; + default: + throw new IllegalStateException("unsupported fragment type"); + } + return metadata; + } else { + return Unpooled.EMPTY_BUFFER; + } + } + + static ByteBuf getData(ByteBuf frame, FrameType frameType) { + ByteBuf data; + switch (frameType) { + case REQUEST_FNF: + data = RequestFireAndForgetFrameCodec.data(frame); + break; + case REQUEST_STREAM: + data = RequestStreamFrameCodec.data(frame); + break; + case REQUEST_RESPONSE: + data = RequestResponseFrameCodec.data(frame); + break; + case REQUEST_CHANNEL: + data = RequestChannelFrameCodec.data(frame); + break; + // Payload and synthetic types + case PAYLOAD: + case NEXT: + case NEXT_COMPLETE: + case COMPLETE: + data = PayloadFrameCodec.data(frame); + break; + default: + throw new IllegalStateException("unsupported fragment type"); + } + return data; } } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java index 57ebd3ebe..fbb984666 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java @@ -1,71 +1,356 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket.fragmentation; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; -import io.netty.buffer.PooledByteBufAllocator; -import io.rsocket.Frame; -import io.rsocket.FrameType; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.collection.IntObjectMap; +import io.rsocket.frame.FragmentationCodec; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; +import java.util.concurrent.atomic.AtomicBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.Disposable; +import reactor.core.publisher.SynchronousSink; +import reactor.util.annotation.Nullable; -/** Assembles Fragmented frames. */ -public class FrameReassembler implements Disposable { - private final FrameType frameType; - private final int streamId; - private final int flags; - private final CompositeByteBuf dataBuffer; - private final CompositeByteBuf metadataBuffer; - - public FrameReassembler(Frame frame) { - this.frameType = frame.getType(); - this.streamId = frame.getStreamId(); - this.flags = frame.flags(); - dataBuffer = PooledByteBufAllocator.DEFAULT.compositeBuffer(); - metadataBuffer = PooledByteBufAllocator.DEFAULT.compositeBuffer(); - } - - public synchronized void append(Frame frame) { - final ByteBuf byteBuf = frame.content(); - final FrameType frameType = FrameHeaderFlyweight.frameType(byteBuf); - final int frameLength = FrameHeaderFlyweight.frameLength(byteBuf); - final int metadataLength = FrameHeaderFlyweight.metadataLength(byteBuf, frameType, frameLength); - final int dataLength = FrameHeaderFlyweight.dataLength(byteBuf, frameType); - if (0 < metadataLength) { - int metadataOffset = FrameHeaderFlyweight.metadataOffset(byteBuf); - if (FrameHeaderFlyweight.hasMetadataLengthField(frameType)) { - metadataOffset += FrameHeaderFlyweight.FRAME_LENGTH_SIZE; +/** + * The implementation of the RSocket reassembly behavior. + * + * @see Fragmentation + * and Reassembly + */ +final class FrameReassembler extends AtomicBoolean implements Disposable { + + private static final long serialVersionUID = -4394598098863449055L; + + private static final Logger logger = LoggerFactory.getLogger(FrameReassembler.class); + + final IntObjectMap headers; + final IntObjectMap metadata; + final IntObjectMap data; + + final ByteBufAllocator allocator; + final int maxInboundPayloadSize; + + public FrameReassembler(ByteBufAllocator allocator, int maxInboundPayloadSize) { + this.allocator = allocator; + this.maxInboundPayloadSize = maxInboundPayloadSize; + this.headers = new IntObjectHashMap<>(); + this.metadata = new IntObjectHashMap<>(); + this.data = new IntObjectHashMap<>(); + } + + @Override + public void dispose() { + if (compareAndSet(false, true)) { + synchronized (FrameReassembler.this) { + for (ByteBuf byteBuf : headers.values()) { + ReferenceCountUtil.safeRelease(byteBuf); + } + headers.clear(); + + for (ByteBuf byteBuf : metadata.values()) { + ReferenceCountUtil.safeRelease(byteBuf); + } + metadata.clear(); + + for (ByteBuf byteBuf : data.values()) { + ReferenceCountUtil.safeRelease(byteBuf); + } + data.clear(); } - metadataBuffer.addComponent(true, byteBuf.retainedSlice(metadataOffset, metadataLength)); } - if (0 < dataLength) { - final int dataOffset = FrameHeaderFlyweight.dataOffset(byteBuf, frameType, frameLength); - dataBuffer.addComponent(true, byteBuf.retainedSlice(dataOffset, dataLength)); + } + + @Override + public boolean isDisposed() { + return get(); + } + + @Nullable + synchronized ByteBuf getHeader(int streamId) { + return headers.get(streamId); + } + + synchronized CompositeByteBuf getMetadata(int streamId) { + CompositeByteBuf byteBuf = metadata.get(streamId); + + if (byteBuf == null) { + byteBuf = allocator.compositeBuffer(); + metadata.put(streamId, byteBuf); } + + return byteBuf; } - public synchronized Frame reassemble() { - return Frame.PayloadFrame.from(streamId, frameType, metadataBuffer, dataBuffer, flags); + synchronized int getMetadataSize(int streamId) { + CompositeByteBuf byteBuf = metadata.get(streamId); + + if (byteBuf == null) { + return 0; + } + + return byteBuf.readableBytes(); } - @Override - public void dispose() { - dataBuffer.release(); - metadataBuffer.release(); + synchronized CompositeByteBuf getData(int streamId) { + CompositeByteBuf byteBuf = data.get(streamId); + + if (byteBuf == null) { + byteBuf = allocator.compositeBuffer(); + data.put(streamId, byteBuf); + } + + return byteBuf; + } + + synchronized int getDataSize(int streamId) { + CompositeByteBuf byteBuf = data.get(streamId); + + if (byteBuf == null) { + return 0; + } + + return byteBuf.readableBytes(); + } + + @Nullable + synchronized ByteBuf removeHeader(int streamId) { + return headers.remove(streamId); + } + + @Nullable + synchronized CompositeByteBuf removeMetadata(int streamId) { + return metadata.remove(streamId); + } + + @Nullable + synchronized CompositeByteBuf removeData(int streamId) { + return data.remove(streamId); + } + + synchronized void putHeader(int streamId, ByteBuf header) { + headers.put(streamId, header); + } + + void cancelAssemble(int streamId) { + ByteBuf header = removeHeader(streamId); + CompositeByteBuf metadata = removeMetadata(streamId); + CompositeByteBuf data = removeData(streamId); + + if (header != null) { + ReferenceCountUtil.safeRelease(header); + } + + if (metadata != null) { + ReferenceCountUtil.safeRelease(metadata); + } + + if (data != null) { + ReferenceCountUtil.safeRelease(data); + } + } + + void handleNoFollowsFlag(ByteBuf frame, SynchronousSink sink, int streamId) { + ByteBuf header = removeHeader(streamId); + if (header != null) { + + int maxReassemblySize = this.maxInboundPayloadSize; + if (maxReassemblySize != Integer.MAX_VALUE) { + int currentPayloadSize = getMetadataSize(streamId) + getDataSize(streamId); + if (currentPayloadSize + frame.readableBytes() - FrameHeaderCodec.size() + > maxReassemblySize) { + frame.release(); + throw new IllegalStateException("Reassembled payload went out of allowed size"); + } + } + + if (FrameHeaderCodec.hasMetadata(header)) { + ByteBuf assembledFrame = assembleFrameWithMetadata(frame, streamId, header); + sink.next(assembledFrame); + } else { + ByteBuf data = assembleData(frame, streamId); + ByteBuf assembledFrame = FragmentationCodec.encode(allocator, header, data); + sink.next(assembledFrame); + } + frame.release(); + } else { + sink.next(frame); + } + } + + void handleFollowsFlag(ByteBuf frame, int streamId, FrameType frameType) { + + int maxReassemblySize = this.maxInboundPayloadSize; + if (maxReassemblySize != Integer.MAX_VALUE) { + int currentPayloadSize = getMetadataSize(streamId) + getDataSize(streamId); + if (currentPayloadSize + frame.readableBytes() - FrameHeaderCodec.size() + > maxReassemblySize) { + frame.release(); + throw new IllegalStateException("Reassembled payload went out of allowed size"); + } + } + + ByteBuf header = getHeader(streamId); + if (header == null) { + header = frame.copy(frame.readerIndex(), FrameHeaderCodec.size()); + + if (frameType == FrameType.REQUEST_CHANNEL || frameType == FrameType.REQUEST_STREAM) { + long i = RequestChannelFrameCodec.initialRequestN(frame); + header.writeInt(i > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) i); + } + putHeader(streamId, header); + } + + ByteBuf metadata = null; + if (FrameHeaderCodec.hasMetadata(frame)) { + switch (frameType) { + case REQUEST_FNF: + metadata = RequestFireAndForgetFrameCodec.metadata(frame); + break; + case REQUEST_STREAM: + metadata = RequestStreamFrameCodec.metadata(frame); + break; + case REQUEST_RESPONSE: + metadata = RequestResponseFrameCodec.metadata(frame); + break; + case REQUEST_CHANNEL: + metadata = RequestChannelFrameCodec.metadata(frame); + break; + // Payload and synthetic types + case PAYLOAD: + case NEXT: + case NEXT_COMPLETE: + case COMPLETE: + metadata = PayloadFrameCodec.metadata(frame); + break; + default: + throw new IllegalStateException("unsupported fragment type"); + } + if (metadata != null) { + getMetadata(streamId).addComponents(true, metadata.retain()); + } + } + + ByteBuf data; + switch (frameType) { + case REQUEST_FNF: + data = RequestFireAndForgetFrameCodec.data(frame).retain(); + break; + case REQUEST_STREAM: + data = RequestStreamFrameCodec.data(frame).retain(); + break; + case REQUEST_RESPONSE: + data = RequestResponseFrameCodec.data(frame).retain(); + break; + case REQUEST_CHANNEL: + data = RequestChannelFrameCodec.data(frame).retain(); + break; + // Payload and synthetic types + case PAYLOAD: + case NEXT: + case NEXT_COMPLETE: + case COMPLETE: + data = PayloadFrameCodec.data(frame).retain(); + break; + default: + frame.release(); + throw new IllegalStateException("unsupported fragment type"); + } + + getData(streamId).addComponents(true, data); + frame.release(); + + if ((metadata != null && metadata.readableBytes() == 0) && data.readableBytes() == 0) { + throw new IllegalStateException("Empty frame."); + } + } + + void reassembleFrame(ByteBuf frame, SynchronousSink sink) { + try { + FrameType frameType = FrameHeaderCodec.frameType(frame); + int streamId = FrameHeaderCodec.streamId(frame); + switch (frameType) { + case CANCEL: + case ERROR: + cancelAssemble(streamId); + } + + if (!frameType.isFragmentable()) { + sink.next(frame); + return; + } + + boolean hasFollows = FrameHeaderCodec.hasFollows(frame); + + if (hasFollows) { + handleFollowsFlag(frame, streamId, frameType); + } else { + handleNoFollowsFlag(frame, sink, streamId); + } + + } catch (Throwable t) { + logger.error("error reassemble frame", t); + sink.error(t); + } + } + + private ByteBuf assembleFrameWithMetadata(ByteBuf frame, int streamId, ByteBuf header) { + ByteBuf metadata; + CompositeByteBuf cm = removeMetadata(streamId); + + ByteBuf decodedMetadata = PayloadFrameCodec.metadata(frame); + if (decodedMetadata != null) { + if (cm != null) { + metadata = cm.addComponents(true, decodedMetadata.retain()); + } else { + metadata = PayloadFrameCodec.metadata(frame).retain(); + } + } else { + metadata = cm; + } + + ByteBuf data = assembleData(frame, streamId); + + return FragmentationCodec.encode(allocator, header, metadata, data); + } + + private ByteBuf assembleData(ByteBuf frame, int streamId) { + ByteBuf data; + CompositeByteBuf cd = removeData(streamId); + if (cd != null) { + cd.addComponents(true, PayloadFrameCodec.data(frame).retain()); + data = cd; + } else { + data = Unpooled.EMPTY_BUFFER; + } + + return data; } } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java new file mode 100644 index 000000000..03f97c75d --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java @@ -0,0 +1,89 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.fragmentation; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.frame.FrameLengthCodec; +import java.util.Objects; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * A {@link DuplexConnection} implementation that reassembles {@link ByteBuf}s. + * + * @see Fragmentation + * and Reassembly + */ +public class ReassemblyDuplexConnection implements DuplexConnection { + private final DuplexConnection delegate; + private final FrameReassembler frameReassembler; + + /** Constructor with the underlying delegate to receive frames from. */ + public ReassemblyDuplexConnection(DuplexConnection delegate, int maxInboundPayloadSize) { + Objects.requireNonNull(delegate, "delegate must not be null"); + this.delegate = delegate; + this.frameReassembler = new FrameReassembler(delegate.alloc(), maxInboundPayloadSize); + + delegate.onClose().doFinally(s -> frameReassembler.dispose()).subscribe(); + } + + public static int assertInboundPayloadSize(int inboundPayloadSize) { + if (inboundPayloadSize < FragmentationDuplexConnection.MIN_MTU_SIZE) { + String msg = + String.format( + "The min allowed inboundPayloadSize size is %d bytes, provided: %d", + FrameLengthCodec.FRAME_LENGTH_MASK, inboundPayloadSize); + throw new IllegalArgumentException(msg); + } else { + return inboundPayloadSize; + } + } + + @Override + public Mono send(Publisher frames) { + return delegate.send(frames); + } + + @Override + public Mono sendOne(ByteBuf frame) { + return delegate.sendOne(frame); + } + + @Override + public Flux receive() { + return delegate.receive().handle(frameReassembler::reassembleFrame); + } + + @Override + public ByteBufAllocator alloc() { + return delegate.alloc(); + } + + @Override + public Mono onClose() { + return delegate.onClose(); + } + + @Override + public void dispose() { + delegate.dispose(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/package-info.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/package-info.java index d4b5244b1..8cc3fb41a 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/package-info.java @@ -1,18 +1,27 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -@javax.annotation.ParametersAreNonnullByDefault +/** + * Support for frame fragmentation and reassembly. + * + * @see Fragmentation + * and Reassembly + */ +@NonNullApi package io.rsocket.fragmentation; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/CancelFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/CancelFrameCodec.java new file mode 100644 index 000000000..d0d929f0f --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/CancelFrameCodec.java @@ -0,0 +1,12 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; + +public class CancelFrameCodec { + private CancelFrameCodec() {} + + public static ByteBuf encode(final ByteBufAllocator allocator, final int streamId) { + return FrameHeaderCodec.encode(allocator, streamId, FrameType.CANCEL, 0); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameCodec.java new file mode 100644 index 000000000..dcacb57dc --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameCodec.java @@ -0,0 +1,66 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.rsocket.RSocketErrorException; +import java.nio.charset.StandardCharsets; + +public class ErrorFrameCodec { + + // defined zero stream id error codes + public static final int INVALID_SETUP = 0x00000001; + public static final int UNSUPPORTED_SETUP = 0x00000002; + public static final int REJECTED_SETUP = 0x00000003; + public static final int REJECTED_RESUME = 0x00000004; + public static final int CONNECTION_ERROR = 0x00000101; + public static final int CONNECTION_CLOSE = 0x00000102; + // defined non-zero stream id error codes + public static final int APPLICATION_ERROR = 0x00000201; + public static final int REJECTED = 0x00000202; + public static final int CANCELED = 0x00000203; + public static final int INVALID = 0x00000204; + // defined user-allowed error codes range + public static final int MIN_USER_ALLOWED_ERROR_CODE = 0x00000301; + public static final int MAX_USER_ALLOWED_ERROR_CODE = 0xFFFFFFFE; + + public static ByteBuf encode( + ByteBufAllocator allocator, int streamId, Throwable t, ByteBuf data) { + ByteBuf header = FrameHeaderCodec.encode(allocator, streamId, FrameType.ERROR, 0); + + int errorCode = + t instanceof RSocketErrorException + ? ((RSocketErrorException) t).errorCode() + : APPLICATION_ERROR; + + header.writeInt(errorCode); + + return allocator.compositeBuffer(2).addComponents(true, header, data); + } + + public static ByteBuf encode(ByteBufAllocator allocator, int streamId, Throwable t) { + String message = t.getMessage() == null ? "" : t.getMessage(); + ByteBuf data = ByteBufUtil.writeUtf8(allocator, message); + return encode(allocator, streamId, t, data); + } + + public static int errorCode(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size()); + int i = byteBuf.readInt(); + byteBuf.resetReaderIndex(); + return i; + } + + public static ByteBuf data(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size() + Integer.BYTES); + ByteBuf slice = byteBuf.slice(); + byteBuf.resetReaderIndex(); + return slice; + } + + public static String dataUtf8(ByteBuf byteBuf) { + return data(byteBuf).toString(StandardCharsets.UTF_8); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java deleted file mode 100644 index 0f5029430..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.rsocket.FrameType; -import io.rsocket.exceptions.ApplicationException; -import io.rsocket.exceptions.CancelException; -import io.rsocket.exceptions.ConnectionException; -import io.rsocket.exceptions.InvalidRequestException; -import io.rsocket.exceptions.InvalidSetupException; -import io.rsocket.exceptions.RejectedException; -import io.rsocket.exceptions.RejectedSetupException; -import io.rsocket.exceptions.UnsupportedSetupException; - -public class ErrorFrameFlyweight { - - private ErrorFrameFlyweight() {} - - // defined error codes - public static final int INVALID_SETUP = 0x00000001; - public static final int UNSUPPORTED_SETUP = 0x00000002; - public static final int REJECTED_SETUP = 0x00000003; - public static final int REJECTED_RESUME = 0x00000004; - public static final int CONNECTION_ERROR = 0x00000101; - public static final int CONNECTION_CLOSE = 0x00000102; - public static final int APPLICATION_ERROR = 0x00000201; - public static final int REJECTED = 0x00000202; - public static final int CANCEL = 0x00000203; - public static final int INVALID = 0x00000204; - - // relative to start of passed offset - private static final int ERROR_CODE_FIELD_OFFSET = FrameHeaderFlyweight.FRAME_HEADER_LENGTH; - private static final int PAYLOAD_OFFSET = ERROR_CODE_FIELD_OFFSET + Integer.BYTES; - - public static int computeFrameLength(final int metadataLength, final int dataLength) { - int length = - FrameHeaderFlyweight.computeFrameHeaderLength(FrameType.ERROR, metadataLength, dataLength); - return length + Integer.BYTES; - } - - public static int encode( - final ByteBuf byteBuf, - final int streamId, - final int errorCode, - final ByteBuf metadata, - final ByteBuf data) { - final int frameLength = computeFrameLength(metadata.readableBytes(), data.readableBytes()); - - int length = - FrameHeaderFlyweight.encodeFrameHeader(byteBuf, frameLength, 0, FrameType.ERROR, streamId); - - byteBuf.setInt(ERROR_CODE_FIELD_OFFSET, errorCode); - length += Integer.BYTES; - - length += FrameHeaderFlyweight.encodeMetadata(byteBuf, FrameType.ERROR, length, metadata); - length += FrameHeaderFlyweight.encodeData(byteBuf, length, data); - - return length; - } - - public static int errorCodeFromException(Throwable ex) { - if (ex instanceof InvalidSetupException) { - return INVALID_SETUP; - } else if (ex instanceof UnsupportedSetupException) { - return UNSUPPORTED_SETUP; - } else if (ex instanceof RejectedSetupException) { - return REJECTED_SETUP; - } else if (ex instanceof ConnectionException) { - return CONNECTION_ERROR; - } else if (ex instanceof InvalidRequestException) { - return INVALID; - } else if (ex instanceof ApplicationException) { - return APPLICATION_ERROR; - } else if (ex instanceof RejectedException) { - return REJECTED; - } else if (ex instanceof CancelException) { - return CANCEL; - } - return INVALID; - } - - public static int errorCode(final ByteBuf byteBuf) { - return byteBuf.getInt(ERROR_CODE_FIELD_OFFSET); - } - - public static int payloadOffset(final ByteBuf byteBuf) { - return FrameHeaderFlyweight.FRAME_HEADER_LENGTH + Integer.BYTES; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameCodec.java new file mode 100644 index 000000000..418926596 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameCodec.java @@ -0,0 +1,67 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import reactor.util.annotation.Nullable; + +public class ExtensionFrameCodec { + private ExtensionFrameCodec() {} + + public static ByteBuf encode( + ByteBufAllocator allocator, + int streamId, + int extendedType, + @Nullable ByteBuf metadata, + ByteBuf data) { + + final boolean hasMetadata = metadata != null; + + int flags = FrameHeaderCodec.FLAGS_I; + + if (hasMetadata) { + flags |= FrameHeaderCodec.FLAGS_M; + } + + final ByteBuf header = FrameHeaderCodec.encode(allocator, streamId, FrameType.EXT, flags); + header.writeInt(extendedType); + + return FrameBodyCodec.encode(allocator, header, metadata, hasMetadata, data); + } + + public static int extendedType(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.EXT, byteBuf); + byteBuf.markReaderIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size()); + int i = byteBuf.readInt(); + byteBuf.resetReaderIndex(); + return i; + } + + public static ByteBuf data(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.EXT, byteBuf); + + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); + byteBuf.markReaderIndex(); + // Extended type + byteBuf.skipBytes(FrameHeaderCodec.size() + Integer.BYTES); + ByteBuf data = FrameBodyCodec.dataWithoutMarking(byteBuf, hasMetadata); + byteBuf.resetReaderIndex(); + return data; + } + + @Nullable + public static ByteBuf metadata(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.EXT, byteBuf); + + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); + if (!hasMetadata) { + return null; + } + byteBuf.markReaderIndex(); + // Extended type + byteBuf.skipBytes(FrameHeaderCodec.size() + Integer.BYTES); + ByteBuf metadata = FrameBodyCodec.metadataWithoutMarking(byteBuf); + byteBuf.resetReaderIndex(); + return metadata; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FragmentationCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/FragmentationCodec.java new file mode 100644 index 000000000..de228b271 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/FragmentationCodec.java @@ -0,0 +1,19 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import reactor.util.annotation.Nullable; + +/** FragmentationFlyweight is used to re-assemble frames */ +public class FragmentationCodec { + public static ByteBuf encode(final ByteBufAllocator allocator, ByteBuf header, ByteBuf data) { + return encode(allocator, header, null, data); + } + + public static ByteBuf encode( + final ByteBufAllocator allocator, ByteBuf header, @Nullable ByteBuf metadata, ByteBuf data) { + + final boolean hasMetadata = metadata != null; + return FrameBodyCodec.encode(allocator, header, metadata, hasMetadata, data); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameBodyCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameBodyCodec.java new file mode 100644 index 000000000..ea011e503 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameBodyCodec.java @@ -0,0 +1,103 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import reactor.util.annotation.Nullable; + +class FrameBodyCodec { + public static final int FRAME_LENGTH_MASK = 0xFFFFFF; + + private FrameBodyCodec() {} + + private static void encodeLength(final ByteBuf byteBuf, final int length) { + if ((length & ~FRAME_LENGTH_MASK) != 0) { + throw new IllegalArgumentException("Length is larger than 24 bits"); + } + // Write each byte separately in reverse order, this mean we can write 1 << 23 without + // overflowing. + byteBuf.writeByte(length >> 16); + byteBuf.writeByte(length >> 8); + byteBuf.writeByte(length); + } + + private static int decodeLength(final ByteBuf byteBuf) { + byte b = byteBuf.readByte(); + int length = (b & 0xFF) << 16; + byte b1 = byteBuf.readByte(); + length |= (b1 & 0xFF) << 8; + byte b2 = byteBuf.readByte(); + length |= b2 & 0xFF; + return length; + } + + static ByteBuf encode( + ByteBufAllocator allocator, + final ByteBuf header, + @Nullable ByteBuf metadata, + boolean hasMetadata, + @Nullable ByteBuf data) { + + final boolean addData; + if (data != null) { + if (data.isReadable()) { + addData = true; + } else { + // even though there is nothing to read, we still have to release here since nobody else + // going to do soo + data.release(); + addData = false; + } + } else { + addData = false; + } + + final boolean addMetadata; + if (hasMetadata) { + if (metadata.isReadable()) { + addMetadata = true; + } else { + // even though there is nothing to read, we still have to release here since nobody else + // going to do soo + metadata.release(); + addMetadata = false; + } + } else { + // has no metadata means it is null, thus no need to release anything + addMetadata = false; + } + + if (hasMetadata) { + int length = metadata.readableBytes(); + encodeLength(header, length); + } + + if (addMetadata && addData) { + return allocator.compositeBuffer(3).addComponents(true, header, metadata, data); + } else if (addMetadata) { + return allocator.compositeBuffer(2).addComponents(true, header, metadata); + } else if (addData) { + return allocator.compositeBuffer(2).addComponents(true, header, data); + } else { + return header; + } + } + + static ByteBuf metadataWithoutMarking(ByteBuf byteBuf) { + int length = decodeLength(byteBuf); + return byteBuf.readSlice(length); + } + + static ByteBuf dataWithoutMarking(ByteBuf byteBuf, boolean hasMetadata) { + if (hasMetadata) { + /*moves reader index*/ + int length = decodeLength(byteBuf); + byteBuf.skipBytes(length); + } + if (byteBuf.readableBytes() > 0) { + return byteBuf.readSlice(byteBuf.readableBytes()); + } else { + return Unpooled.EMPTY_BUFFER; + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderCodec.java new file mode 100644 index 000000000..28f39459d --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderCodec.java @@ -0,0 +1,136 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import org.reactivestreams.Subscriber; + +/** + * Per connection frame flyweight. + * + *

Not the latest frame layout, but close. Does not include - fragmentation / reassembly - encode + * should remove Type param and have it as part of method name (1 encode per type?) + * + *

Not thread-safe. Assumed to be used single-threaded + */ +public final class FrameHeaderCodec { + /** (I)gnore flag: a value of 0 indicates the protocol can't ignore this frame */ + public static final int FLAGS_I = 0b10_0000_0000; + /** (M)etadata flag: a value of 1 indicates the frame contains metadata */ + public static final int FLAGS_M = 0b01_0000_0000; + /** + * (F)ollows: More fragments follow this fragment (in case of fragmented REQUEST_x or PAYLOAD + * frames) + */ + public static final int FLAGS_F = 0b00_1000_0000; + /** (C)omplete: bit to indicate stream completion ({@link Subscriber#onComplete()}) */ + public static final int FLAGS_C = 0b00_0100_0000; + /** (N)ext: bit to indicate payload or metadata present ({@link Subscriber#onNext(Object)}) */ + public static final int FLAGS_N = 0b00_0010_0000; + + public static final String DISABLE_FRAME_TYPE_CHECK = "io.rsocket.frames.disableFrameTypeCheck"; + private static final int FRAME_FLAGS_MASK = 0b0000_0011_1111_1111; + private static final int FRAME_TYPE_BITS = 6; + private static final int FRAME_TYPE_SHIFT = 16 - FRAME_TYPE_BITS; + private static final int HEADER_SIZE = Integer.BYTES + Short.BYTES; + private static boolean disableFrameTypeCheck; + + static { + disableFrameTypeCheck = Boolean.getBoolean(DISABLE_FRAME_TYPE_CHECK); + } + + private FrameHeaderCodec() {} + + static ByteBuf encodeStreamZero( + final ByteBufAllocator allocator, final FrameType frameType, int flags) { + return encode(allocator, 0, frameType, flags); + } + + public static ByteBuf encode( + final ByteBufAllocator allocator, final int streamId, final FrameType frameType, int flags) { + if (!frameType.canHaveMetadata() && ((flags & FLAGS_M) == FLAGS_M)) { + throw new IllegalStateException("bad value for metadata flag"); + } + + short typeAndFlags = (short) (frameType.getEncodedType() << FRAME_TYPE_SHIFT | (short) flags); + + return allocator.buffer().writeInt(streamId).writeShort(typeAndFlags); + } + + public static boolean hasFollows(ByteBuf byteBuf) { + return (flags(byteBuf) & FLAGS_F) == FLAGS_F; + } + + public static int streamId(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + int streamId = byteBuf.readInt(); + byteBuf.resetReaderIndex(); + return streamId; + } + + public static int flags(final ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + byteBuf.skipBytes(Integer.BYTES); + short typeAndFlags = byteBuf.readShort(); + byteBuf.resetReaderIndex(); + return typeAndFlags & FRAME_FLAGS_MASK; + } + + public static boolean hasMetadata(ByteBuf byteBuf) { + return (flags(byteBuf) & FLAGS_M) == FLAGS_M; + } + + /** + * faster version of {@link #frameType(ByteBuf)} which does not replace PAYLOAD with synthetic + * type + */ + public static FrameType nativeFrameType(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + byteBuf.skipBytes(Integer.BYTES); + int typeAndFlags = byteBuf.readShort() & 0xFFFF; + FrameType result = FrameType.fromEncodedType(typeAndFlags >> FRAME_TYPE_SHIFT); + byteBuf.resetReaderIndex(); + return result; + } + + public static FrameType frameType(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + byteBuf.skipBytes(Integer.BYTES); + int typeAndFlags = byteBuf.readShort() & 0xFFFF; + + FrameType result = FrameType.fromEncodedType(typeAndFlags >> FRAME_TYPE_SHIFT); + + if (FrameType.PAYLOAD == result) { + final int flags = typeAndFlags & FRAME_FLAGS_MASK; + + boolean complete = FLAGS_C == (flags & FLAGS_C); + boolean next = FLAGS_N == (flags & FLAGS_N); + if (next && complete) { + result = FrameType.NEXT_COMPLETE; + } else if (complete) { + result = FrameType.COMPLETE; + } else if (next) { + result = FrameType.NEXT; + } else { + throw new IllegalArgumentException("Payload must set either or both of NEXT and COMPLETE."); + } + } + + byteBuf.resetReaderIndex(); + + return result; + } + + public static void ensureFrameType(final FrameType frameType, ByteBuf byteBuf) { + if (!disableFrameTypeCheck) { + final FrameType typeInFrame = frameType(byteBuf); + + if (typeInFrame != frameType) { + throw new AssertionError("expected " + frameType + ", but saw " + typeInFrame); + } + } + } + + public static int size() { + return HEADER_SIZE; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java deleted file mode 100644 index bd783b74f..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java +++ /dev/null @@ -1,345 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.rsocket.FrameType; - -/** - * Per connection frame flyweight. - * - *

Not the latest frame layout, but close. Does not include - fragmentation / reassembly - encode - * should remove Type param and have it as part of method name (1 encode per type?) - * - *

Not thread-safe. Assumed to be used single-threaded - */ -public class FrameHeaderFlyweight { - - private FrameHeaderFlyweight() {} - - public static final int FRAME_HEADER_LENGTH; - - private static final int FRAME_TYPE_BITS = 6; - private static final int FRAME_TYPE_SHIFT = 16 - FRAME_TYPE_BITS; - private static final int FRAME_FLAGS_MASK = 0b0000_0011_1111_1111; - - public static final int FRAME_LENGTH_SIZE = 3; - public static final int FRAME_LENGTH_MASK = 0xFFFFFF; - - private static final int FRAME_LENGTH_FIELD_OFFSET; - private static final int FRAME_TYPE_AND_FLAGS_FIELD_OFFSET; - private static final int STREAM_ID_FIELD_OFFSET; - private static final int PAYLOAD_OFFSET; - - public static final int FLAGS_I = 0b10_0000_0000; - public static final int FLAGS_M = 0b01_0000_0000; - - public static final int FLAGS_F = 0b00_1000_0000; - public static final int FLAGS_C = 0b00_0100_0000; - public static final int FLAGS_N = 0b00_0010_0000; - - static { - FRAME_LENGTH_FIELD_OFFSET = 0; - STREAM_ID_FIELD_OFFSET = FRAME_LENGTH_FIELD_OFFSET + FRAME_LENGTH_SIZE; - FRAME_TYPE_AND_FLAGS_FIELD_OFFSET = STREAM_ID_FIELD_OFFSET + Integer.BYTES; - PAYLOAD_OFFSET = FRAME_TYPE_AND_FLAGS_FIELD_OFFSET + Short.BYTES; - FRAME_HEADER_LENGTH = PAYLOAD_OFFSET; - } - - public static int computeFrameHeaderLength( - final FrameType frameType, int metadataLength, final int dataLength) { - return PAYLOAD_OFFSET + computeMetadataLength(frameType, metadataLength) + dataLength; - } - - public static int encodeFrameHeader( - final ByteBuf byteBuf, - final int frameLength, - final int flags, - final FrameType frameType, - final int streamId) { - if ((frameLength & ~FRAME_LENGTH_MASK) != 0) { - throw new IllegalArgumentException("Frame length is larger than 24 bits"); - } - - // frame length field needs to be excluded from the length - encodeLength(byteBuf, FRAME_LENGTH_FIELD_OFFSET, frameLength - FRAME_LENGTH_SIZE); - - byteBuf.setInt(STREAM_ID_FIELD_OFFSET, streamId); - short typeAndFlags = (short) (frameType.getEncodedType() << FRAME_TYPE_SHIFT | (short) flags); - byteBuf.setShort(FRAME_TYPE_AND_FLAGS_FIELD_OFFSET, typeAndFlags); - - return FRAME_HEADER_LENGTH; - } - - public static int encodeMetadata( - final ByteBuf byteBuf, - final FrameType frameType, - final int metadataOffset, - final ByteBuf metadata) { - int length = 0; - final int metadataLength = metadata.readableBytes(); - - if (0 < metadataLength) { - int typeAndFlags = byteBuf.getShort(FRAME_TYPE_AND_FLAGS_FIELD_OFFSET); - typeAndFlags |= FLAGS_M; - byteBuf.setShort(FRAME_TYPE_AND_FLAGS_FIELD_OFFSET, (short) typeAndFlags); - - if (hasMetadataLengthField(frameType)) { - encodeLength(byteBuf, metadataOffset, metadataLength); - length += FRAME_LENGTH_SIZE; - } - byteBuf.setBytes(metadataOffset + length, metadata); - length += metadataLength; - } - - return length; - } - - public static int encodeData(final ByteBuf byteBuf, final int dataOffset, final ByteBuf data) { - int length = 0; - final int dataLength = data.readableBytes(); - - if (0 < dataLength) { - byteBuf.setBytes(dataOffset, data); - length += dataLength; - } - - return length; - } - - // only used for types simple enough that they don't have their own FrameFlyweights - public static int encode( - final ByteBuf byteBuf, - final int streamId, - int flags, - final FrameType frameType, - final ByteBuf metadata, - final ByteBuf data) { - final int frameLength = - computeFrameHeaderLength(frameType, metadata.readableBytes(), data.readableBytes()); - - final FrameType outFrameType; - switch (frameType) { - case PAYLOAD: - throw new IllegalArgumentException( - "Don't encode raw PAYLOAD frames, use NEXT_COMPLETE, COMPLETE or NEXT"); - case NEXT_COMPLETE: - outFrameType = FrameType.PAYLOAD; - flags |= FLAGS_C | FLAGS_N; - break; - case COMPLETE: - outFrameType = FrameType.PAYLOAD; - flags |= FLAGS_C; - break; - case NEXT: - outFrameType = FrameType.PAYLOAD; - flags |= FLAGS_N; - break; - default: - outFrameType = frameType; - break; - } - - int length = encodeFrameHeader(byteBuf, frameLength, flags, outFrameType, streamId); - - length += encodeMetadata(byteBuf, frameType, length, metadata); - length += encodeData(byteBuf, length, data); - - return length; - } - - public static int flags(final ByteBuf byteBuf) { - short typeAndFlags = byteBuf.getShort(FRAME_TYPE_AND_FLAGS_FIELD_OFFSET); - return typeAndFlags & FRAME_FLAGS_MASK; - } - - public static FrameType frameType(final ByteBuf byteBuf) { - int typeAndFlags = byteBuf.getShort(FRAME_TYPE_AND_FLAGS_FIELD_OFFSET); - FrameType result = FrameType.from(typeAndFlags >> FRAME_TYPE_SHIFT); - - if (FrameType.PAYLOAD == result) { - final int flags = typeAndFlags & FRAME_FLAGS_MASK; - - boolean complete = FLAGS_C == (flags & FLAGS_C); - boolean next = FLAGS_N == (flags & FLAGS_N); - if (next && complete) { - result = FrameType.NEXT_COMPLETE; - } else if (complete) { - result = FrameType.COMPLETE; - } else if (next) { - result = FrameType.NEXT; - } else { - throw new IllegalArgumentException("Payload must set either or both of NEXT and COMPLETE."); - } - } - - return result; - } - - public static int streamId(final ByteBuf byteBuf) { - return byteBuf.getInt(STREAM_ID_FIELD_OFFSET); - } - - public static ByteBuf sliceFrameData(final ByteBuf byteBuf) { - final FrameType frameType = frameType(byteBuf); - final int frameLength = frameLength(byteBuf); - final int dataLength = dataLength(byteBuf, frameType); - final int dataOffset = dataOffset(byteBuf, frameType, frameLength); - ByteBuf result = Unpooled.EMPTY_BUFFER; - - if (0 < dataLength) { - result = byteBuf.slice(dataOffset, dataLength); - } - - return result; - } - - public static ByteBuf sliceFrameMetadata(final ByteBuf byteBuf) { - final FrameType frameType = frameType(byteBuf); - final int frameLength = frameLength(byteBuf); - final int metadataLength = Math.max(0, metadataLength(byteBuf, frameType, frameLength)); - int metadataOffset = metadataOffset(byteBuf); - if (hasMetadataLengthField(frameType)) { - metadataOffset += FRAME_LENGTH_SIZE; - } - ByteBuf result = Unpooled.EMPTY_BUFFER; - - if (0 < metadataLength) { - result = byteBuf.slice(metadataOffset, metadataLength); - } - - return result; - } - - public static int frameLength(final ByteBuf byteBuf) { - // frame length field was excluded from the length so we will add it to represent - // the entire block - return decodeLength(byteBuf, FRAME_LENGTH_FIELD_OFFSET) + FRAME_LENGTH_SIZE; - } - - private static int metadataFieldLength(ByteBuf byteBuf, FrameType frameType, int frameLength) { - return computeMetadataLength(frameType, metadataLength(byteBuf, frameType, frameLength)); - } - - public static int metadataLength(ByteBuf byteBuf, FrameType frameType, int frameLength) { - int metadataOffset = metadataOffset(byteBuf); - if (!hasMetadataLengthField(frameType)) { - return frameLength - metadataOffset; - } else { - return decodeMetadataLength(byteBuf, metadataOffset); - } - } - - static int decodeMetadataLength(final ByteBuf byteBuf, final int metadataOffset) { - int metadataLength = 0; - - int flags = flags(byteBuf); - if (FLAGS_M == (FLAGS_M & flags)) { - metadataLength = decodeLength(byteBuf, metadataOffset); - } - - return metadataLength; - } - - private static int computeMetadataLength(FrameType frameType, final int length) { - if (!hasMetadataLengthField(frameType)) { - // Frames with only metadata does not need metadata length field - return length; - } else { - return length == 0 ? 0 : length + FRAME_LENGTH_SIZE; - } - } - - public static boolean hasMetadataLengthField(FrameType frameType) { - return frameType.canHaveData(); - } - - public static void encodeLength(final ByteBuf byteBuf, final int offset, final int length) { - if ((length & ~FRAME_LENGTH_MASK) != 0) { - throw new IllegalArgumentException("Length is larger than 24 bits"); - } - // Write each byte separately in reverse order, this mean we can write 1 << 23 without overflowing. - byteBuf.setByte(offset, length >> 16); - byteBuf.setByte(offset + 1, length >> 8); - byteBuf.setByte(offset + 2, length); - } - - private static int decodeLength(final ByteBuf byteBuf, final int offset) { - int length = (byteBuf.getByte(offset) & 0xFF) << 16; - length |= (byteBuf.getByte(offset + 1) & 0xFF) << 8; - length |= byteBuf.getByte(offset + 2) & 0xFF; - return length; - } - - public static int dataLength(final ByteBuf byteBuf, final FrameType frameType) { - return dataLength(byteBuf, frameType, payloadOffset(byteBuf)); - } - - static int dataLength(final ByteBuf byteBuf, final FrameType frameType, final int payloadOffset) { - final int frameLength = frameLength(byteBuf); - final int metadataLength = metadataFieldLength(byteBuf, frameType, frameLength); - - return frameLength - metadataLength - payloadOffset; - } - - public static int payloadLength(final ByteBuf byteBuf) { - final int frameLength = frameLength(byteBuf); - final int payloadOffset = payloadOffset(byteBuf); - - return frameLength - payloadOffset; - } - - private static int payloadOffset(final ByteBuf byteBuf) { - int typeAndFlags = byteBuf.getShort(FRAME_TYPE_AND_FLAGS_FIELD_OFFSET); - FrameType frameType = FrameType.from(typeAndFlags >> FRAME_TYPE_SHIFT); - int result = PAYLOAD_OFFSET; - - switch (frameType) { - case SETUP: - result = SetupFrameFlyweight.payloadOffset(byteBuf); - break; - case ERROR: - result = ErrorFrameFlyweight.payloadOffset(byteBuf); - break; - case LEASE: - result = LeaseFrameFlyweight.payloadOffset(byteBuf); - break; - case KEEPALIVE: - result = KeepaliveFrameFlyweight.payloadOffset(byteBuf); - break; - case REQUEST_RESPONSE: - case FIRE_AND_FORGET: - case REQUEST_STREAM: - case REQUEST_CHANNEL: - result = RequestFrameFlyweight.payloadOffset(frameType, byteBuf); - break; - case REQUEST_N: - result = RequestNFrameFlyweight.payloadOffset(byteBuf); - break; - } - - return result; - } - - public static int metadataOffset(final ByteBuf byteBuf) { - return payloadOffset(byteBuf); - } - - public static int dataOffset(ByteBuf byteBuf, FrameType frameType, int frameLength) { - return payloadOffset(byteBuf) + metadataFieldLength(byteBuf, frameType, frameLength); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthCodec.java new file mode 100644 index 000000000..f6c19c8ee --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthCodec.java @@ -0,0 +1,54 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; + +/** + * Some transports like TCP aren't framed, and require a length. This is used by DuplexConnections + * for transports that need to send length + */ +public class FrameLengthCodec { + public static final int FRAME_LENGTH_MASK = 0xFFFFFF; + public static final int FRAME_LENGTH_SIZE = 3; + + private FrameLengthCodec() {} + + private static void encodeLength(final ByteBuf byteBuf, final int length) { + if ((length & ~FRAME_LENGTH_MASK) != 0) { + throw new IllegalArgumentException("Length is larger than 24 bits"); + } + // Write each byte separately in reverse order, this mean we can write 1 << 23 without + // overflowing. + byteBuf.writeByte(length >> 16); + byteBuf.writeByte(length >> 8); + byteBuf.writeByte(length); + } + + private static int decodeLength(final ByteBuf byteBuf) { + int length = (byteBuf.readByte() & 0xFF) << 16; + length |= (byteBuf.readByte() & 0xFF) << 8; + length |= byteBuf.readByte() & 0xFF; + return length; + } + + public static ByteBuf encode(ByteBufAllocator allocator, int length, ByteBuf frame) { + ByteBuf buffer = allocator.buffer(); + encodeLength(buffer, length); + return allocator.compositeBuffer(2).addComponents(true, buffer, frame); + } + + public static int length(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + int length = decodeLength(byteBuf); + byteBuf.resetReaderIndex(); + return length; + } + + public static ByteBuf frame(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + byteBuf.skipBytes(3); + ByteBuf slice = byteBuf.slice(); + byteBuf.resetReaderIndex(); + return slice; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameType.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameType.java new file mode 100644 index 000000000..8ac743f87 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameType.java @@ -0,0 +1,315 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.frame; + +import java.util.Arrays; + +/** + * Types of Frame that can be sent. + * + * @see Frame + * Types + */ +public enum FrameType { + + /** Reserved. */ + RESERVED(0x00), + + // CONNECTION + + /** + * Sent by client to initiate protocol processing. + * + * @see Setup + * Frame + */ + SETUP(0x01, Flags.CAN_HAVE_DATA | Flags.CAN_HAVE_METADATA), + + /** + * Sent by Responder to grant the ability to send requests. + * + * @see Lease + * Frame + */ + LEASE(0x02, Flags.CAN_HAVE_METADATA), + + /** + * Connection keepalive. + * + * @see Keepalive + * Frame + */ + KEEPALIVE(0x03, Flags.CAN_HAVE_DATA), + + // START REQUEST + + /** + * Request single response. + * + * @see Request + * Response Frame + */ + REQUEST_RESPONSE( + 0x04, + Flags.CAN_HAVE_DATA + | Flags.CAN_HAVE_METADATA + | Flags.IS_FRAGMENTABLE + | Flags.IS_REQUEST_TYPE), + + /** + * A single one-way message. + * + * @see Request + * Fire-and-Forget Frame + */ + REQUEST_FNF( + 0x05, + Flags.CAN_HAVE_DATA + | Flags.CAN_HAVE_METADATA + | Flags.IS_FRAGMENTABLE + | Flags.IS_REQUEST_TYPE), + + /** + * Request a completable stream. + * + * @see Request + * Stream Frame + */ + REQUEST_STREAM( + 0x06, + Flags.CAN_HAVE_METADATA + | Flags.CAN_HAVE_DATA + | Flags.HAS_INITIAL_REQUEST_N + | Flags.IS_FRAGMENTABLE + | Flags.IS_REQUEST_TYPE), + + /** + * Request a completable stream in both directions. + * + * @see Request + * Channel Frame + */ + REQUEST_CHANNEL( + 0x07, + Flags.CAN_HAVE_METADATA + | Flags.CAN_HAVE_DATA + | Flags.HAS_INITIAL_REQUEST_N + | Flags.IS_FRAGMENTABLE + | Flags.IS_REQUEST_TYPE), + + // DURING REQUEST + + /** + * Request N more items with Reactive Streams semantics. + * + * @see RequestN + * Frame + */ + REQUEST_N(0x08), + + /** + * Cancel outstanding request. + * + * @see Cancel + * Frame + */ + CANCEL(0x09), + + // RESPONSE + + /** + * Payload on a stream. For example, response to a request, or message on a channel. + * + * @see Payload + * Frame + */ + PAYLOAD(0x0A, Flags.CAN_HAVE_DATA | Flags.CAN_HAVE_METADATA | Flags.IS_FRAGMENTABLE), + + /** + * Error at connection or application level. + * + * @see Error + * Frame + */ + ERROR(0x0B, Flags.CAN_HAVE_DATA), + + // METADATA + + /** + * Asynchronous Metadata frame. + * + * @see Metadata + * Push Frame + */ + METADATA_PUSH(0x0C, Flags.CAN_HAVE_METADATA), + + // RESUMPTION + + /** + * Replaces SETUP for Resuming Operation (optional). + * + * @see Resume + * Frame + */ + RESUME(0x0D), + + /** + * Sent in response to a RESUME if resuming operation possible (optional). + * + * @see Resume OK + * Frame + */ + RESUME_OK(0x0E), + + // SYNTHETIC PAYLOAD TYPES + + /** A {@link #PAYLOAD} frame with {@code NEXT} flag set. */ + NEXT(0xA0, Flags.CAN_HAVE_DATA | Flags.CAN_HAVE_METADATA | Flags.IS_FRAGMENTABLE), + + /** A {@link #PAYLOAD} frame with {@code COMPLETE} flag set. */ + COMPLETE(0xB0), + + /** A {@link #PAYLOAD} frame with {@code NEXT} and {@code COMPLETE} flags set. */ + NEXT_COMPLETE(0xC0, Flags.CAN_HAVE_DATA | Flags.CAN_HAVE_METADATA | Flags.IS_FRAGMENTABLE), + + /** + * Used To Extend more frame types as well as extensions. + * + * @see Extension + * Frame + */ + EXT(0x3F, Flags.CAN_HAVE_DATA | Flags.CAN_HAVE_METADATA); + + /** The size of the encoded frame type */ + static final int ENCODED_SIZE = 6; + + private static final FrameType[] FRAME_TYPES_BY_ENCODED_TYPE; + + static { + FRAME_TYPES_BY_ENCODED_TYPE = new FrameType[getMaximumEncodedType() + 1]; + + for (FrameType frameType : values()) { + FRAME_TYPES_BY_ENCODED_TYPE[frameType.encodedType] = frameType; + } + } + + private final int encodedType; + private final int flags; + + FrameType(int encodedType) { + this(encodedType, Flags.EMPTY); + } + + FrameType(int encodedType, int flags) { + this.encodedType = encodedType; + this.flags = flags; + } + + /** + * Returns the {@code FrameType} that matches the specified {@code encodedType}. + * + * @param encodedType the encoded type + * @return the {@code FrameType} that matches the specified {@code encodedType} + */ + public static FrameType fromEncodedType(int encodedType) { + FrameType frameType = FRAME_TYPES_BY_ENCODED_TYPE[encodedType]; + + if (frameType == null) { + throw new IllegalArgumentException(String.format("Frame type %d is unknown", encodedType)); + } + + return frameType; + } + + private static int getMaximumEncodedType() { + return Arrays.stream(values()).mapToInt(frameType -> frameType.encodedType).max().orElse(0); + } + + /** + * Whether the frame type can have data. + * + * @return whether the frame type can have data + */ + public boolean canHaveData() { + return Flags.CAN_HAVE_DATA == (flags & Flags.CAN_HAVE_DATA); + } + + /** + * Whether the frame type can have metadata + * + * @return whether the frame type can have metadata + */ + public boolean canHaveMetadata() { + return Flags.CAN_HAVE_METADATA == (flags & Flags.CAN_HAVE_METADATA); + } + + /** + * Returns the encoded type. + * + * @return the encoded type + */ + public int getEncodedType() { + return encodedType; + } + + /** + * Whether the frame type starts with an initial {@code requestN}. + * + * @return wether the frame type starts with an initial {@code requestN} + */ + public boolean hasInitialRequestN() { + return Flags.HAS_INITIAL_REQUEST_N == (flags & Flags.HAS_INITIAL_REQUEST_N); + } + + /** + * Whether the frame type is fragmentable. + * + * @return whether the frame type is fragmentable + */ + public boolean isFragmentable() { + return Flags.IS_FRAGMENTABLE == (flags & Flags.IS_FRAGMENTABLE); + } + + /** + * Whether the frame type is a request type. + * + * @return whether the frame type is a request type + */ + public boolean isRequestType() { + return Flags.IS_REQUEST_TYPE == (flags & Flags.IS_REQUEST_TYPE); + } + + private static class Flags { + private static final int EMPTY = 0b00000; + private static final int CAN_HAVE_DATA = 0b10000; + private static final int CAN_HAVE_METADATA = 0b01000; + private static final int IS_FRAGMENTABLE = 0b00100; + private static final int IS_REQUEST_TYPE = 0b00010; + private static final int HAS_INITIAL_REQUEST_N = 0b00001; + + private Flags() {} + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java new file mode 100644 index 000000000..66d18c8a7 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java @@ -0,0 +1,117 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; + +public class FrameUtil { + + private FrameUtil() {} + + public static String toString(ByteBuf frame) { + FrameType frameType = FrameHeaderCodec.frameType(frame); + int streamId = FrameHeaderCodec.streamId(frame); + StringBuilder payload = new StringBuilder(); + + payload + .append("\nFrame => Stream ID: ") + .append(streamId) + .append(" Type: ") + .append(frameType) + .append(" Flags: 0b") + .append(Integer.toBinaryString(FrameHeaderCodec.flags(frame))) + .append(" Length: " + frame.readableBytes()); + + if (frameType.hasInitialRequestN()) { + payload.append(" InitialRequestN: ").append(RequestStreamFrameCodec.initialRequestN(frame)); + } + + if (frameType == FrameType.REQUEST_N) { + payload.append(" RequestN: ").append(RequestNFrameCodec.requestN(frame)); + } + + if (FrameHeaderCodec.hasMetadata(frame)) { + payload.append("\nMetadata:\n"); + + ByteBufUtil.appendPrettyHexDump(payload, getMetadata(frame, frameType)); + } + + payload.append("\nData:\n"); + ByteBufUtil.appendPrettyHexDump(payload, getData(frame, frameType)); + + return payload.toString(); + } + + private static ByteBuf getMetadata(ByteBuf frame, FrameType frameType) { + boolean hasMetadata = FrameHeaderCodec.hasMetadata(frame); + if (hasMetadata) { + ByteBuf metadata; + switch (frameType) { + case REQUEST_FNF: + metadata = RequestFireAndForgetFrameCodec.metadata(frame); + break; + case REQUEST_STREAM: + metadata = RequestStreamFrameCodec.metadata(frame); + break; + case REQUEST_RESPONSE: + metadata = RequestResponseFrameCodec.metadata(frame); + break; + case REQUEST_CHANNEL: + metadata = RequestChannelFrameCodec.metadata(frame); + break; + // Payload and synthetic types + case PAYLOAD: + case NEXT: + case NEXT_COMPLETE: + case COMPLETE: + metadata = PayloadFrameCodec.metadata(frame); + break; + case METADATA_PUSH: + metadata = MetadataPushFrameCodec.metadata(frame); + break; + case SETUP: + metadata = SetupFrameCodec.metadata(frame); + break; + case LEASE: + metadata = LeaseFrameCodec.metadata(frame); + break; + default: + return Unpooled.EMPTY_BUFFER; + } + return metadata; + } else { + return Unpooled.EMPTY_BUFFER; + } + } + + private static ByteBuf getData(ByteBuf frame, FrameType frameType) { + ByteBuf data; + switch (frameType) { + case REQUEST_FNF: + data = RequestFireAndForgetFrameCodec.data(frame); + break; + case REQUEST_STREAM: + data = RequestStreamFrameCodec.data(frame); + break; + case REQUEST_RESPONSE: + data = RequestResponseFrameCodec.data(frame); + break; + case REQUEST_CHANNEL: + data = RequestChannelFrameCodec.data(frame); + break; + // Payload and synthetic types + case PAYLOAD: + case NEXT: + case NEXT_COMPLETE: + case COMPLETE: + data = PayloadFrameCodec.data(frame); + break; + case SETUP: + data = SetupFrameCodec.data(frame); + break; + default: + return Unpooled.EMPTY_BUFFER; + } + return data; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/GenericFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/GenericFrameCodec.java new file mode 100644 index 000000000..56a93d869 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/GenericFrameCodec.java @@ -0,0 +1,159 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.IllegalReferenceCountException; +import io.rsocket.Payload; +import reactor.util.annotation.Nullable; + +class GenericFrameCodec { + + static ByteBuf encodeReleasingPayload( + final ByteBufAllocator allocator, + final FrameType frameType, + final int streamId, + boolean complete, + boolean next, + final Payload payload) { + return encodeReleasingPayload(allocator, frameType, streamId, complete, next, 0, payload); + } + + static ByteBuf encodeReleasingPayload( + final ByteBufAllocator allocator, + final FrameType frameType, + final int streamId, + boolean complete, + boolean next, + int requestN, + final Payload payload) { + + // if refCnt exceptions throws here it is safe to do no-op + boolean hasMetadata = payload.hasMetadata(); + // if refCnt exceptions throws here it is safe to do no-op still + final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; + final ByteBuf data; + // retaining data safely. May throw either NPE or RefCntE + try { + data = payload.data().retain(); + } catch (IllegalReferenceCountException | NullPointerException e) { + if (hasMetadata) { + metadata.release(); + } + throw e; + } + // releasing payload safely since it can be already released wheres we have to release retained + // data and metadata as well + try { + payload.release(); + } catch (IllegalReferenceCountException e) { + data.release(); + if (hasMetadata) { + metadata.release(); + } + throw e; + } + + return encode(allocator, frameType, streamId, false, complete, next, requestN, metadata, data); + } + + static ByteBuf encode( + final ByteBufAllocator allocator, + final FrameType frameType, + final int streamId, + boolean fragmentFollows, + @Nullable ByteBuf metadata, + ByteBuf data) { + return encode(allocator, frameType, streamId, fragmentFollows, false, false, 0, metadata, data); + } + + static ByteBuf encode( + final ByteBufAllocator allocator, + final FrameType frameType, + final int streamId, + boolean fragmentFollows, + boolean complete, + boolean next, + int requestN, + @Nullable ByteBuf metadata, + @Nullable ByteBuf data) { + + final boolean hasMetadata = metadata != null; + + int flags = 0; + + if (hasMetadata) { + flags |= FrameHeaderCodec.FLAGS_M; + } + + if (fragmentFollows) { + flags |= FrameHeaderCodec.FLAGS_F; + } + + if (complete) { + flags |= FrameHeaderCodec.FLAGS_C; + } + + if (next) { + flags |= FrameHeaderCodec.FLAGS_N; + } + + final ByteBuf header = FrameHeaderCodec.encode(allocator, streamId, frameType, flags); + + if (requestN > 0) { + header.writeInt(requestN); + } + + return FrameBodyCodec.encode(allocator, header, metadata, hasMetadata, data); + } + + static ByteBuf data(ByteBuf byteBuf) { + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); + int idx = byteBuf.readerIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size()); + ByteBuf data = FrameBodyCodec.dataWithoutMarking(byteBuf, hasMetadata); + byteBuf.readerIndex(idx); + return data; + } + + @Nullable + static ByteBuf metadata(ByteBuf byteBuf) { + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); + if (!hasMetadata) { + return null; + } + byteBuf.markReaderIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size()); + ByteBuf metadata = FrameBodyCodec.metadataWithoutMarking(byteBuf); + byteBuf.resetReaderIndex(); + return metadata; + } + + static ByteBuf dataWithRequestN(ByteBuf byteBuf) { + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); + byteBuf.markReaderIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size() + Integer.BYTES); + ByteBuf data = FrameBodyCodec.dataWithoutMarking(byteBuf, hasMetadata); + byteBuf.resetReaderIndex(); + return data; + } + + @Nullable + static ByteBuf metadataWithRequestN(ByteBuf byteBuf) { + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); + if (!hasMetadata) { + return null; + } + byteBuf.markReaderIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size() + Integer.BYTES); + ByteBuf metadata = FrameBodyCodec.metadataWithoutMarking(byteBuf); + byteBuf.resetReaderIndex(); + return metadata; + } + + static int initialRequestN(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + int i = byteBuf.skipBytes(FrameHeaderCodec.size()).readInt(); + byteBuf.resetReaderIndex(); + return i; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/KeepAliveFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/KeepAliveFrameCodec.java new file mode 100644 index 000000000..752d5b3eb --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/KeepAliveFrameCodec.java @@ -0,0 +1,56 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; + +public class KeepAliveFrameCodec { + /** + * (R)espond: Set by the sender of the KEEPALIVE, to which the responder MUST reply with a + * KEEPALIVE without the R flag set + */ + public static final int FLAGS_KEEPALIVE_R = 0b00_1000_0000; + + public static final long LAST_POSITION_MASK = 0x8000000000000000L; + + private KeepAliveFrameCodec() {} + + public static ByteBuf encode( + final ByteBufAllocator allocator, + final boolean respond, + final long lastPosition, + final ByteBuf data) { + final int flags = respond ? FLAGS_KEEPALIVE_R : 0; + ByteBuf header = FrameHeaderCodec.encodeStreamZero(allocator, FrameType.KEEPALIVE, flags); + + long lp = 0; + if (lastPosition > 0) { + lp |= lastPosition; + } + + header.writeLong(lp); + + return FrameBodyCodec.encode(allocator, header, null, false, data); + } + + public static boolean respondFlag(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.KEEPALIVE, byteBuf); + int flags = FrameHeaderCodec.flags(byteBuf); + return (flags & FLAGS_KEEPALIVE_R) == FLAGS_KEEPALIVE_R; + } + + public static long lastPosition(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.KEEPALIVE, byteBuf); + byteBuf.markReaderIndex(); + long l = byteBuf.skipBytes(FrameHeaderCodec.size()).readLong(); + byteBuf.resetReaderIndex(); + return l; + } + + public static ByteBuf data(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.KEEPALIVE, byteBuf); + byteBuf.markReaderIndex(); + ByteBuf slice = byteBuf.skipBytes(FrameHeaderCodec.size() + Long.BYTES).slice(); + byteBuf.resetReaderIndex(); + return slice; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/KeepaliveFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/KeepaliveFrameFlyweight.java deleted file mode 100644 index 41f124eb7..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/KeepaliveFrameFlyweight.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.rsocket.FrameType; - -public class KeepaliveFrameFlyweight { - public static final int FLAGS_KEEPALIVE_R = 0b00_1000_0000; - - private KeepaliveFrameFlyweight() {} - - private static final int LAST_POSITION_OFFSET = FrameHeaderFlyweight.FRAME_HEADER_LENGTH; - private static final int PAYLOAD_OFFSET = LAST_POSITION_OFFSET + Long.BYTES; - - public static int computeFrameLength(final int dataLength) { - return FrameHeaderFlyweight.computeFrameHeaderLength(FrameType.SETUP, 0, dataLength) - + Long.BYTES; - } - - public static int encode(final ByteBuf byteBuf, int flags, final ByteBuf data) { - final int frameLength = computeFrameLength(data.readableBytes()); - - int length = - FrameHeaderFlyweight.encodeFrameHeader(byteBuf, frameLength, flags, FrameType.KEEPALIVE, 0); - - // We don't support resumability, last position is always zero - byteBuf.setLong(length, 0); - length += Long.BYTES; - - length += FrameHeaderFlyweight.encodeData(byteBuf, length, data); - - return length; - } - - public static int payloadOffset(final ByteBuf byteBuf) { - return PAYLOAD_OFFSET; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameCodec.java new file mode 100644 index 000000000..f20c25d3b --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameCodec.java @@ -0,0 +1,83 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import reactor.util.annotation.Nullable; + +public class LeaseFrameCodec { + + public static ByteBuf encode( + final ByteBufAllocator allocator, + final int ttl, + final int numRequests, + @Nullable final ByteBuf metadata) { + + final boolean hasMetadata = metadata != null; + + int flags = 0; + + if (hasMetadata) { + flags |= FrameHeaderCodec.FLAGS_M; + } + + final ByteBuf header = + FrameHeaderCodec.encodeStreamZero(allocator, FrameType.LEASE, flags) + .writeInt(ttl) + .writeInt(numRequests); + + final boolean addMetadata; + if (hasMetadata) { + if (metadata.isReadable()) { + addMetadata = true; + } else { + // even though there is nothing to read, we still have to release here since nobody else + // going to do soo + metadata.release(); + addMetadata = false; + } + } else { + // has no metadata means it is null, thus no need to release anything + addMetadata = false; + } + + if (addMetadata) { + return allocator.compositeBuffer(2).addComponents(true, header, metadata); + } else { + return header; + } + } + + public static int ttl(final ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.LEASE, byteBuf); + byteBuf.markReaderIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size()); + int ttl = byteBuf.readInt(); + byteBuf.resetReaderIndex(); + return ttl; + } + + public static int numRequests(final ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.LEASE, byteBuf); + byteBuf.markReaderIndex(); + // Ttl + byteBuf.skipBytes(FrameHeaderCodec.size() + Integer.BYTES); + int numRequests = byteBuf.readInt(); + byteBuf.resetReaderIndex(); + return numRequests; + } + + @Nullable + public static ByteBuf metadata(final ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.LEASE, byteBuf); + if (FrameHeaderCodec.hasMetadata(byteBuf)) { + byteBuf.markReaderIndex(); + // Ttl + Num of requests + byteBuf.skipBytes(FrameHeaderCodec.size() + Integer.BYTES * 2); + ByteBuf metadata = byteBuf.slice(); + byteBuf.resetReaderIndex(); + return metadata; + } else { + return null; + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java deleted file mode 100644 index 5312ef821..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.rsocket.FrameType; - -public class LeaseFrameFlyweight { - private LeaseFrameFlyweight() {} - - // relative to start of passed offset - private static final int TTL_FIELD_OFFSET = FrameHeaderFlyweight.FRAME_HEADER_LENGTH; - private static final int NUM_REQUESTS_FIELD_OFFSET = TTL_FIELD_OFFSET + Integer.BYTES; - private static final int PAYLOAD_OFFSET = NUM_REQUESTS_FIELD_OFFSET + Integer.BYTES; - - public static int computeFrameLength(final int metadataLength) { - int length = FrameHeaderFlyweight.computeFrameHeaderLength(FrameType.LEASE, metadataLength, 0); - return length + Integer.BYTES * 2; - } - - public static int encode( - final ByteBuf byteBuf, final int ttl, final int numRequests, final ByteBuf metadata) { - final int frameLength = computeFrameLength(metadata.readableBytes()); - - int length = - FrameHeaderFlyweight.encodeFrameHeader(byteBuf, frameLength, 0, FrameType.LEASE, 0); - - byteBuf.setInt(TTL_FIELD_OFFSET, ttl); - byteBuf.setInt(NUM_REQUESTS_FIELD_OFFSET, numRequests); - - length += Integer.BYTES * 2; - length += FrameHeaderFlyweight.encodeMetadata(byteBuf, FrameType.LEASE, length, metadata); - - return length; - } - - public static int ttl(final ByteBuf byteBuf) { - return byteBuf.getInt(TTL_FIELD_OFFSET); - } - - public static int numRequests(final ByteBuf byteBuf) { - return byteBuf.getInt(NUM_REQUESTS_FIELD_OFFSET); - } - - public static int payloadOffset(final ByteBuf byteBuf) { - return PAYLOAD_OFFSET; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameCodec.java new file mode 100644 index 000000000..d8ffe3eef --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameCodec.java @@ -0,0 +1,43 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.IllegalReferenceCountException; +import io.rsocket.Payload; + +public class MetadataPushFrameCodec { + + public static ByteBuf encodeReleasingPayload(ByteBufAllocator allocator, Payload payload) { + if (!payload.hasMetadata()) { + throw new IllegalStateException( + "Metadata push requires to have metadata present" + " in the given Payload"); + } + final ByteBuf metadata = payload.metadata().retain(); + // releasing payload safely since it can be already released wheres we have to release retained + // data and metadata as well + try { + payload.release(); + } catch (IllegalReferenceCountException e) { + metadata.release(); + throw e; + } + return encode(allocator, metadata); + } + + public static ByteBuf encode(ByteBufAllocator allocator, ByteBuf metadata) { + ByteBuf header = + FrameHeaderCodec.encodeStreamZero( + allocator, FrameType.METADATA_PUSH, FrameHeaderCodec.FLAGS_M); + return allocator.compositeBuffer(2).addComponents(true, header, metadata); + } + + public static ByteBuf metadata(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + int headerSize = FrameHeaderCodec.size(); + int metadataLength = byteBuf.readableBytes() - headerSize; + byteBuf.skipBytes(headerSize); + ByteBuf metadata = byteBuf.readSlice(metadataLength); + byteBuf.resetReaderIndex(); + return metadata; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameCodec.java new file mode 100644 index 000000000..1ae9c6750 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameCodec.java @@ -0,0 +1,56 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; +import reactor.util.annotation.Nullable; + +public class PayloadFrameCodec { + + private PayloadFrameCodec() {} + + public static ByteBuf encodeNextReleasingPayload( + ByteBufAllocator allocator, int streamId, Payload payload) { + + return encodeReleasingPayload(allocator, streamId, false, payload); + } + + public static ByteBuf encodeNextCompleteReleasingPayload( + ByteBufAllocator allocator, int streamId, Payload payload) { + + return encodeReleasingPayload(allocator, streamId, true, payload); + } + + static ByteBuf encodeReleasingPayload( + ByteBufAllocator allocator, int streamId, boolean complete, Payload payload) { + + return GenericFrameCodec.encodeReleasingPayload( + allocator, FrameType.PAYLOAD, streamId, complete, true, payload); + } + + public static ByteBuf encodeComplete(ByteBufAllocator allocator, int streamId) { + return encode(allocator, streamId, false, true, false, null, null); + } + + public static ByteBuf encode( + ByteBufAllocator allocator, + int streamId, + boolean fragmentFollows, + boolean complete, + boolean next, + @Nullable ByteBuf metadata, + @Nullable ByteBuf data) { + + return GenericFrameCodec.encode( + allocator, FrameType.PAYLOAD, streamId, fragmentFollows, complete, next, 0, metadata, data); + } + + public static ByteBuf data(ByteBuf byteBuf) { + return GenericFrameCodec.data(byteBuf); + } + + @Nullable + public static ByteBuf metadata(ByteBuf byteBuf) { + return GenericFrameCodec.metadata(byteBuf); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameCodec.java new file mode 100644 index 000000000..60906083d --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameCodec.java @@ -0,0 +1,69 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; +import reactor.util.annotation.Nullable; + +public class RequestChannelFrameCodec { + + private RequestChannelFrameCodec() {} + + public static ByteBuf encodeReleasingPayload( + ByteBufAllocator allocator, + int streamId, + boolean complete, + long initialRequestN, + Payload payload) { + + if (initialRequestN < 1) { + throw new IllegalArgumentException("request n is less than 1"); + } + + int reqN = initialRequestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) initialRequestN; + + return GenericFrameCodec.encodeReleasingPayload( + allocator, FrameType.REQUEST_CHANNEL, streamId, complete, false, reqN, payload); + } + + public static ByteBuf encode( + ByteBufAllocator allocator, + int streamId, + boolean fragmentFollows, + boolean complete, + long initialRequestN, + @Nullable ByteBuf metadata, + ByteBuf data) { + + if (initialRequestN < 1) { + throw new IllegalArgumentException("request n is less than 1"); + } + + int reqN = initialRequestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) initialRequestN; + + return GenericFrameCodec.encode( + allocator, + FrameType.REQUEST_CHANNEL, + streamId, + fragmentFollows, + complete, + false, + reqN, + metadata, + data); + } + + public static ByteBuf data(ByteBuf byteBuf) { + return GenericFrameCodec.dataWithRequestN(byteBuf); + } + + @Nullable + public static ByteBuf metadata(ByteBuf byteBuf) { + return GenericFrameCodec.metadataWithRequestN(byteBuf); + } + + public static long initialRequestN(ByteBuf byteBuf) { + int requestN = GenericFrameCodec.initialRequestN(byteBuf); + return requestN == Integer.MAX_VALUE ? Long.MAX_VALUE : requestN; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameCodec.java new file mode 100644 index 000000000..b91199179 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameCodec.java @@ -0,0 +1,38 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; +import reactor.util.annotation.Nullable; + +public class RequestFireAndForgetFrameCodec { + + private RequestFireAndForgetFrameCodec() {} + + public static ByteBuf encodeReleasingPayload( + ByteBufAllocator allocator, int streamId, Payload payload) { + + return GenericFrameCodec.encodeReleasingPayload( + allocator, FrameType.REQUEST_FNF, streamId, false, false, payload); + } + + public static ByteBuf encode( + ByteBufAllocator allocator, + int streamId, + boolean fragmentFollows, + @Nullable ByteBuf metadata, + ByteBuf data) { + + return GenericFrameCodec.encode( + allocator, FrameType.REQUEST_FNF, streamId, fragmentFollows, metadata, data); + } + + public static ByteBuf data(ByteBuf byteBuf) { + return GenericFrameCodec.data(byteBuf); + } + + @Nullable + public static ByteBuf metadata(ByteBuf byteBuf) { + return GenericFrameCodec.metadata(byteBuf); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestFrameFlyweight.java deleted file mode 100644 index fb2eab504..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestFrameFlyweight.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.rsocket.FrameType; - -public class RequestFrameFlyweight { - - private RequestFrameFlyweight() {} - - // relative to start of passed offset - private static final int INITIAL_REQUEST_N_FIELD_OFFSET = - FrameHeaderFlyweight.FRAME_HEADER_LENGTH; - - public static int computeFrameLength( - final FrameType type, final int metadataLength, final int dataLength) { - int length = FrameHeaderFlyweight.computeFrameHeaderLength(type, metadataLength, dataLength); - - if (type.hasInitialRequestN()) { - length += Integer.BYTES; - } - - return length; - } - - public static int encode( - final ByteBuf byteBuf, - final int streamId, - int flags, - final FrameType type, - final int initialRequestN, - final ByteBuf metadata, - final ByteBuf data) { - final int frameLength = - computeFrameLength(type, metadata.readableBytes(), data.readableBytes()); - - int length = - FrameHeaderFlyweight.encodeFrameHeader(byteBuf, frameLength, flags, type, streamId); - - byteBuf.setInt(INITIAL_REQUEST_N_FIELD_OFFSET, initialRequestN); - length += Integer.BYTES; - - length += FrameHeaderFlyweight.encodeMetadata(byteBuf, type, length, metadata); - length += FrameHeaderFlyweight.encodeData(byteBuf, length, data); - - return length; - } - - public static int encode( - final ByteBuf byteBuf, - final int streamId, - final int flags, - final FrameType type, - final ByteBuf metadata, - final ByteBuf data) { - if (type.hasInitialRequestN()) { - throw new AssertionError(type + " must not be encoded without initial request N"); - } - final int frameLength = - computeFrameLength(type, metadata.readableBytes(), data.readableBytes()); - - int length = - FrameHeaderFlyweight.encodeFrameHeader(byteBuf, frameLength, flags, type, streamId); - - length += FrameHeaderFlyweight.encodeMetadata(byteBuf, type, length, metadata); - length += FrameHeaderFlyweight.encodeData(byteBuf, length, data); - - return length; - } - - public static int initialRequestN(final ByteBuf byteBuf) { - return byteBuf.getInt(INITIAL_REQUEST_N_FIELD_OFFSET); - } - - public static int payloadOffset(final FrameType type, final ByteBuf byteBuf) { - int result = FrameHeaderFlyweight.FRAME_HEADER_LENGTH; - - if (type.hasInitialRequestN()) { - result += Integer.BYTES; - } - - return result; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameCodec.java new file mode 100644 index 000000000..66bdd46f4 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameCodec.java @@ -0,0 +1,30 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; + +public class RequestNFrameCodec { + private RequestNFrameCodec() {} + + public static ByteBuf encode( + final ByteBufAllocator allocator, final int streamId, long requestN) { + + if (requestN < 1) { + throw new IllegalArgumentException("request n is less than 1"); + } + + int reqN = requestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) requestN; + + ByteBuf header = FrameHeaderCodec.encode(allocator, streamId, FrameType.REQUEST_N, 0); + return header.writeInt(reqN); + } + + public static long requestN(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.REQUEST_N, byteBuf); + byteBuf.markReaderIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size()); + int i = byteBuf.readInt(); + byteBuf.resetReaderIndex(); + return i == Integer.MAX_VALUE ? Long.MAX_VALUE : i; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java deleted file mode 100644 index 2e2daef4a..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.rsocket.FrameType; - -public class RequestNFrameFlyweight { - private RequestNFrameFlyweight() {} - - // relative to start of passed offset - private static final int REQUEST_N_FIELD_OFFSET = FrameHeaderFlyweight.FRAME_HEADER_LENGTH; - - public static int computeFrameLength() { - int length = FrameHeaderFlyweight.computeFrameHeaderLength(FrameType.REQUEST_N, 0, 0); - - return length + Integer.BYTES; - } - - public static int encode(final ByteBuf byteBuf, final int streamId, final int requestN) { - final int frameLength = computeFrameLength(); - - int length = - FrameHeaderFlyweight.encodeFrameHeader( - byteBuf, frameLength, 0, FrameType.REQUEST_N, streamId); - - byteBuf.setInt(REQUEST_N_FIELD_OFFSET, requestN); - - return length + Integer.BYTES; - } - - public static int requestN(final ByteBuf byteBuf) { - return byteBuf.getInt(REQUEST_N_FIELD_OFFSET); - } - - public static int payloadOffset(final ByteBuf byteBuf) { - return FrameHeaderFlyweight.FRAME_HEADER_LENGTH + Integer.BYTES; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameCodec.java new file mode 100644 index 000000000..4a37acfd5 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameCodec.java @@ -0,0 +1,37 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; +import reactor.util.annotation.Nullable; + +public class RequestResponseFrameCodec { + + private RequestResponseFrameCodec() {} + + public static ByteBuf encodeReleasingPayload( + ByteBufAllocator allocator, int streamId, Payload payload) { + + return GenericFrameCodec.encodeReleasingPayload( + allocator, FrameType.REQUEST_RESPONSE, streamId, false, false, payload); + } + + public static ByteBuf encode( + ByteBufAllocator allocator, + int streamId, + boolean fragmentFollows, + @Nullable ByteBuf metadata, + ByteBuf data) { + return GenericFrameCodec.encode( + allocator, FrameType.REQUEST_RESPONSE, streamId, fragmentFollows, metadata, data); + } + + public static ByteBuf data(ByteBuf byteBuf) { + return GenericFrameCodec.data(byteBuf); + } + + @Nullable + public static ByteBuf metadata(ByteBuf byteBuf) { + return GenericFrameCodec.metadata(byteBuf); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameCodec.java new file mode 100644 index 000000000..2f5dbf0d8 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameCodec.java @@ -0,0 +1,64 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; +import reactor.util.annotation.Nullable; + +public class RequestStreamFrameCodec { + + private RequestStreamFrameCodec() {} + + public static ByteBuf encodeReleasingPayload( + ByteBufAllocator allocator, int streamId, long initialRequestN, Payload payload) { + + if (initialRequestN < 1) { + throw new IllegalArgumentException("request n is less than 1"); + } + + int reqN = initialRequestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) initialRequestN; + + return GenericFrameCodec.encodeReleasingPayload( + allocator, FrameType.REQUEST_STREAM, streamId, false, false, reqN, payload); + } + + public static ByteBuf encode( + ByteBufAllocator allocator, + int streamId, + boolean fragmentFollows, + long initialRequestN, + @Nullable ByteBuf metadata, + ByteBuf data) { + + if (initialRequestN < 1) { + throw new IllegalArgumentException("request n is less than 1"); + } + + int reqN = initialRequestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) initialRequestN; + + return GenericFrameCodec.encode( + allocator, + FrameType.REQUEST_STREAM, + streamId, + fragmentFollows, + false, + false, + reqN, + metadata, + data); + } + + public static ByteBuf data(ByteBuf byteBuf) { + return GenericFrameCodec.dataWithRequestN(byteBuf); + } + + @Nullable + public static ByteBuf metadata(ByteBuf byteBuf) { + return GenericFrameCodec.metadataWithRequestN(byteBuf); + } + + public static long initialRequestN(ByteBuf byteBuf) { + int requestN = GenericFrameCodec.initialRequestN(byteBuf); + return requestN == Integer.MAX_VALUE ? Long.MAX_VALUE : requestN; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameCodec.java new file mode 100644 index 000000000..aae89f7ab --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameCodec.java @@ -0,0 +1,112 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import java.util.UUID; + +public class ResumeFrameCodec { + static final int CURRENT_VERSION = SetupFrameCodec.CURRENT_VERSION; + + public static ByteBuf encode( + ByteBufAllocator allocator, + ByteBuf token, + long lastReceivedServerPos, + long firstAvailableClientPos) { + + ByteBuf byteBuf = FrameHeaderCodec.encodeStreamZero(allocator, FrameType.RESUME, 0); + byteBuf.writeInt(CURRENT_VERSION); + token.markReaderIndex(); + byteBuf.writeShort(token.readableBytes()); + byteBuf.writeBytes(token); + token.resetReaderIndex(); + byteBuf.writeLong(lastReceivedServerPos); + byteBuf.writeLong(firstAvailableClientPos); + + return byteBuf; + } + + public static int version(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.RESUME, byteBuf); + + byteBuf.markReaderIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size()); + int version = byteBuf.readInt(); + byteBuf.resetReaderIndex(); + + return version; + } + + public static ByteBuf token(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.RESUME, byteBuf); + + byteBuf.markReaderIndex(); + // header + version + int tokenPos = FrameHeaderCodec.size() + Integer.BYTES; + byteBuf.skipBytes(tokenPos); + // token + int tokenLength = byteBuf.readShort() & 0xFFFF; + ByteBuf token = byteBuf.readSlice(tokenLength); + byteBuf.resetReaderIndex(); + + return token; + } + + public static long lastReceivedServerPos(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.RESUME, byteBuf); + + byteBuf.markReaderIndex(); + // header + version + int tokenPos = FrameHeaderCodec.size() + Integer.BYTES; + byteBuf.skipBytes(tokenPos); + // token + int tokenLength = byteBuf.readShort() & 0xFFFF; + byteBuf.skipBytes(tokenLength); + long lastReceivedServerPos = byteBuf.readLong(); + byteBuf.resetReaderIndex(); + + return lastReceivedServerPos; + } + + public static long firstAvailableClientPos(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.RESUME, byteBuf); + + byteBuf.markReaderIndex(); + // header + version + int tokenPos = FrameHeaderCodec.size() + Integer.BYTES; + byteBuf.skipBytes(tokenPos); + // token + int tokenLength = byteBuf.readShort() & 0xFFFF; + byteBuf.skipBytes(tokenLength); + // last received server position + byteBuf.skipBytes(Long.BYTES); + long firstAvailableClientPos = byteBuf.readLong(); + byteBuf.resetReaderIndex(); + + return firstAvailableClientPos; + } + + public static ByteBuf generateResumeToken() { + UUID uuid = UUID.randomUUID(); + ByteBuf bb = Unpooled.buffer(16); + bb.writeLong(uuid.getMostSignificantBits()); + bb.writeLong(uuid.getLeastSignificantBits()); + return bb; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFrameCodec.java new file mode 100644 index 000000000..2b6951e49 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFrameCodec.java @@ -0,0 +1,22 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; + +public class ResumeOkFrameCodec { + + public static ByteBuf encode(final ByteBufAllocator allocator, long lastReceivedClientPos) { + ByteBuf byteBuf = FrameHeaderCodec.encodeStreamZero(allocator, FrameType.RESUME_OK, 0); + byteBuf.writeLong(lastReceivedClientPos); + return byteBuf; + } + + public static long lastReceivedClientPos(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.RESUME_OK, byteBuf); + byteBuf.markReaderIndex(); + long lastReceivedClientPosition = byteBuf.skipBytes(FrameHeaderCodec.size()).readLong(); + byteBuf.resetReaderIndex(); + + return lastReceivedClientPosition; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameCodec.java new file mode 100644 index 000000000..547e2436e --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameCodec.java @@ -0,0 +1,226 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.rsocket.Payload; +import java.nio.charset.StandardCharsets; +import reactor.util.annotation.Nullable; + +public class SetupFrameCodec { + /** + * A flag used to indicate that the client requires connection resumption, if possible (the frame + * contains a Resume Identification Token) + */ + public static final int FLAGS_RESUME_ENABLE = 0b00_1000_0000; + + /** A flag used to indicate that the client will honor LEASE sent by the server */ + public static final int FLAGS_WILL_HONOR_LEASE = 0b00_0100_0000; + + public static final int CURRENT_VERSION = VersionCodec.encode(1, 0); + + private static final int VERSION_FIELD_OFFSET = FrameHeaderCodec.size(); + private static final int KEEPALIVE_INTERVAL_FIELD_OFFSET = VERSION_FIELD_OFFSET + Integer.BYTES; + private static final int KEEPALIVE_MAX_LIFETIME_FIELD_OFFSET = + KEEPALIVE_INTERVAL_FIELD_OFFSET + Integer.BYTES; + private static final int VARIABLE_DATA_OFFSET = + KEEPALIVE_MAX_LIFETIME_FIELD_OFFSET + Integer.BYTES; + + public static ByteBuf encode( + final ByteBufAllocator allocator, + final boolean lease, + final int keepaliveInterval, + final int maxLifetime, + final String metadataMimeType, + final String dataMimeType, + final Payload setupPayload) { + return encode( + allocator, + lease, + keepaliveInterval, + maxLifetime, + Unpooled.EMPTY_BUFFER, + metadataMimeType, + dataMimeType, + setupPayload); + } + + public static ByteBuf encode( + final ByteBufAllocator allocator, + final boolean lease, + final int keepaliveInterval, + final int maxLifetime, + final ByteBuf resumeToken, + final String metadataMimeType, + final String dataMimeType, + final Payload setupPayload) { + + final ByteBuf data = setupPayload.sliceData(); + final boolean hasMetadata = setupPayload.hasMetadata(); + final ByteBuf metadata = hasMetadata ? setupPayload.sliceMetadata() : null; + + int flags = 0; + + if (resumeToken.readableBytes() > 0) { + flags |= FLAGS_RESUME_ENABLE; + } + + if (lease) { + flags |= FLAGS_WILL_HONOR_LEASE; + } + + if (hasMetadata) { + flags |= FrameHeaderCodec.FLAGS_M; + } + + final ByteBuf header = FrameHeaderCodec.encodeStreamZero(allocator, FrameType.SETUP, flags); + + header.writeInt(CURRENT_VERSION).writeInt(keepaliveInterval).writeInt(maxLifetime); + + if ((flags & FLAGS_RESUME_ENABLE) != 0) { + resumeToken.markReaderIndex(); + header.writeShort(resumeToken.readableBytes()).writeBytes(resumeToken); + resumeToken.resetReaderIndex(); + } + + // Write metadata mime-type + int length = ByteBufUtil.utf8Bytes(metadataMimeType); + header.writeByte(length); + ByteBufUtil.writeUtf8(header, metadataMimeType); + + // Write data mime-type + length = ByteBufUtil.utf8Bytes(dataMimeType); + header.writeByte(length); + ByteBufUtil.writeUtf8(header, dataMimeType); + + return FrameBodyCodec.encode(allocator, header, metadata, hasMetadata, data); + } + + public static int version(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.SETUP, byteBuf); + byteBuf.markReaderIndex(); + int version = byteBuf.skipBytes(VERSION_FIELD_OFFSET).readInt(); + byteBuf.resetReaderIndex(); + return version; + } + + public static String humanReadableVersion(ByteBuf byteBuf) { + int encodedVersion = version(byteBuf); + return VersionCodec.major(encodedVersion) + "." + VersionCodec.minor(encodedVersion); + } + + public static boolean isSupportedVersion(ByteBuf byteBuf) { + return CURRENT_VERSION == version(byteBuf); + } + + public static int resumeTokenLength(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + int tokenLength = byteBuf.skipBytes(VARIABLE_DATA_OFFSET).readShort() & 0xFFFF; + byteBuf.resetReaderIndex(); + return tokenLength; + } + + public static int keepAliveInterval(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + int keepAliveInterval = byteBuf.skipBytes(KEEPALIVE_INTERVAL_FIELD_OFFSET).readInt(); + byteBuf.resetReaderIndex(); + return keepAliveInterval; + } + + public static int keepAliveMaxLifetime(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + int keepAliveMaxLifetime = byteBuf.skipBytes(KEEPALIVE_MAX_LIFETIME_FIELD_OFFSET).readInt(); + byteBuf.resetReaderIndex(); + return keepAliveMaxLifetime; + } + + public static boolean honorLease(ByteBuf byteBuf) { + return (FLAGS_WILL_HONOR_LEASE & FrameHeaderCodec.flags(byteBuf)) == FLAGS_WILL_HONOR_LEASE; + } + + public static boolean resumeEnabled(ByteBuf byteBuf) { + return (FLAGS_RESUME_ENABLE & FrameHeaderCodec.flags(byteBuf)) == FLAGS_RESUME_ENABLE; + } + + public static ByteBuf resumeToken(ByteBuf byteBuf) { + if (resumeEnabled(byteBuf)) { + byteBuf.markReaderIndex(); + // header + int resumePos = + FrameHeaderCodec.size() + + + // version + Integer.BYTES + + + // keep-alive interval + Integer.BYTES + + + // keep-alive maxLifeTime + Integer.BYTES; + + int tokenLength = byteBuf.skipBytes(resumePos).readShort() & 0xFFFF; + ByteBuf resumeToken = byteBuf.readSlice(tokenLength); + byteBuf.resetReaderIndex(); + return resumeToken; + } else { + return Unpooled.EMPTY_BUFFER; + } + } + + public static String metadataMimeType(ByteBuf byteBuf) { + int skip = bytesToSkipToMimeType(byteBuf); + byteBuf.markReaderIndex(); + int length = byteBuf.skipBytes(skip).readUnsignedByte(); + String mimeType = byteBuf.slice(byteBuf.readerIndex(), length).toString(StandardCharsets.UTF_8); + byteBuf.resetReaderIndex(); + return mimeType; + } + + public static String dataMimeType(ByteBuf byteBuf) { + int skip = bytesToSkipToMimeType(byteBuf); + byteBuf.markReaderIndex(); + int metadataLength = byteBuf.skipBytes(skip).readByte(); + int dataLength = byteBuf.skipBytes(metadataLength).readByte(); + String mimeType = byteBuf.readSlice(dataLength).toString(StandardCharsets.UTF_8); + byteBuf.resetReaderIndex(); + return mimeType; + } + + @Nullable + public static ByteBuf metadata(ByteBuf byteBuf) { + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); + if (!hasMetadata) { + return null; + } + byteBuf.markReaderIndex(); + skipToPayload(byteBuf); + ByteBuf metadata = FrameBodyCodec.metadataWithoutMarking(byteBuf); + byteBuf.resetReaderIndex(); + return metadata; + } + + public static ByteBuf data(ByteBuf byteBuf) { + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); + byteBuf.markReaderIndex(); + skipToPayload(byteBuf); + ByteBuf data = FrameBodyCodec.dataWithoutMarking(byteBuf, hasMetadata); + byteBuf.resetReaderIndex(); + return data; + } + + private static int bytesToSkipToMimeType(ByteBuf byteBuf) { + int bytesToSkip = VARIABLE_DATA_OFFSET; + if ((FLAGS_RESUME_ENABLE & FrameHeaderCodec.flags(byteBuf)) == FLAGS_RESUME_ENABLE) { + bytesToSkip += resumeTokenLength(byteBuf) + Short.BYTES; + } + return bytesToSkip; + } + + private static void skipToPayload(ByteBuf byteBuf) { + int skip = bytesToSkipToMimeType(byteBuf); + byte length = byteBuf.skipBytes(skip).readByte(); + length = byteBuf.skipBytes(length).readByte(); + byteBuf.skipBytes(length); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java deleted file mode 100644 index 8e8fd05d2..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.rsocket.FrameType; -import java.nio.charset.StandardCharsets; - -public class SetupFrameFlyweight { - private SetupFrameFlyweight() {} - - public static final int FLAGS_RESUME_ENABLE = 0b00_1000_0000; - public static final int FLAGS_WILL_HONOR_LEASE = 0b00_0100_0000; - public static final int FLAGS_STRICT_INTERPRETATION = 0b00_0010_0000; - - public static final int VALID_FLAGS = - FLAGS_RESUME_ENABLE | FLAGS_WILL_HONOR_LEASE | FLAGS_STRICT_INTERPRETATION; - - public static final int CURRENT_VERSION = VersionFlyweight.encode(1, 0); - - // relative to start of passed offset - private static final int VERSION_FIELD_OFFSET = FrameHeaderFlyweight.FRAME_HEADER_LENGTH; - private static final int KEEPALIVE_INTERVAL_FIELD_OFFSET = VERSION_FIELD_OFFSET + Integer.BYTES; - private static final int MAX_LIFETIME_FIELD_OFFSET = - KEEPALIVE_INTERVAL_FIELD_OFFSET + Integer.BYTES; - private static final int VARIABLE_DATA_OFFSET = MAX_LIFETIME_FIELD_OFFSET + Integer.BYTES; - - public static int computeFrameLength( - final int flags, - final String metadataMimeType, - final String dataMimeType, - final int metadataLength, - final int dataLength) { - return computeFrameLength(flags, 0, metadataMimeType, dataMimeType, metadataLength, dataLength); - } - - private static int computeFrameLength( - final int flags, - final int resumeTokenLength, - final String metadataMimeType, - final String dataMimeType, - final int metadataLength, - final int dataLength) { - int length = - FrameHeaderFlyweight.computeFrameHeaderLength(FrameType.SETUP, metadataLength, dataLength); - - length += Integer.BYTES * 3; - - if ((flags & FLAGS_RESUME_ENABLE) != 0) { - length += Short.BYTES + resumeTokenLength; - } - - length += 1 + metadataMimeType.getBytes(StandardCharsets.UTF_8).length; - length += 1 + dataMimeType.getBytes(StandardCharsets.UTF_8).length; - - return length; - } - - public static int encode( - final ByteBuf byteBuf, - int flags, - final int keepaliveInterval, - final int maxLifetime, - final String metadataMimeType, - final String dataMimeType, - final ByteBuf metadata, - final ByteBuf data) { - if ((flags & FLAGS_RESUME_ENABLE) != 0) { - throw new IllegalArgumentException("RESUME_ENABLE not supported"); - } - - return encode( - byteBuf, - flags, - keepaliveInterval, - maxLifetime, - Unpooled.EMPTY_BUFFER, - metadataMimeType, - dataMimeType, - metadata, - data); - } - - // Only exposed for testing, other code shouldn't create frames with resumption tokens for now - static int encode( - final ByteBuf byteBuf, - int flags, - final int keepaliveInterval, - final int maxLifetime, - final ByteBuf resumeToken, - final String metadataMimeType, - final String dataMimeType, - final ByteBuf metadata, - final ByteBuf data) { - final int frameLength = - computeFrameLength( - flags, - resumeToken.readableBytes(), - metadataMimeType, - dataMimeType, - metadata.readableBytes(), - data.readableBytes()); - - int length = - FrameHeaderFlyweight.encodeFrameHeader(byteBuf, frameLength, flags, FrameType.SETUP, 0); - - byteBuf.setInt(VERSION_FIELD_OFFSET, CURRENT_VERSION); - byteBuf.setInt(KEEPALIVE_INTERVAL_FIELD_OFFSET, keepaliveInterval); - byteBuf.setInt(MAX_LIFETIME_FIELD_OFFSET, maxLifetime); - - length += Integer.BYTES * 3; - - if ((flags & FLAGS_RESUME_ENABLE) != 0) { - byteBuf.setShort(length, resumeToken.readableBytes()); - length += Short.BYTES; - int resumeTokenLength = resumeToken.readableBytes(); - byteBuf.setBytes(length, resumeToken, resumeTokenLength); - length += resumeTokenLength; - } - - length += putMimeType(byteBuf, length, metadataMimeType); - length += putMimeType(byteBuf, length, dataMimeType); - - length += FrameHeaderFlyweight.encodeMetadata(byteBuf, FrameType.SETUP, length, metadata); - length += FrameHeaderFlyweight.encodeData(byteBuf, length, data); - - return length; - } - - public static int version(final ByteBuf byteBuf) { - return byteBuf.getInt(VERSION_FIELD_OFFSET); - } - - public static int keepaliveInterval(final ByteBuf byteBuf) { - return byteBuf.getInt(KEEPALIVE_INTERVAL_FIELD_OFFSET); - } - - public static int maxLifetime(final ByteBuf byteBuf) { - return byteBuf.getInt(MAX_LIFETIME_FIELD_OFFSET); - } - - public static String metadataMimeType(final ByteBuf byteBuf) { - final byte[] bytes = getMimeType(byteBuf, metadataMimetypeOffset(byteBuf)); - return new String(bytes, StandardCharsets.UTF_8); - } - - public static String dataMimeType(final ByteBuf byteBuf) { - int fieldOffset = metadataMimetypeOffset(byteBuf); - - fieldOffset += 1 + byteBuf.getByte(fieldOffset); - - final byte[] bytes = getMimeType(byteBuf, fieldOffset); - return new String(bytes, StandardCharsets.UTF_8); - } - - public static int payloadOffset(final ByteBuf byteBuf) { - int fieldOffset = metadataMimetypeOffset(byteBuf); - - final int metadataMimeTypeLength = byteBuf.getByte(fieldOffset); - fieldOffset += 1 + metadataMimeTypeLength; - - final int dataMimeTypeLength = byteBuf.getByte(fieldOffset); - fieldOffset += 1 + dataMimeTypeLength; - - return fieldOffset; - } - - private static int metadataMimetypeOffset(final ByteBuf byteBuf) { - return VARIABLE_DATA_OFFSET + resumeTokenTotalLength(byteBuf); - } - - private static int resumeTokenTotalLength(final ByteBuf byteBuf) { - if ((FrameHeaderFlyweight.flags(byteBuf) & FLAGS_RESUME_ENABLE) == 0) { - return 0; - } else { - return Short.BYTES + byteBuf.getShort(VARIABLE_DATA_OFFSET); - } - } - - private static int putMimeType( - final ByteBuf byteBuf, final int fieldOffset, final String mimeType) { - byte[] bytes = mimeType.getBytes(StandardCharsets.UTF_8); - - byteBuf.setByte(fieldOffset, (byte) bytes.length); - byteBuf.setBytes(fieldOffset + 1, bytes); - - return 1 + bytes.length; - } - - private static byte[] getMimeType(final ByteBuf byteBuf, final int fieldOffset) { - final int length = byteBuf.getByte(fieldOffset); - final byte[] bytes = new byte[length]; - - byteBuf.getBytes(fieldOffset + 1, bytes); - return bytes; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/VersionFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/VersionCodec.java similarity index 83% rename from rsocket-core/src/main/java/io/rsocket/frame/VersionFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/VersionCodec.java index 639ea4615..35e4aa86a 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/VersionFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/VersionCodec.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,14 +16,14 @@ package io.rsocket.frame; -public class VersionFlyweight { +public class VersionCodec { public static int encode(int major, int minor) { return (major << 16) | (minor & 0xFFFF); } public static int major(int version) { - return version >> 16; + return version >> 16 & 0xFFFF; } public static int minor(int version) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java b/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java new file mode 100644 index 000000000..0d8063e0b --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java @@ -0,0 +1,69 @@ +package io.rsocket.frame.decoder; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.rsocket.Payload; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.MetadataPushFrameCodec; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; +import io.rsocket.util.DefaultPayload; +import java.nio.ByteBuffer; + +/** Default Frame decoder that copies the frames contents for easy of use. */ +class DefaultPayloadDecoder implements PayloadDecoder { + + @Override + public Payload apply(ByteBuf byteBuf) { + ByteBuf m; + ByteBuf d; + FrameType type = FrameHeaderCodec.frameType(byteBuf); + switch (type) { + case REQUEST_FNF: + d = RequestFireAndForgetFrameCodec.data(byteBuf); + m = RequestFireAndForgetFrameCodec.metadata(byteBuf); + break; + case REQUEST_RESPONSE: + d = RequestResponseFrameCodec.data(byteBuf); + m = RequestResponseFrameCodec.metadata(byteBuf); + break; + case REQUEST_STREAM: + d = RequestStreamFrameCodec.data(byteBuf); + m = RequestStreamFrameCodec.metadata(byteBuf); + break; + case REQUEST_CHANNEL: + d = RequestChannelFrameCodec.data(byteBuf); + m = RequestChannelFrameCodec.metadata(byteBuf); + break; + case NEXT: + case NEXT_COMPLETE: + d = PayloadFrameCodec.data(byteBuf); + m = PayloadFrameCodec.metadata(byteBuf); + break; + case METADATA_PUSH: + d = Unpooled.EMPTY_BUFFER; + m = MetadataPushFrameCodec.metadata(byteBuf); + break; + default: + throw new IllegalArgumentException("unsupported frame type: " + type); + } + + ByteBuffer data = ByteBuffer.allocate(d.readableBytes()); + data.put(d.nioBuffer()); + data.flip(); + + if (m != null) { + ByteBuffer metadata = ByteBuffer.allocate(m.readableBytes()); + metadata.put(m.nioBuffer()); + metadata.flip(); + + return DefaultPayload.create(data, metadata); + } + + return DefaultPayload.create(data); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/decoder/PayloadDecoder.java b/rsocket-core/src/main/java/io/rsocket/frame/decoder/PayloadDecoder.java new file mode 100644 index 000000000..197eca9b0 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/decoder/PayloadDecoder.java @@ -0,0 +1,10 @@ +package io.rsocket.frame.decoder; + +import io.netty.buffer.ByteBuf; +import io.rsocket.Payload; +import java.util.function.Function; + +public interface PayloadDecoder extends Function { + PayloadDecoder DEFAULT = new DefaultPayloadDecoder(); + PayloadDecoder ZERO_COPY = new ZeroCopyPayloadDecoder(); +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/decoder/ZeroCopyPayloadDecoder.java b/rsocket-core/src/main/java/io/rsocket/frame/decoder/ZeroCopyPayloadDecoder.java new file mode 100644 index 000000000..3a0dc7bb5 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/decoder/ZeroCopyPayloadDecoder.java @@ -0,0 +1,58 @@ +package io.rsocket.frame.decoder; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.rsocket.Payload; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.MetadataPushFrameCodec; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; +import io.rsocket.util.ByteBufPayload; + +/** + * Frame decoder that decodes a frame to a payload without copying. The caller is responsible for + * for releasing the payload to free memory when they no long need it. + */ +public class ZeroCopyPayloadDecoder implements PayloadDecoder { + @Override + public Payload apply(ByteBuf byteBuf) { + ByteBuf m; + ByteBuf d; + FrameType type = FrameHeaderCodec.frameType(byteBuf); + switch (type) { + case REQUEST_FNF: + d = RequestFireAndForgetFrameCodec.data(byteBuf); + m = RequestFireAndForgetFrameCodec.metadata(byteBuf); + break; + case REQUEST_RESPONSE: + d = RequestResponseFrameCodec.data(byteBuf); + m = RequestResponseFrameCodec.metadata(byteBuf); + break; + case REQUEST_STREAM: + d = RequestStreamFrameCodec.data(byteBuf); + m = RequestStreamFrameCodec.metadata(byteBuf); + break; + case REQUEST_CHANNEL: + d = RequestChannelFrameCodec.data(byteBuf); + m = RequestChannelFrameCodec.metadata(byteBuf); + break; + case NEXT: + case NEXT_COMPLETE: + d = PayloadFrameCodec.data(byteBuf); + m = PayloadFrameCodec.metadata(byteBuf); + break; + case METADATA_PUSH: + d = Unpooled.EMPTY_BUFFER; + m = MetadataPushFrameCodec.metadata(byteBuf); + break; + default: + throw new IllegalArgumentException("unsupported frame type: " + type); + } + + return ByteBufPayload.create(d.retain(), m != null ? m.retain() : null); + } +} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/NotConnectedException.java b/rsocket-core/src/main/java/io/rsocket/frame/decoder/package-info.java similarity index 62% rename from rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/NotConnectedException.java rename to rsocket-core/src/main/java/io/rsocket/frame/decoder/package-info.java index b1666db07..82e8acaf3 100644 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/NotConnectedException.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/decoder/package-info.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,14 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.aeron.internal; -public class NotConnectedException extends RuntimeException { - public NotConnectedException() { - super(); - } +/** + * Support for encoding and decoding of RSocket frames to and from {@link io.rsocket.Payload + * Payload}. + */ +@NonNullApi +package io.rsocket.frame.decoder; - public NotConnectedException(String message) { - super(message); - } -} +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/package-info.java b/rsocket-core/src/main/java/io/rsocket/frame/package-info.java index d177a5eb9..69f6d6860 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/package-info.java @@ -1,18 +1,24 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -@javax.annotation.ParametersAreNonnullByDefault +/** + * Support for encoding and decoding of RSocket frames to and from {@link io.rsocket.Payload + * Payload}. + */ +@NonNullApi package io.rsocket.frame; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java new file mode 100644 index 000000000..9668e5e18 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java @@ -0,0 +1,30 @@ +package io.rsocket.internal; + +import io.rsocket.DuplexConnection; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; + +public abstract class BaseDuplexConnection implements DuplexConnection { + private MonoProcessor onClose = MonoProcessor.create(); + + public BaseDuplexConnection() { + onClose.doFinally(s -> doOnClose()).subscribe(); + } + + protected abstract void doOnClose(); + + @Override + public final Mono onClose() { + return onClose; + } + + @Override + public final void dispose() { + onClose.onComplete(); + } + + @Override + public final boolean isDisposed() { + return onClose.isDisposed(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java index 9ac2e2a1a..48ae62906 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,11 +16,14 @@ package io.rsocket.internal; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Closeable; import io.rsocket.DuplexConnection; -import io.rsocket.Frame; -import io.rsocket.FrameType; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameUtil; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; -import io.rsocket.plugins.PluginRegistry; +import io.rsocket.plugins.InitializingInterceptorRegistry; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,55 +43,79 @@ *

The only way to differentiate these two frames is determining whether the stream Id is odd or * even. Even IDs are for the streams initiated by server and odds are for streams initiated by the * client. - * - *

*/ -public class ClientServerInputMultiplexer { +public class ClientServerInputMultiplexer implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger("io.rsocket.FrameLogger"); + private static final InitializingInterceptorRegistry emptyInterceptorRegistry = + new InitializingInterceptorRegistry(); - private final DuplexConnection streamZeroConnection; + private final DuplexConnection setupConnection; private final DuplexConnection serverConnection; private final DuplexConnection clientConnection; private final DuplexConnection source; + private final DuplexConnection clientServerConnection; + + private boolean setupReceived; - public ClientServerInputMultiplexer(DuplexConnection source, PluginRegistry plugins) { + public ClientServerInputMultiplexer(DuplexConnection source) { + this(source, emptyInterceptorRegistry, false); + } + + public ClientServerInputMultiplexer( + DuplexConnection source, InitializingInterceptorRegistry registry, boolean isClient) { this.source = source; - final MonoProcessor> streamZero = MonoProcessor.create(); - final MonoProcessor> server = MonoProcessor.create(); - final MonoProcessor> client = MonoProcessor.create(); + final MonoProcessor> setup = MonoProcessor.create(); + final MonoProcessor> server = MonoProcessor.create(); + final MonoProcessor> client = MonoProcessor.create(); - source = plugins.applyConnection(Type.SOURCE, source); - streamZeroConnection = - plugins.applyConnection(Type.STREAM_ZERO, new InternalDuplexConnection(source, streamZero)); + source = registry.initConnection(Type.SOURCE, source); + setupConnection = + registry.initConnection(Type.SETUP, new InternalDuplexConnection(source, setup)); serverConnection = - plugins.applyConnection(Type.SERVER, new InternalDuplexConnection(source, server)); + registry.initConnection(Type.SERVER, new InternalDuplexConnection(source, server)); clientConnection = - plugins.applyConnection(Type.CLIENT, new InternalDuplexConnection(source, client)); + registry.initConnection(Type.CLIENT, new InternalDuplexConnection(source, client)); + clientServerConnection = new InternalDuplexConnection(source, client, server); source .receive() .groupBy( frame -> { - int streamId = frame.getStreamId(); + int streamId = FrameHeaderCodec.streamId(frame); final Type type; if (streamId == 0) { - if (frame.getType() == FrameType.SETUP) { - type = Type.STREAM_ZERO; - } else { - type = Type.CLIENT; + switch (FrameHeaderCodec.frameType(frame)) { + case SETUP: + case RESUME: + case RESUME_OK: + type = Type.SETUP; + setupReceived = true; + break; + case LEASE: + case KEEPALIVE: + case ERROR: + type = isClient ? Type.CLIENT : Type.SERVER; + break; + default: + type = isClient ? Type.SERVER : Type.CLIENT; } } else if ((streamId & 0b1) == 0) { type = Type.SERVER; } else { type = Type.CLIENT; } + if (!isClient && type != Type.SETUP && !setupReceived) { + frame.release(); + throw new IllegalStateException( + "SETUP or LEASE frame must be received before any others."); + } return type; }) .subscribe( group -> { switch (group.key()) { - case STREAM_ZERO: - streamZero.onNext(group); + case SETUP: + setup.onNext(group); break; case SERVER: @@ -99,9 +126,18 @@ public ClientServerInputMultiplexer(DuplexConnection source, PluginRegistry plug client.onNext(group); break; } + }, + ex -> { + setup.onError(ex); + server.onError(ex); + client.onError(ex); }); } + public DuplexConnection asClientServerConnection() { + return clientServerConnection; + } + public DuplexConnection asServerConnection() { return serverConnection; } @@ -110,58 +146,85 @@ public DuplexConnection asClientConnection() { return clientConnection; } - public DuplexConnection asStreamZeroConnection() { - return streamZeroConnection; + public DuplexConnection asSetupConnection() { + return setupConnection; + } + + @Override + public void dispose() { + source.dispose(); } - public Mono close() { - return source.close(); + @Override + public boolean isDisposed() { + return source.isDisposed(); + } + + @Override + public Mono onClose() { + return source.onClose(); } private static class InternalDuplexConnection implements DuplexConnection { private final DuplexConnection source; - private final MonoProcessor> processor; + private final MonoProcessor>[] processors; private final boolean debugEnabled; - public InternalDuplexConnection(DuplexConnection source, MonoProcessor> processor) { + @SafeVarargs + public InternalDuplexConnection( + DuplexConnection source, MonoProcessor>... processors) { this.source = source; - this.processor = processor; + this.processors = processors; this.debugEnabled = LOGGER.isDebugEnabled(); } @Override - public Mono send(Publisher frame) { + public Mono send(Publisher frame) { if (debugEnabled) { - frame = Flux.from(frame).doOnNext(f -> LOGGER.debug("sending -> " + f.toString())); + frame = Flux.from(frame).doOnNext(f -> LOGGER.debug("sending -> " + FrameUtil.toString(f))); } return source.send(frame); } @Override - public Mono sendOne(Frame frame) { + public Mono sendOne(ByteBuf frame) { if (debugEnabled) { - LOGGER.debug("sending -> " + frame.toString()); + LOGGER.debug("sending -> " + FrameUtil.toString(frame)); } return source.sendOne(frame); } @Override - public Flux receive() { - return processor.flatMapMany( - f -> { - if (debugEnabled) { - return f.doOnNext(frame -> LOGGER.debug("receiving -> " + frame.toString())); - } else { - return f; - } - }); + public Flux receive() { + return Flux.fromArray(processors) + .flatMap( + p -> + p.flatMapMany( + f -> { + if (debugEnabled) { + return f.doOnNext( + frame -> LOGGER.debug("receiving -> " + FrameUtil.toString(frame))); + } else { + return f; + } + })); + } + + @Override + public ByteBufAllocator alloc() { + return source.alloc(); + } + + @Override + public void dispose() { + source.dispose(); } @Override - public Mono close() { - return source.close(); + public boolean isDisposed() { + return source.isDisposed(); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java b/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java deleted file mode 100755 index cc4ac5443..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.internal; - -import java.util.concurrent.atomic.AtomicBoolean; -import javax.annotation.Nullable; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Operators; - -/** */ -public class LimitableRequestPublisher extends Flux implements Subscription { - private final Publisher source; - - private final AtomicBoolean canceled; - - private long internalRequested; - - private long externalRequested; - - private volatile boolean subscribed; - - private volatile @Nullable Subscription internalSubscription; - - private LimitableRequestPublisher(Publisher source) { - this.source = source; - this.canceled = new AtomicBoolean(); - } - - public static LimitableRequestPublisher wrap(Publisher source) { - return new LimitableRequestPublisher<>(source); - } - - @Override - public void subscribe(Subscriber destination) { - synchronized (this) { - if (subscribed) { - throw new IllegalStateException("only one subscriber at a time"); - } - - subscribed = true; - } - - destination.onSubscribe(new InnerSubscription()); - source.subscribe(new InnerSubscriber<>(destination)); - } - - public void increaseRequestLimit(long n) { - synchronized (this) { - externalRequested = Operators.addCap(n, externalRequested); - } - - requestN(); - } - - @Override - public void request(long n) { - increaseRequestLimit(n); - } - - private void requestN() { - long r; - synchronized (this) { - if (internalSubscription == null) { - return; - } - - r = Math.min(internalRequested, externalRequested); - externalRequested -= r; - internalRequested -= r; - } - - if (r > 0) { - internalSubscription.request(r); - } - } - - public void cancel() { - if (canceled.compareAndSet(false, true) && internalSubscription != null) { - internalSubscription.cancel(); - internalSubscription = null; - subscribed = false; - } - } - - private class InnerSubscriber implements Subscriber { - Subscriber destination; - - public InnerSubscriber(Subscriber destination) { - this.destination = destination; - } - - @Override - public void onSubscribe(Subscription s) { - synchronized (LimitableRequestPublisher.this) { - LimitableRequestPublisher.this.internalSubscription = s; - - if (canceled.get()) { - s.cancel(); - subscribed = false; - LimitableRequestPublisher.this.internalSubscription = null; - } - } - - requestN(); - } - - @Override - public void onNext(T t) { - try { - destination.onNext(t); - } catch (Throwable e) { - onError(e); - } - } - - @Override - public void onError(Throwable t) { - destination.onError(t); - } - - @Override - public void onComplete() { - destination.onComplete(); - } - } - - private class InnerSubscription implements Subscription { - @Override - public void request(long n) { - synchronized (LimitableRequestPublisher.this) { - internalRequested = Operators.addCap(n, internalRequested); - } - - requestN(); - } - - @Override - public void cancel() { - LimitableRequestPublisher.this.cancel(); - } - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/SynchronizedIntObjectHashMap.java b/rsocket-core/src/main/java/io/rsocket/internal/SynchronizedIntObjectHashMap.java new file mode 100644 index 000000000..fd6bf0aed --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/SynchronizedIntObjectHashMap.java @@ -0,0 +1,748 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package io.rsocket.internal; + +import static io.netty.util.internal.MathUtil.safeFindNextPositivePowerOfTwo; + +import io.netty.util.collection.IntObjectMap; +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + * A hash map implementation of {@link IntObjectMap} that uses open addressing for keys. To minimize + * the memory footprint, this class uses open addressing rather than chaining. Collisions are + * resolved using linear probing. Deletions implement compaction, so cost of remove can approach + * O(N) for full maps, which makes a small loadFactor recommended. + * + * @param The value type stored in the map. + */ +public class SynchronizedIntObjectHashMap implements IntObjectMap { + + /** Default initial capacity. Used if not specified in the constructor */ + public static final int DEFAULT_CAPACITY = 8; + + /** Default load factor. Used if not specified in the constructor */ + public static final float DEFAULT_LOAD_FACTOR = 0.5f; + + /** + * Placeholder for null values, so we can use the actual null to mean available. (Better than + * using a placeholder for available: less references for GC processing.) + */ + private static final Object NULL_VALUE = new Object(); + + /** The maximum number of elements allowed without allocating more space. */ + private int maxSize; + + /** The load factor for the map. Used to calculate {@link #maxSize}. */ + private final float loadFactor; + + private int[] keys; + private V[] values; + private int size; + private int mask; + + private final Set keySet = new KeySet(); + private final Set> entrySet = new EntrySet(); + private final Iterable> entries = PrimitiveIterator::new; + + public SynchronizedIntObjectHashMap() { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); + } + + public SynchronizedIntObjectHashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + public SynchronizedIntObjectHashMap(int initialCapacity, float loadFactor) { + if (loadFactor <= 0.0f || loadFactor > 1.0f) { + // Cannot exceed 1 because we can never store more than capacity elements; + // using a bigger loadFactor would trigger rehashing before the desired load is reached. + throw new IllegalArgumentException("loadFactor must be > 0 and <= 1"); + } + + this.loadFactor = loadFactor; + + // Adjust the initial capacity if necessary. + int capacity = safeFindNextPositivePowerOfTwo(initialCapacity); + mask = capacity - 1; + + // Allocate the arrays. + keys = new int[capacity]; + @SuppressWarnings({"unchecked", "SuspiciousArrayCast"}) + V[] temp = (V[]) new Object[capacity]; + values = temp; + + // Initialize the maximum size value. + maxSize = calcMaxSize(capacity); + } + + private static T toExternal(T value) { + assert value != null : "null is not a legitimate internal value. Concurrent Modification?"; + return value == NULL_VALUE ? null : value; + } + + @SuppressWarnings("unchecked") + private static T toInternal(T value) { + return value == null ? (T) NULL_VALUE : value; + } + + public synchronized V[] getValuesCopy() { + V[] values = this.values; + return Arrays.copyOf(values, values.length); + } + + @Override + public synchronized V get(int key) { + int index = indexOf(key); + return index == -1 ? null : toExternal(values[index]); + } + + @Override + public synchronized V put(int key, V value) { + int startIndex = hashIndex(key); + int index = startIndex; + + for (; ; ) { + if (values[index] == null) { + // Found empty slot, use it. + keys[index] = key; + values[index] = toInternal(value); + growSize(); + return null; + } + if (keys[index] == key) { + // Found existing entry with this key, just replace the value. + V previousValue = values[index]; + values[index] = toInternal(value); + return toExternal(previousValue); + } + + // Conflict, keep probing ... + if ((index = probeNext(index)) == startIndex) { + // Can only happen if the map was full at MAX_ARRAY_SIZE and couldn't grow. + throw new IllegalStateException("Unable to insert"); + } + } + } + + @Override + public synchronized void putAll(Map sourceMap) { + if (sourceMap instanceof SynchronizedIntObjectHashMap) { + // Optimization - iterate through the arrays. + @SuppressWarnings("unchecked") + SynchronizedIntObjectHashMap source = (SynchronizedIntObjectHashMap) sourceMap; + for (int i = 0; i < source.values.length; ++i) { + V sourceValue = source.values[i]; + if (sourceValue != null) { + put(source.keys[i], sourceValue); + } + } + return; + } + + // Otherwise, just add each entry. + for (Entry entry : sourceMap.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public synchronized V remove(int key) { + int index = indexOf(key); + if (index == -1) { + return null; + } + + V prev = values[index]; + removeAt(index); + return toExternal(prev); + } + + @Override + public synchronized int size() { + return size; + } + + @Override + public synchronized boolean isEmpty() { + return size == 0; + } + + @Override + public synchronized void clear() { + Arrays.fill(keys, 0); + Arrays.fill(values, null); + size = 0; + } + + @Override + public synchronized boolean containsKey(int key) { + return indexOf(key) >= 0; + } + + @Override + public synchronized boolean containsValue(Object value) { + @SuppressWarnings("unchecked") + V v1 = toInternal((V) value); + for (V v2 : values) { + // The map supports null values; this will be matched as NULL_VALUE.equals(NULL_VALUE). + if (v2 != null && v2.equals(v1)) { + return true; + } + } + return false; + } + + @Override + public synchronized Iterable> entries() { + return entries; + } + + @Override + public synchronized Collection values() { + return new AbstractCollection() { + @Override + public Iterator iterator() { + return new Iterator() { + final PrimitiveIterator iter = new PrimitiveIterator(); + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public V next() { + return iter.next().value(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public int size() { + return size; + } + }; + } + + @Override + public synchronized int hashCode() { + // Hashcode is based on all non-zero, valid keys. We have to scan the whole keys + // array, which may have different lengths for two maps of same size(), so the + // capacity cannot be used as input for hashing but the size can. + int hash = size; + for (int key : keys) { + // 0 can be a valid key or unused slot, but won't impact the hashcode in either case. + // This way we can use a cheap loop without conditionals, or hard-to-unroll operations, + // or the devastatingly bad memory locality of visiting value objects. + // Also, it's important to use a hash function that does not depend on the ordering + // of terms, only their values; since the map is an unordered collection and + // entries can end up in different positions in different maps that have the same + // elements, but with different history of puts/removes, due to conflicts. + hash ^= hashCode(key); + } + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof IntObjectMap)) { + return false; + } + @SuppressWarnings("rawtypes") + IntObjectMap other = (IntObjectMap) obj; + synchronized (this) { + if (size != other.size()) { + return false; + } + for (int i = 0; i < values.length; ++i) { + V value = values[i]; + if (value != null) { + int key = keys[i]; + Object otherValue = other.get(key); + if (value == NULL_VALUE) { + if (otherValue != null) { + return false; + } + } else if (!value.equals(otherValue)) { + return false; + } + } + } + } + return true; + } + + @Override + public synchronized boolean containsKey(Object key) { + return containsKey(objectToKey(key)); + } + + @Override + public synchronized V get(Object key) { + return get(objectToKey(key)); + } + + @Override + public synchronized V put(Integer key, V value) { + return put(objectToKey(key), value); + } + + @Override + public synchronized V remove(Object key) { + return remove(objectToKey(key)); + } + + @Override + public synchronized Set keySet() { + return keySet; + } + + @Override + public synchronized Set> entrySet() { + return entrySet; + } + + private int objectToKey(Object key) { + return ((Integer) key).intValue(); + } + + /** + * Locates the index for the given key. This method probes using double hashing. + * + * @param key the key for an entry in the map. + * @return the index where the key was found, or {@code -1} if no entry is found for that key. + */ + private int indexOf(int key) { + int startIndex = hashIndex(key); + int index = startIndex; + + for (; ; ) { + if (values[index] == null) { + // It's available, so no chance that this value exists anywhere in the map. + return -1; + } + if (key == keys[index]) { + return index; + } + + // Conflict, keep probing ... + if ((index = probeNext(index)) == startIndex) { + return -1; + } + } + } + + /** Returns the hashed index for the given key. */ + private int hashIndex(int key) { + // The array lengths are always a power of two, so we can use a bitmask to stay inside the array + // bounds. + return hashCode(key) & mask; + } + + /** Returns the hash code for the key. */ + private static int hashCode(int key) { + return key; + } + + /** Get the next sequential index after {@code index} and wraps if necessary. */ + private int probeNext(int index) { + // The array lengths are always a power of two, so we can use a bitmask to stay inside the array + // bounds. + return (index + 1) & mask; + } + + /** Grows the map size after an insertion. If necessary, performs a rehash of the map. */ + private void growSize() { + size++; + + if (size > maxSize) { + if (keys.length == Integer.MAX_VALUE) { + throw new IllegalStateException("Max capacity reached at size=" + size); + } + + // Double the capacity. + rehash(keys.length << 1); + } + } + + /** + * Removes entry at the given index position. Also performs opportunistic, incremental rehashing + * if necessary to not break conflict chains. + * + * @param index the index position of the element to remove. + * @return {@code true} if the next item was moved back. {@code false} otherwise. + */ + private boolean removeAt(final int index) { + --size; + // Clearing the key is not strictly necessary (for GC like in a regular collection), + // but recommended for security. The memory location is still fresh in the cache anyway. + keys[index] = 0; + values[index] = null; + + // In the interval from index to the next available entry, the arrays may have entries + // that are displaced from their base position due to prior conflicts. Iterate these + // entries and move them back if possible, optimizing future lookups. + // Knuth Section 6.4 Algorithm R, also used by the JDK's IdentityHashMap. + + int nextFree = index; + int i = probeNext(index); + for (V value = values[i]; value != null; value = values[i = probeNext(i)]) { + int key = keys[i]; + int bucket = hashIndex(key); + if (i < bucket && (bucket <= nextFree || nextFree <= i) + || bucket <= nextFree && nextFree <= i) { + // Move the displaced entry "back" to the first available position. + keys[nextFree] = key; + values[nextFree] = value; + // Put the first entry after the displaced entry + keys[i] = 0; + values[i] = null; + nextFree = i; + } + } + return nextFree != index; + } + + /** Calculates the maximum size allowed before rehashing. */ + private int calcMaxSize(int capacity) { + // Clip the upper bound so that there will always be at least one available slot. + int upperBound = capacity - 1; + return Math.min(upperBound, (int) (capacity * loadFactor)); + } + + /** + * Rehashes the map for the given capacity. + * + * @param newCapacity the new capacity for the map. + */ + private void rehash(int newCapacity) { + int[] oldKeys = keys; + V[] oldVals = values; + + keys = new int[newCapacity]; + @SuppressWarnings({"unchecked", "SuspiciousArrayCast"}) + V[] temp = (V[]) new Object[newCapacity]; + values = temp; + + maxSize = calcMaxSize(newCapacity); + mask = newCapacity - 1; + + // Insert to the new arrays. + for (int i = 0; i < oldVals.length; ++i) { + V oldVal = oldVals[i]; + if (oldVal != null) { + // Inlined put(), but much simpler: we don't need to worry about + // duplicated keys, growing/rehashing, or failing to insert. + int oldKey = oldKeys[i]; + int index = hashIndex(oldKey); + + for (; ; ) { + if (values[index] == null) { + keys[index] = oldKey; + values[index] = oldVal; + break; + } + + // Conflict, keep probing. Can wrap around, but never reaches startIndex again. + index = probeNext(index); + } + } + } + } + + @Override + public synchronized String toString() { + if (isEmpty()) { + return "{}"; + } + StringBuilder sb = new StringBuilder(4 * size); + sb.append('{'); + boolean first = true; + for (int i = 0; i < values.length; ++i) { + V value = values[i]; + if (value != null) { + if (!first) { + sb.append(", "); + } + sb.append(keyToString(keys[i])) + .append('=') + .append(value == this ? "(this Map)" : toExternal(value)); + first = false; + } + } + return sb.append('}').toString(); + } + + /** + * Helper method called by {@link #toString()} in order to convert a single map key into a string. + * This is protected to allow subclasses to override the appearance of a given key. + */ + protected String keyToString(int key) { + return Integer.toString(key); + } + + /** Set implementation for iterating over the entries of the map. */ + private final class EntrySet extends AbstractSet> { + @Override + public Iterator> iterator() { + return new MapIterator(); + } + + @Override + public int size() { + return SynchronizedIntObjectHashMap.this.size(); + } + } + + /** Set implementation for iterating over the keys. */ + private final class KeySet extends AbstractSet { + @Override + public int size() { + return SynchronizedIntObjectHashMap.this.size(); + } + + @Override + public boolean contains(Object o) { + return SynchronizedIntObjectHashMap.this.containsKey(o); + } + + @Override + public boolean remove(Object o) { + return SynchronizedIntObjectHashMap.this.remove(o) != null; + } + + @Override + public boolean retainAll(Collection retainedKeys) { + synchronized (SynchronizedIntObjectHashMap.this) { + boolean changed = false; + for (Iterator> iter = entries().iterator(); iter.hasNext(); ) { + PrimitiveEntry entry = iter.next(); + if (!retainedKeys.contains(entry.key())) { + changed = true; + iter.remove(); + } + } + return changed; + } + } + + @Override + public void clear() { + SynchronizedIntObjectHashMap.this.clear(); + } + + @Override + public Iterator iterator() { + synchronized (SynchronizedIntObjectHashMap.this) { + final Iterator> iter = entrySet.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + synchronized (SynchronizedIntObjectHashMap.this) { + return iter.hasNext(); + } + } + + @Override + public Integer next() { + synchronized (SynchronizedIntObjectHashMap.this) { + return iter.next().getKey(); + } + } + + @Override + public void remove() { + synchronized (SynchronizedIntObjectHashMap.this) { + iter.remove(); + } + } + }; + } + } + } + + /** + * Iterator over primitive entries. Entry key/values are overwritten by each call to {@link + * #next()}. + */ + private final class PrimitiveIterator implements Iterator>, PrimitiveEntry { + private int prevIndex = -1; + private int nextIndex = -1; + private int entryIndex = -1; + + private void scanNext() { + while (++nextIndex != values.length && values[nextIndex] == null) {} + } + + @Override + public boolean hasNext() { + synchronized (SynchronizedIntObjectHashMap.this) { + if (nextIndex == -1) { + scanNext(); + } + return nextIndex != values.length; + } + } + + @Override + public PrimitiveEntry next() { + synchronized (SynchronizedIntObjectHashMap.this) { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + prevIndex = nextIndex; + scanNext(); + + // Always return the same Entry object, just change its index each time. + entryIndex = prevIndex; + return this; + } + } + + @Override + public void remove() { + synchronized (SynchronizedIntObjectHashMap.this) { + if (prevIndex == -1) { + throw new IllegalStateException("next must be called before each remove."); + } + if (removeAt(prevIndex)) { + // removeAt may move elements "back" in the array if they have been displaced because + // their + // spot in the + // array was occupied when they were inserted. If this occurs then the nextIndex is now + // invalid and + // should instead point to the prevIndex which now holds an element which was "moved + // back". + nextIndex = prevIndex; + } + prevIndex = -1; + } + } + + // Entry implementation. Since this implementation uses a single Entry, we coalesce that + // into the Iterator object (potentially making loop optimization much easier). + + @Override + public int key() { + synchronized (SynchronizedIntObjectHashMap.this) { + return keys[entryIndex]; + } + } + + @Override + public V value() { + synchronized (SynchronizedIntObjectHashMap.this) { + return toExternal(values[entryIndex]); + } + } + + @Override + public void setValue(V value) { + synchronized (SynchronizedIntObjectHashMap.this) { + values[entryIndex] = toInternal(value); + } + } + } + + /** Iterator used by the {@link Map} interface. */ + private final class MapIterator implements Iterator> { + private final PrimitiveIterator iter = new PrimitiveIterator(); + + @Override + public boolean hasNext() { + synchronized (SynchronizedIntObjectHashMap.this) { + return iter.hasNext(); + } + } + + @Override + public Entry next() { + synchronized (SynchronizedIntObjectHashMap.this) { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + iter.next(); + + return new MapEntry(iter.entryIndex); + } + } + + @Override + public void remove() { + synchronized (SynchronizedIntObjectHashMap.this) { + iter.remove(); + } + } + } + + /** A single entry in the map. */ + final class MapEntry implements Entry { + private final int entryIndex; + + MapEntry(int entryIndex) { + this.entryIndex = entryIndex; + } + + @Override + public Integer getKey() { + synchronized (SynchronizedIntObjectHashMap.this) { + verifyExists(); + return keys[entryIndex]; + } + } + + @Override + public V getValue() { + synchronized (SynchronizedIntObjectHashMap.this) { + verifyExists(); + return toExternal(values[entryIndex]); + } + } + + @Override + public V setValue(V value) { + synchronized (SynchronizedIntObjectHashMap.this) { + verifyExists(); + V prevValue = toExternal(values[entryIndex]); + values[entryIndex] = toInternal(value); + return prevValue; + } + } + + private void verifyExists() { + if (values[entryIndex] == null) { + throw new IllegalStateException("The map entry has been removed"); + } + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java new file mode 100644 index 000000000..94d5e9a7a --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -0,0 +1,493 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.internal; + +import io.netty.util.ReferenceCounted; +import io.rsocket.internal.jctools.queues.MpscUnboundedArrayQueue; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Exceptions; +import reactor.core.Fuseable; +import reactor.core.publisher.FluxProcessor; +import reactor.core.publisher.Operators; +import reactor.util.annotation.Nullable; +import reactor.util.concurrent.Queues; +import reactor.util.context.Context; + +/** + * A Processor implementation that takes a custom queue and allows only a single subscriber. + * + *

The implementation keeps the order of signals. + * + * @param the input and output type + */ +public final class UnboundedProcessor extends FluxProcessor + implements Fuseable.QueueSubscription, Fuseable { + + final Queue queue; + final Queue priorityQueue; + + volatile boolean done; + Throwable error; + // important to not loose the downstream too early and miss discard hook, while + // having relevant hasDownstreams() + boolean hasDownstream; + volatile CoreSubscriber actual; + + volatile boolean cancelled; + + volatile boolean terminated; + + volatile int once; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater ONCE = + AtomicIntegerFieldUpdater.newUpdater(UnboundedProcessor.class, "once"); + + volatile int wip; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(UnboundedProcessor.class, "wip"); + + volatile int discardGuard; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater DISCARD_GUARD = + AtomicIntegerFieldUpdater.newUpdater(UnboundedProcessor.class, "discardGuard"); + + volatile long requested; + + @SuppressWarnings("rawtypes") + static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(UnboundedProcessor.class, "requested"); + + boolean outputFused; + + public UnboundedProcessor() { + this.queue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); + this.priorityQueue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); + } + + @Override + public int getBufferSize() { + return Integer.MAX_VALUE; + } + + @Override + public Object scanUnsafe(Attr key) { + if (Attr.BUFFERED == key) return queue.size(); + if (Attr.PREFETCH == key) return Integer.MAX_VALUE; + return super.scanUnsafe(key); + } + + void drainRegular(Subscriber a) { + int missed = 1; + + final Queue q = queue; + final Queue pq = priorityQueue; + + for (; ; ) { + + long r = requested; + long e = 0L; + + while (r != e) { + boolean d = done; + + T t = pq.poll(); + boolean empty = t == null; + if (empty) { + t = q.poll(); + empty = t == null; + } + + if (checkTerminated(d, empty, a)) { + if (!empty) { + release(t); + } + return; + } + + if (empty) { + break; + } + + a.onNext(t); + + e++; + } + + if (r == e) { + if (checkTerminated(done, q.isEmpty() && pq.isEmpty(), a)) { + return; + } + } + + if (e != 0 && r != Long.MAX_VALUE) { + REQUESTED.addAndGet(this, -e); + } + + missed = WIP.addAndGet(this, -missed); + if (missed == 0) { + break; + } + } + } + + void drainFused(Subscriber a) { + int missed = 1; + + for (; ; ) { + + if (cancelled) { + if (terminated) { + this.clear(); + } + hasDownstream = false; + return; + } + + boolean d = done; + + a.onNext(null); + + if (d) { + hasDownstream = false; + + Throwable ex = error; + if (ex != null) { + a.onError(ex); + } else { + a.onComplete(); + } + return; + } + + missed = WIP.addAndGet(this, -missed); + if (missed == 0) { + break; + } + } + } + + public void drain() { + final int previousWip = WIP.getAndIncrement(this); + if (previousWip != 0) { + if (previousWip < 0 || terminated) { + this.clear(); + } + return; + } + + int missed = 1; + + for (; ; ) { + Subscriber a = actual; + if (a != null) { + + if (outputFused) { + drainFused(a); + } else { + drainRegular(a); + } + return; + } + + missed = WIP.addAndGet(this, -missed); + if (missed == 0) { + break; + } + } + } + + boolean checkTerminated(boolean d, boolean empty, Subscriber a) { + if (cancelled) { + this.clear(); + hasDownstream = false; + return true; + } + if (d && empty) { + this.clear(); + Throwable e = error; + hasDownstream = false; + if (e != null) { + a.onError(e); + } else { + a.onComplete(); + } + return true; + } + + return false; + } + + @Override + public void onSubscribe(Subscription s) { + if (done || cancelled) { + s.cancel(); + } else { + s.request(Long.MAX_VALUE); + } + } + + @Override + public int getPrefetch() { + return Integer.MAX_VALUE; + } + + @Override + public Context currentContext() { + CoreSubscriber actual = this.actual; + return actual != null ? actual.currentContext() : Context.empty(); + } + + public void onNextPrioritized(T t) { + if (done || cancelled) { + Operators.onNextDropped(t, currentContext()); + release(t); + return; + } + + if (!priorityQueue.offer(t)) { + Throwable ex = + Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); + onError(Operators.onOperatorError(null, ex, t, currentContext())); + release(t); + return; + } + drain(); + } + + @Override + public void onNext(T t) { + if (done || cancelled) { + Operators.onNextDropped(t, currentContext()); + release(t); + return; + } + + if (!queue.offer(t)) { + Throwable ex = + Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); + onError(Operators.onOperatorError(null, ex, t, currentContext())); + release(t); + return; + } + drain(); + } + + @Override + public void onError(Throwable t) { + if (done || cancelled) { + Operators.onErrorDropped(t, currentContext()); + return; + } + + error = t; + done = true; + + drain(); + } + + @Override + public void onComplete() { + if (done || cancelled) { + return; + } + + done = true; + + drain(); + } + + @Override + public void subscribe(CoreSubscriber actual) { + Objects.requireNonNull(actual, "subscribe"); + if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { + + actual.onSubscribe(this); + this.actual = actual; + drain(); + } else { + Operators.error( + actual, + new IllegalStateException("UnboundedProcessor " + "allows only a single Subscriber")); + } + } + + @Override + public void request(long n) { + if (Operators.validate(n)) { + Operators.addCap(REQUESTED, this, n); + drain(); + } + } + + @Override + public void cancel() { + if (cancelled) { + return; + } + cancelled = true; + + if (WIP.getAndIncrement(this) == 0) { + if (!outputFused || terminated) { + this.clear(); + } + hasDownstream = false; + } + } + + @Override + @Nullable + public T poll() { + Queue pq = this.priorityQueue; + if (!pq.isEmpty()) { + return pq.poll(); + } + return queue.poll(); + } + + @Override + public int size() { + return priorityQueue.size() + queue.size(); + } + + @Override + public boolean isEmpty() { + return priorityQueue.isEmpty() && queue.isEmpty(); + } + + @Override + public void clear() { + terminated = true; + for (; ; ) { + int wip = this.wip; + + clearSafely(); + + if (WIP.compareAndSet(this, wip, Integer.MIN_VALUE)) { + return; + } + } + } + + void clearSafely() { + if (DISCARD_GUARD.getAndIncrement(this) != 0) { + return; + } + + int missed = 1; + + for (; ; ) { + T t; + while ((t = queue.poll()) != null) { + release(t); + } + while ((t = priorityQueue.poll()) != null) { + release(t); + } + + missed = DISCARD_GUARD.addAndGet(this, -missed); + if (missed == 0) { + break; + } + } + } + + @Override + public int requestFusion(int requestedMode) { + if ((requestedMode & Fuseable.ASYNC) != 0) { + outputFused = true; + return Fuseable.ASYNC; + } + return Fuseable.NONE; + } + + @Override + public void dispose() { + if (cancelled) { + return; + } + + error = new CancellationException("Disposed"); + done = true; + + if (WIP.getAndIncrement(this) == 0) { + cancelled = true; + final CoreSubscriber a = this.actual; + + if (!outputFused || terminated) { + clear(); + } + + if (a != null) { + try { + a.onError(error); + } catch (Throwable ignored) { + } + } + hasDownstream = false; + } + } + + @Override + public boolean isDisposed() { + return cancelled || done; + } + + @Override + public boolean isTerminated() { + return done; + } + + @Override + @Nullable + public Throwable getError() { + return error; + } + + @Override + public long downstreamCount() { + return hasDownstreams() ? 1L : 0L; + } + + @Override + public boolean hasDownstreams() { + return hasDownstream; + } + + void release(T t) { + if (t instanceof ReferenceCounted) { + ReferenceCounted refCounted = (ReferenceCounted) t; + if (refCounted.refCnt() > 0) { + try { + refCounted.release(); + } catch (Throwable ex) { + // no ops + } + } + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseLinkedQueue.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseLinkedQueue.java new file mode 100644 index 000000000..a99ef8a49 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseLinkedQueue.java @@ -0,0 +1,302 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.internal.jctools.queues; + +import static io.rsocket.internal.jctools.queues.UnsafeAccess.UNSAFE; +import static io.rsocket.internal.jctools.queues.UnsafeAccess.fieldOffset; + +import java.util.AbstractQueue; +import java.util.Iterator; + +abstract class BaseLinkedQueuePad0 extends AbstractQueue implements MessagePassingQueue { + byte b000, b001, b002, b003, b004, b005, b006, b007; // 8b + byte b010, b011, b012, b013, b014, b015, b016, b017; // 16b + byte b020, b021, b022, b023, b024, b025, b026, b027; // 24b + byte b030, b031, b032, b033, b034, b035, b036, b037; // 32b + byte b040, b041, b042, b043, b044, b045, b046, b047; // 40b + byte b050, b051, b052, b053, b054, b055, b056, b057; // 48b + byte b060, b061, b062, b063, b064, b065, b066, b067; // 56b + byte b070, b071, b072, b073, b074, b075, b076, b077; // 64b + byte b100, b101, b102, b103, b104, b105, b106, b107; // 72b + byte b110, b111, b112, b113, b114, b115, b116, b117; // 80b + byte b120, b121, b122, b123, b124, b125, b126, b127; // 88b + byte b130, b131, b132, b133, b134, b135, b136, b137; // 96b + byte b140, b141, b142, b143, b144, b145, b146, b147; // 104b + byte b150, b151, b152, b153, b154, b155, b156, b157; // 112b + byte b160, b161, b162, b163, b164, b165, b166, b167; // 120b + // byte b170,b171,b172,b173,b174,b175,b176,b177;//128b + // * drop 8b as object header acts as padding and is >= 8b * +} + +// $gen:ordered-fields +abstract class BaseLinkedQueueProducerNodeRef extends BaseLinkedQueuePad0 { + static final long P_NODE_OFFSET = + fieldOffset(BaseLinkedQueueProducerNodeRef.class, "producerNode"); + + private volatile LinkedQueueNode producerNode; + + final void spProducerNode(LinkedQueueNode newValue) { + UNSAFE.putObject(this, P_NODE_OFFSET, newValue); + } + + final void soProducerNode(LinkedQueueNode newValue) { + UNSAFE.putOrderedObject(this, P_NODE_OFFSET, newValue); + } + + final LinkedQueueNode lvProducerNode() { + return producerNode; + } + + final boolean casProducerNode(LinkedQueueNode expect, LinkedQueueNode newValue) { + return UNSAFE.compareAndSwapObject(this, P_NODE_OFFSET, expect, newValue); + } + + final LinkedQueueNode lpProducerNode() { + return producerNode; + } +} + +abstract class BaseLinkedQueuePad1 extends BaseLinkedQueueProducerNodeRef { + byte b000, b001, b002, b003, b004, b005, b006, b007; // 8b + byte b010, b011, b012, b013, b014, b015, b016, b017; // 16b + byte b020, b021, b022, b023, b024, b025, b026, b027; // 24b + byte b030, b031, b032, b033, b034, b035, b036, b037; // 32b + byte b040, b041, b042, b043, b044, b045, b046, b047; // 40b + byte b050, b051, b052, b053, b054, b055, b056, b057; // 48b + byte b060, b061, b062, b063, b064, b065, b066, b067; // 56b + byte b070, b071, b072, b073, b074, b075, b076, b077; // 64b + byte b100, b101, b102, b103, b104, b105, b106, b107; // 72b + byte b110, b111, b112, b113, b114, b115, b116, b117; // 80b + byte b120, b121, b122, b123, b124, b125, b126, b127; // 88b + byte b130, b131, b132, b133, b134, b135, b136, b137; // 96b + byte b140, b141, b142, b143, b144, b145, b146, b147; // 104b + byte b150, b151, b152, b153, b154, b155, b156, b157; // 112b + byte b160, b161, b162, b163, b164, b165, b166, b167; // 120b + byte b170, b171, b172, b173, b174, b175, b176, b177; // 128b +} + +// $gen:ordered-fields +abstract class BaseLinkedQueueConsumerNodeRef extends BaseLinkedQueuePad1 { + private static final long C_NODE_OFFSET = + fieldOffset(BaseLinkedQueueConsumerNodeRef.class, "consumerNode"); + + private LinkedQueueNode consumerNode; + + final void spConsumerNode(LinkedQueueNode newValue) { + consumerNode = newValue; + } + + @SuppressWarnings("unchecked") + final LinkedQueueNode lvConsumerNode() { + return (LinkedQueueNode) UNSAFE.getObjectVolatile(this, C_NODE_OFFSET); + } + + final LinkedQueueNode lpConsumerNode() { + return consumerNode; + } +} + +abstract class BaseLinkedQueuePad2 extends BaseLinkedQueueConsumerNodeRef { + byte b000, b001, b002, b003, b004, b005, b006, b007; // 8b + byte b010, b011, b012, b013, b014, b015, b016, b017; // 16b + byte b020, b021, b022, b023, b024, b025, b026, b027; // 24b + byte b030, b031, b032, b033, b034, b035, b036, b037; // 32b + byte b040, b041, b042, b043, b044, b045, b046, b047; // 40b + byte b050, b051, b052, b053, b054, b055, b056, b057; // 48b + byte b060, b061, b062, b063, b064, b065, b066, b067; // 56b + byte b070, b071, b072, b073, b074, b075, b076, b077; // 64b + byte b100, b101, b102, b103, b104, b105, b106, b107; // 72b + byte b110, b111, b112, b113, b114, b115, b116, b117; // 80b + byte b120, b121, b122, b123, b124, b125, b126, b127; // 88b + byte b130, b131, b132, b133, b134, b135, b136, b137; // 96b + byte b140, b141, b142, b143, b144, b145, b146, b147; // 104b + byte b150, b151, b152, b153, b154, b155, b156, b157; // 112b + byte b160, b161, b162, b163, b164, b165, b166, b167; // 120b + byte b170, b171, b172, b173, b174, b175, b176, b177; // 128b +} + +/** + * A base data structure for concurrent linked queues. For convenience also pulled in common single + * consumer methods since at this time there's no plan to implement MC. + */ +abstract class BaseLinkedQueue extends BaseLinkedQueuePad2 { + + @Override + public final Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + return this.getClass().getName(); + } + + protected final LinkedQueueNode newNode() { + return new LinkedQueueNode(); + } + + protected final LinkedQueueNode newNode(E e) { + return new LinkedQueueNode(e); + } + + /** + * {@inheritDoc}
+ * + *

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

IMPLEMENTATION NOTES:
+ * Queue is empty when producerNode is the same as consumerNode. An alternative implementation + * would be to observe the producerNode.value is null, which also means an empty queue because + * only the consumerNode.value is allowed to be null. + * + * @see MessagePassingQueue#isEmpty() + */ + @Override + public boolean isEmpty() { + LinkedQueueNode consumerNode = lvConsumerNode(); + LinkedQueueNode producerNode = lvProducerNode(); + return consumerNode == producerNode; + } + + protected E getSingleConsumerNodeValue( + LinkedQueueNode currConsumerNode, LinkedQueueNode nextNode) { + // we have to null out the value because we are going to hang on to the node + final E nextValue = nextNode.getAndNullValue(); + + // Fix up the next ref of currConsumerNode to prevent promoted nodes from keeping new ones + // alive. + // We use a reference to self instead of null because null is already a meaningful value (the + // next of + // producer node is null). + currConsumerNode.soNext(currConsumerNode); + spConsumerNode(nextNode); + // currConsumerNode is now no longer referenced and can be collected + return nextValue; + } + + @Override + public E relaxedPoll() { + final LinkedQueueNode currConsumerNode = lpConsumerNode(); + final LinkedQueueNode nextNode = currConsumerNode.lvNext(); + if (nextNode != null) { + return getSingleConsumerNodeValue(currConsumerNode, nextNode); + } + return null; + } + + @Override + public E relaxedPeek() { + final LinkedQueueNode nextNode = lpConsumerNode().lvNext(); + if (nextNode != null) { + return nextNode.lpValue(); + } + return null; + } + + @Override + public boolean relaxedOffer(E e) { + return offer(e); + } + + @Override + public int drain(Consumer c) { + long result = 0; // use long to force safepoint into loop below + int drained; + do { + drained = drain(c, 4096); + result += drained; + } while (drained == 4096 && result <= Integer.MAX_VALUE - 4096); + return (int) result; + } + + @Override + public int drain(Consumer c, int limit) { + LinkedQueueNode chaserNode = this.lpConsumerNode(); + for (int i = 0; i < limit; i++) { + final LinkedQueueNode nextNode = chaserNode.lvNext(); + + if (nextNode == null) { + return i; + } + // we have to null out the value because we are going to hang on to the node + final E nextValue = getSingleConsumerNodeValue(chaserNode, nextNode); + chaserNode = nextNode; + c.accept(nextValue); + } + return limit; + } + + @Override + public void drain(Consumer c, WaitStrategy wait, ExitCondition exit) { + LinkedQueueNode chaserNode = this.lpConsumerNode(); + int idleCounter = 0; + while (exit.keepRunning()) { + for (int i = 0; i < 4096; i++) { + final LinkedQueueNode nextNode = chaserNode.lvNext(); + if (nextNode == null) { + idleCounter = wait.idle(idleCounter); + continue; + } + + idleCounter = 0; + // we have to null out the value because we are going to hang on to the node + final E nextValue = getSingleConsumerNodeValue(chaserNode, nextNode); + chaserNode = nextNode; + c.accept(nextValue); + } + } + } + + @Override + public int capacity() { + return UNBOUNDED_CAPACITY; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseMpscLinkedArrayQueue.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseMpscLinkedArrayQueue.java new file mode 100644 index 000000000..cfad5ef71 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseMpscLinkedArrayQueue.java @@ -0,0 +1,705 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.internal.jctools.queues; + +import static io.rsocket.internal.jctools.queues.LinkedArrayQueueUtil.length; +import static io.rsocket.internal.jctools.queues.LinkedArrayQueueUtil.modifiedCalcCircularRefElementOffset; +import static io.rsocket.internal.jctools.queues.UnsafeAccess.UNSAFE; +import static io.rsocket.internal.jctools.queues.UnsafeAccess.fieldOffset; +import static io.rsocket.internal.jctools.queues.UnsafeRefArrayAccess.allocateRefArray; +import static io.rsocket.internal.jctools.queues.UnsafeRefArrayAccess.calcCircularRefElementOffset; +import static io.rsocket.internal.jctools.queues.UnsafeRefArrayAccess.calcRefElementOffset; +import static io.rsocket.internal.jctools.queues.UnsafeRefArrayAccess.lvRefElement; +import static io.rsocket.internal.jctools.queues.UnsafeRefArrayAccess.soRefElement; + +import io.rsocket.internal.jctools.queues.IndexedQueueSizeUtil.IndexedQueue; +import java.util.AbstractQueue; +import java.util.Iterator; +import java.util.NoSuchElementException; + +abstract class BaseMpscLinkedArrayQueuePad1 extends AbstractQueue implements IndexedQueue { + byte b000, b001, b002, b003, b004, b005, b006, b007; // 8b + byte b010, b011, b012, b013, b014, b015, b016, b017; // 16b + byte b020, b021, b022, b023, b024, b025, b026, b027; // 24b + byte b030, b031, b032, b033, b034, b035, b036, b037; // 32b + byte b040, b041, b042, b043, b044, b045, b046, b047; // 40b + byte b050, b051, b052, b053, b054, b055, b056, b057; // 48b + byte b060, b061, b062, b063, b064, b065, b066, b067; // 56b + byte b070, b071, b072, b073, b074, b075, b076, b077; // 64b + byte b100, b101, b102, b103, b104, b105, b106, b107; // 72b + byte b110, b111, b112, b113, b114, b115, b116, b117; // 80b + byte b120, b121, b122, b123, b124, b125, b126, b127; // 88b + byte b130, b131, b132, b133, b134, b135, b136, b137; // 96b + byte b140, b141, b142, b143, b144, b145, b146, b147; // 104b + byte b150, b151, b152, b153, b154, b155, b156, b157; // 112b + byte b160, b161, b162, b163, b164, b165, b166, b167; // 120b + byte b170, b171, b172, b173, b174, b175, b176, b177; // 128b +} + +// $gen:ordered-fields +abstract class BaseMpscLinkedArrayQueueProducerFields extends BaseMpscLinkedArrayQueuePad1 { + private static final long P_INDEX_OFFSET = + fieldOffset(BaseMpscLinkedArrayQueueProducerFields.class, "producerIndex"); + + private volatile long producerIndex; + + @Override + public final long lvProducerIndex() { + return producerIndex; + } + + final void soProducerIndex(long newValue) { + UNSAFE.putOrderedLong(this, P_INDEX_OFFSET, newValue); + } + + final boolean casProducerIndex(long expect, long newValue) { + return UNSAFE.compareAndSwapLong(this, P_INDEX_OFFSET, expect, newValue); + } +} + +abstract class BaseMpscLinkedArrayQueuePad2 extends BaseMpscLinkedArrayQueueProducerFields { + byte b000, b001, b002, b003, b004, b005, b006, b007; // 8b + byte b010, b011, b012, b013, b014, b015, b016, b017; // 16b + byte b020, b021, b022, b023, b024, b025, b026, b027; // 24b + byte b030, b031, b032, b033, b034, b035, b036, b037; // 32b + byte b040, b041, b042, b043, b044, b045, b046, b047; // 40b + byte b050, b051, b052, b053, b054, b055, b056, b057; // 48b + byte b060, b061, b062, b063, b064, b065, b066, b067; // 56b + byte b070, b071, b072, b073, b074, b075, b076, b077; // 64b + byte b100, b101, b102, b103, b104, b105, b106, b107; // 72b + byte b110, b111, b112, b113, b114, b115, b116, b117; // 80b + byte b120, b121, b122, b123, b124, b125, b126, b127; // 88b + byte b130, b131, b132, b133, b134, b135, b136, b137; // 96b + byte b140, b141, b142, b143, b144, b145, b146, b147; // 104b + byte b150, b151, b152, b153, b154, b155, b156, b157; // 112b + byte b160, b161, b162, b163, b164, b165, b166, b167; // 120b + byte b170, b171, b172, b173, b174, b175, b176, b177; // 128b +} + +// $gen:ordered-fields +abstract class BaseMpscLinkedArrayQueueConsumerFields extends BaseMpscLinkedArrayQueuePad2 { + private static final long C_INDEX_OFFSET = + fieldOffset(BaseMpscLinkedArrayQueueConsumerFields.class, "consumerIndex"); + + private volatile long consumerIndex; + protected long consumerMask; + protected E[] consumerBuffer; + + @Override + public final long lvConsumerIndex() { + return consumerIndex; + } + + final long lpConsumerIndex() { + return UNSAFE.getLong(this, C_INDEX_OFFSET); + } + + final void soConsumerIndex(long newValue) { + UNSAFE.putOrderedLong(this, C_INDEX_OFFSET, newValue); + } +} + +abstract class BaseMpscLinkedArrayQueuePad3 extends BaseMpscLinkedArrayQueueConsumerFields { + byte b000, b001, b002, b003, b004, b005, b006, b007; // 8b + byte b010, b011, b012, b013, b014, b015, b016, b017; // 16b + byte b020, b021, b022, b023, b024, b025, b026, b027; // 24b + byte b030, b031, b032, b033, b034, b035, b036, b037; // 32b + byte b040, b041, b042, b043, b044, b045, b046, b047; // 40b + byte b050, b051, b052, b053, b054, b055, b056, b057; // 48b + byte b060, b061, b062, b063, b064, b065, b066, b067; // 56b + byte b070, b071, b072, b073, b074, b075, b076, b077; // 64b + byte b100, b101, b102, b103, b104, b105, b106, b107; // 72b + byte b110, b111, b112, b113, b114, b115, b116, b117; // 80b + byte b120, b121, b122, b123, b124, b125, b126, b127; // 88b + byte b130, b131, b132, b133, b134, b135, b136, b137; // 96b + byte b140, b141, b142, b143, b144, b145, b146, b147; // 104b + byte b150, b151, b152, b153, b154, b155, b156, b157; // 112b + byte b160, b161, b162, b163, b164, b165, b166, b167; // 120b + byte b170, b171, b172, b173, b174, b175, b176, b177; // 128b +} + +// $gen:ordered-fields +abstract class BaseMpscLinkedArrayQueueColdProducerFields + extends BaseMpscLinkedArrayQueuePad3 { + private static final long P_LIMIT_OFFSET = + fieldOffset(BaseMpscLinkedArrayQueueColdProducerFields.class, "producerLimit"); + + private volatile long producerLimit; + protected long producerMask; + protected E[] producerBuffer; + + final long lvProducerLimit() { + return producerLimit; + } + + final boolean casProducerLimit(long expect, long newValue) { + return UNSAFE.compareAndSwapLong(this, P_LIMIT_OFFSET, expect, newValue); + } + + final void soProducerLimit(long newValue) { + UNSAFE.putOrderedLong(this, P_LIMIT_OFFSET, newValue); + } +} + +/** + * An MPSC array queue which starts at initialCapacity and grows to maxCapacity in + * linked chunks of the initial size. The queue grows only when the current buffer is full and + * elements are not copied on resize, instead a link to the new buffer is stored in the old buffer + * for the consumer to follow. + */ +abstract class BaseMpscLinkedArrayQueue extends BaseMpscLinkedArrayQueueColdProducerFields + implements MessagePassingQueue, QueueProgressIndicators { + // No post padding here, subclasses must add + private static final Object JUMP = new Object(); + private static final Object BUFFER_CONSUMED = new Object(); + private static final int CONTINUE_TO_P_INDEX_CAS = 0; + private static final int RETRY = 1; + private static final int QUEUE_FULL = 2; + private static final int QUEUE_RESIZE = 3; + + /** + * @param initialCapacity the queue initial capacity. If chunk size is fixed this will be the + * chunk size. Must be 2 or more. + */ + public BaseMpscLinkedArrayQueue(final int initialCapacity) { + RangeUtil.checkGreaterThanOrEqual(initialCapacity, 2, "initialCapacity"); + + int p2capacity = Pow2.roundToPowerOfTwo(initialCapacity); + // leave lower bit of mask clear + long mask = (p2capacity - 1) << 1; + // need extra element to point at next array + E[] buffer = allocateRefArray(p2capacity + 1); + producerBuffer = buffer; + producerMask = mask; + consumerBuffer = buffer; + consumerMask = mask; + soProducerLimit(mask); // we know it's all empty to start with + } + + @Override + public int size() { + // NOTE: because indices are on even numbers we cannot use the size util. + + /* + * It is possible for a thread to be interrupted or reschedule between the read of the producer and + * consumer indices, therefore protection is required to ensure size is within valid range. In the + * event of concurrent polls/offers to this method the size is OVER estimated as we read consumer + * index BEFORE the producer index. + */ + long after = lvConsumerIndex(); + long size; + while (true) { + final long before = after; + final long currentProducerIndex = lvProducerIndex(); + after = lvConsumerIndex(); + if (before == after) { + size = ((currentProducerIndex - after) >> 1); + break; + } + } + // Long overflow is impossible, so size is always positive. Integer overflow is possible for the + // unbounded + // indexed queues. + if (size > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else { + return (int) size; + } + } + + @Override + public boolean isEmpty() { + // Order matters! + // Loading consumer before producer allows for producer increments after consumer index is read. + // This ensures this method is conservative in it's estimate. Note that as this is an MPMC there + // is + // nothing we can do to make this an exact method. + return (this.lvConsumerIndex() == this.lvProducerIndex()); + } + + @Override + public String toString() { + return this.getClass().getName(); + } + + @Override + public boolean offer(final E e) { + if (null == e) { + throw new NullPointerException(); + } + + long mask; + E[] buffer; + long pIndex; + + while (true) { + long producerLimit = lvProducerLimit(); + pIndex = lvProducerIndex(); + // lower bit is indicative of resize, if we see it we spin until it's cleared + if ((pIndex & 1) == 1) { + continue; + } + // pIndex is even (lower bit is 0) -> actual index is (pIndex >> 1) + + // mask/buffer may get changed by resizing -> only use for array access after successful CAS. + mask = this.producerMask; + buffer = this.producerBuffer; + // a successful CAS ties the ordering, lv(pIndex) - [mask/buffer] -> cas(pIndex) + + // assumption behind this optimization is that queue is almost always empty or near empty + if (producerLimit <= pIndex) { + int result = offerSlowPath(mask, pIndex, producerLimit); + switch (result) { + case CONTINUE_TO_P_INDEX_CAS: + break; + case RETRY: + continue; + case QUEUE_FULL: + return false; + case QUEUE_RESIZE: + resize(mask, buffer, pIndex, e, null); + return true; + } + } + + if (casProducerIndex(pIndex, pIndex + 2)) { + break; + } + } + // INDEX visible before ELEMENT + final long offset = modifiedCalcCircularRefElementOffset(pIndex, mask); + soRefElement(buffer, offset, e); // release element e + return true; + } + + /** + * {@inheritDoc} + * + *

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

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

The iterator provides a best-effort snapshot of the elements in the queue. The returned + * iterator is not guaranteed to return elements in queue order, and races with the consumer + * thread may cause gaps in the sequence of returned elements. Like {link #relaxedPoll}, the + * iterator may not immediately return newly inserted elements. + * + * @return The iterator. + */ + @Override + public Iterator iterator() { + return new WeakIterator(consumerBuffer, lvConsumerIndex(), lvProducerIndex()); + } + + private static class WeakIterator implements Iterator { + private final long pIndex; + private long nextIndex; + private E nextElement; + private E[] currentBuffer; + private int mask; + + WeakIterator(E[] currentBuffer, long cIndex, long pIndex) { + this.pIndex = pIndex >> 1; + this.nextIndex = cIndex >> 1; + setBuffer(currentBuffer); + nextElement = getNext(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + @Override + public boolean hasNext() { + return nextElement != null; + } + + @Override + public E next() { + final E e = nextElement; + if (e == null) { + throw new NoSuchElementException(); + } + nextElement = getNext(); + return e; + } + + private void setBuffer(E[] buffer) { + this.currentBuffer = buffer; + this.mask = length(buffer) - 2; + } + + private E getNext() { + while (nextIndex < pIndex) { + long index = nextIndex++; + E e = lvRefElement(currentBuffer, calcCircularRefElementOffset(index, mask)); + // skip removed/not yet visible elements + if (e == null) { + continue; + } + + // not null && not JUMP -> found next element + if (e != JUMP) { + return e; + } + + // need to jump to the next buffer + int nextBufferIndex = mask + 1; + Object nextBuffer = lvRefElement(currentBuffer, calcRefElementOffset(nextBufferIndex)); + + if (nextBuffer == BUFFER_CONSUMED || nextBuffer == null) { + // Consumer may have passed us, or the next buffer is not visible yet: drop out early + return null; + } + + setBuffer((E[]) nextBuffer); + // now with the new array retry the load, it can't be a JUMP, but we need to repeat same + // index + e = lvRefElement(currentBuffer, calcCircularRefElementOffset(index, mask)); + // skip removed/not yet visible elements + if (e == null) { + continue; + } else { + return e; + } + } + return null; + } + } + + private void resize(long oldMask, E[] oldBuffer, long pIndex, E e, Supplier s) { + assert (e != null && s == null) || (e == null || s != null); + int newBufferLength = getNextBufferSize(oldBuffer); + final E[] newBuffer; + try { + newBuffer = allocateRefArray(newBufferLength); + } catch (OutOfMemoryError oom) { + assert lvProducerIndex() == pIndex + 1; + soProducerIndex(pIndex); + throw oom; + } + + producerBuffer = newBuffer; + final int newMask = (newBufferLength - 2) << 1; + producerMask = newMask; + + final long offsetInOld = modifiedCalcCircularRefElementOffset(pIndex, oldMask); + final long offsetInNew = modifiedCalcCircularRefElementOffset(pIndex, newMask); + + soRefElement(newBuffer, offsetInNew, e == null ? s.get() : e); // element in new array + soRefElement(oldBuffer, nextArrayOffset(oldMask), newBuffer); // buffer linked + + // ASSERT code + final long cIndex = lvConsumerIndex(); + final long availableInQueue = availableInQueue(pIndex, cIndex); + RangeUtil.checkPositive(availableInQueue, "availableInQueue"); + + // Invalidate racing CASs + // We never set the limit beyond the bounds of a buffer + soProducerLimit(pIndex + Math.min(newMask, availableInQueue)); + + // make resize visible to the other producers + soProducerIndex(pIndex + 2); + + // INDEX visible before ELEMENT, consistent with consumer expectation + + // make resize visible to consumer + soRefElement(oldBuffer, offsetInOld, JUMP); + } + + /** @return next buffer size(inclusive of next array pointer) */ + protected abstract int getNextBufferSize(E[] buffer); + + /** @return current buffer capacity for elements (excluding next pointer and jump entry) * 2 */ + protected abstract long getCurrentBufferCapacity(long mask); +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/IndexedQueueSizeUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/IndexedQueueSizeUtil.java new file mode 100644 index 000000000..40116bbe1 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/IndexedQueueSizeUtil.java @@ -0,0 +1,59 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.internal.jctools.queues; + +final class IndexedQueueSizeUtil { + public static int size(IndexedQueue iq) { + /* + * It is possible for a thread to be interrupted or reschedule between the read of the producer and + * consumer indices, therefore protection is required to ensure size is within valid range. In the + * event of concurrent polls/offers to this method the size is OVER estimated as we read consumer + * index BEFORE the producer index. + */ + long after = iq.lvConsumerIndex(); + long size; + while (true) { + final long before = after; + final long currentProducerIndex = iq.lvProducerIndex(); + after = iq.lvConsumerIndex(); + if (before == after) { + size = (currentProducerIndex - after); + break; + } + } + // Long overflow is impossible (), so size is always positive. Integer overflow is possible for + // the unbounded + // indexed queues. + if (size > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else { + return (int) size; + } + } + + public static boolean isEmpty(IndexedQueue iq) { + // Order matters! + // Loading consumer before producer allows for producer increments after consumer index is read. + // This ensures this method is conservative in it's estimate. Note that as this is an MPMC there + // is + // nothing we can do to make this an exact method. + return (iq.lvConsumerIndex() == iq.lvProducerIndex()); + } + + public interface IndexedQueue { + long lvConsumerIndex(); + + long lvProducerIndex(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedArrayQueueUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedArrayQueueUtil.java new file mode 100644 index 000000000..37651f351 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedArrayQueueUtil.java @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.internal.jctools.queues; + +import static io.rsocket.internal.jctools.queues.UnsafeRefArrayAccess.REF_ARRAY_BASE; +import static io.rsocket.internal.jctools.queues.UnsafeRefArrayAccess.REF_ELEMENT_SHIFT; + +/** This is used for method substitution in the LinkedArray classes code generation. */ +final class LinkedArrayQueueUtil { + static int length(Object[] buf) { + return buf.length; + } + + /** + * This method assumes index is actually (index << 1) because lower bit is used for resize. This + * is compensated for by reducing the element shift. The computation is constant folded, so + * there's no cost. + */ + static long modifiedCalcCircularRefElementOffset(long index, long mask) { + return REF_ARRAY_BASE + ((index & mask) << (REF_ELEMENT_SHIFT - 1)); + } + + static long nextArrayOffset(Object[] curr) { + return REF_ARRAY_BASE + ((long) (length(curr) - 1) << REF_ELEMENT_SHIFT); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedQueueNode.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedQueueNode.java new file mode 100644 index 000000000..72e78bb92 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedQueueNode.java @@ -0,0 +1,63 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.internal.jctools.queues; + +import static io.rsocket.internal.jctools.queues.UnsafeAccess.UNSAFE; +import static io.rsocket.internal.jctools.queues.UnsafeAccess.fieldOffset; + +final class LinkedQueueNode { + private static final long NEXT_OFFSET = fieldOffset(LinkedQueueNode.class, "next"); + + private E value; + private volatile LinkedQueueNode next; + + LinkedQueueNode() { + this(null); + } + + LinkedQueueNode(E val) { + spValue(val); + } + + /** + * Gets the current value and nulls out the reference to it from this node. + * + * @return value + */ + public E getAndNullValue() { + E temp = lpValue(); + spValue(null); + return temp; + } + + public E lpValue() { + return value; + } + + public void spValue(E newValue) { + value = newValue; + } + + public void soNext(LinkedQueueNode n) { + UNSAFE.putOrderedObject(this, NEXT_OFFSET, n); + } + + public void spNext(LinkedQueueNode n) { + UNSAFE.putObject(this, NEXT_OFFSET, n); + } + + public LinkedQueueNode lvNext() { + return next; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueue.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueue.java new file mode 100644 index 000000000..7a0fa901f --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueue.java @@ -0,0 +1,339 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.internal.jctools.queues; + +import java.util.Queue; + +/** + * Message passing queues are intended for concurrent method passing. A subset of {@link Queue} + * methods are provided with the same semantics, while further functionality which accomodates the + * concurrent usecase is also on offer. + * + *

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

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

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

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

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

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

+ * + *

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

+ * + *

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

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

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

+ * + *

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

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

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

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

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

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

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

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

+ * + *

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

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

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

+ * + *

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

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

WARNING: Explicit assumptions are made with regards to {@link Supplier#get} make sure + * you have read and understood these before using this method. + * + * @throws IllegalArgumentException s OR wait OR exit are {@code null} + */ + void fill(Supplier s, WaitStrategy wait, ExitCondition exit); +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueueUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueueUtil.java new file mode 100644 index 000000000..cb03364d8 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueueUtil.java @@ -0,0 +1,100 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.internal.jctools.queues; + +import io.rsocket.internal.jctools.queues.MessagePassingQueue.Consumer; +import io.rsocket.internal.jctools.queues.MessagePassingQueue.ExitCondition; +import io.rsocket.internal.jctools.queues.MessagePassingQueue.Supplier; +import io.rsocket.internal.jctools.queues.MessagePassingQueue.WaitStrategy; + +final class MessagePassingQueueUtil { + public static int drain(MessagePassingQueue queue, Consumer c, int limit) { + if (null == c) throw new IllegalArgumentException("c is null"); + if (limit < 0) throw new IllegalArgumentException("limit is negative: " + limit); + if (limit == 0) return 0; + E e; + int i = 0; + for (; i < limit && (e = queue.relaxedPoll()) != null; i++) { + c.accept(e); + } + return i; + } + + public static int drain(MessagePassingQueue queue, Consumer c) { + if (null == c) throw new IllegalArgumentException("c is null"); + E e; + int i = 0; + while ((e = queue.relaxedPoll()) != null) { + i++; + c.accept(e); + } + return i; + } + + public static void drain( + MessagePassingQueue queue, Consumer c, WaitStrategy wait, ExitCondition exit) { + if (null == c) throw new IllegalArgumentException("c is null"); + if (null == wait) throw new IllegalArgumentException("wait is null"); + if (null == exit) throw new IllegalArgumentException("exit condition is null"); + + int idleCounter = 0; + while (exit.keepRunning()) { + final E e = queue.relaxedPoll(); + if (e == null) { + idleCounter = wait.idle(idleCounter); + continue; + } + idleCounter = 0; + c.accept(e); + } + } + + public static void fill( + MessagePassingQueue q, Supplier s, WaitStrategy wait, ExitCondition exit) { + if (null == wait) throw new IllegalArgumentException("waiter is null"); + if (null == exit) throw new IllegalArgumentException("exit condition is null"); + + int idleCounter = 0; + while (exit.keepRunning()) { + if (q.fill(s, PortableJvmInfo.RECOMENDED_OFFER_BATCH) == 0) { + idleCounter = wait.idle(idleCounter); + continue; + } + idleCounter = 0; + } + } + + public static int fillBounded(MessagePassingQueue q, Supplier s) { + return fillInBatchesToLimit(q, s, PortableJvmInfo.RECOMENDED_OFFER_BATCH, q.capacity()); + } + + public static int fillInBatchesToLimit( + MessagePassingQueue q, Supplier s, int batch, int limit) { + long result = + 0; // result is a long because we want to have a safepoint check at regular intervals + do { + final int filled = q.fill(s, batch); + if (filled == 0) { + return (int) result; + } + result += filled; + } while (result <= limit); + return (int) result; + } + + public static int fillUnbounded(MessagePassingQueue q, Supplier s) { + return fillInBatchesToLimit(q, s, PortableJvmInfo.RECOMENDED_OFFER_BATCH, 4096); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MpscUnboundedArrayQueue.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MpscUnboundedArrayQueue.java new file mode 100644 index 000000000..179070be4 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MpscUnboundedArrayQueue.java @@ -0,0 +1,76 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.internal.jctools.queues; + +import static io.rsocket.internal.jctools.queues.LinkedArrayQueueUtil.length; +import static io.rsocket.internal.jctools.queues.MessagePassingQueueUtil.fillUnbounded; + +/** + * An MPSC array queue which starts at initialCapacity and grows indefinitely in linked + * chunks of the initial size. The queue grows only when the current chunk is full and elements are + * not copied on resize, instead a link to the new chunk is stored in the old chunk for the consumer + * to follow. + */ +public class MpscUnboundedArrayQueue extends BaseMpscLinkedArrayQueue { + byte b000, b001, b002, b003, b004, b005, b006, b007; // 8b + byte b010, b011, b012, b013, b014, b015, b016, b017; // 16b + byte b020, b021, b022, b023, b024, b025, b026, b027; // 24b + byte b030, b031, b032, b033, b034, b035, b036, b037; // 32b + byte b040, b041, b042, b043, b044, b045, b046, b047; // 40b + byte b050, b051, b052, b053, b054, b055, b056, b057; // 48b + byte b060, b061, b062, b063, b064, b065, b066, b067; // 56b + byte b070, b071, b072, b073, b074, b075, b076, b077; // 64b + byte b100, b101, b102, b103, b104, b105, b106, b107; // 72b + byte b110, b111, b112, b113, b114, b115, b116, b117; // 80b + byte b120, b121, b122, b123, b124, b125, b126, b127; // 88b + byte b130, b131, b132, b133, b134, b135, b136, b137; // 96b + byte b140, b141, b142, b143, b144, b145, b146, b147; // 104b + byte b150, b151, b152, b153, b154, b155, b156, b157; // 112b + byte b160, b161, b162, b163, b164, b165, b166, b167; // 120b + byte b170, b171, b172, b173, b174, b175, b176, b177; // 128b + + public MpscUnboundedArrayQueue(int chunkSize) { + super(chunkSize); + } + + @Override + protected long availableInQueue(long pIndex, long cIndex) { + return Integer.MAX_VALUE; + } + + @Override + public int capacity() { + return MessagePassingQueue.UNBOUNDED_CAPACITY; + } + + @Override + public int drain(Consumer c) { + return drain(c, 4096); + } + + @Override + public int fill(Supplier s) { + return fillUnbounded(this, s); + } + + @Override + protected int getNextBufferSize(E[] buffer) { + return length(buffer); + } + + @Override + protected long getCurrentBufferCapacity(long mask) { + return mask; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/PortableJvmInfo.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/PortableJvmInfo.java new file mode 100644 index 000000000..f037857e8 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/PortableJvmInfo.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.internal.jctools.queues; + +/** JVM Information that is standard and available on all JVMs (i.e. does not use unsafe) */ +interface PortableJvmInfo { + int CACHE_LINE_SIZE = Integer.getInteger("jctools.cacheLineSize", 64); + int CPUs = Runtime.getRuntime().availableProcessors(); + int RECOMENDED_OFFER_BATCH = CPUs * 4; + int RECOMENDED_POLL_BATCH = CPUs * 4; +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/Pow2.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/Pow2.java new file mode 100644 index 000000000..282a22f02 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/Pow2.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.internal.jctools.queues; + +/** Power of 2 utility functions. */ +final class Pow2 { + public static final int MAX_POW2 = 1 << 30; + + /** + * @param value from which next positive power of two will be found. + * @return the next positive power of 2, this value if it is a power of 2. Negative values are + * mapped to 1. + * @throws IllegalArgumentException is value is more than MAX_POW2 or less than 0 + */ + public static int roundToPowerOfTwo(final int value) { + if (value > MAX_POW2) { + throw new IllegalArgumentException( + "There is no larger power of 2 int for value:" + value + " since it exceeds 2^31."); + } + if (value < 0) { + throw new IllegalArgumentException("Given value:" + value + ". Expecting value >= 0."); + } + final int nextPow2 = 1 << (32 - Integer.numberOfLeadingZeros(value - 1)); + return nextPow2; + } + + /** + * @param value to be tested to see if it is a power of two. + * @return true if the value is a power of 2 otherwise false. + */ + public static boolean isPowerOfTwo(final int value) { + return (value & (value - 1)) == 0; + } + + /** + * Align a value to the next multiple up of alignment. If the value equals an alignment multiple + * then it is returned unchanged. + * + * @param value to be aligned up. + * @param alignment to be used, must be a power of 2. + * @return the value aligned to the next boundary. + */ + public static long align(final long value, final int alignment) { + if (!isPowerOfTwo(alignment)) { + throw new IllegalArgumentException("alignment must be a power of 2:" + alignment); + } + return (value + (alignment - 1)) & ~(alignment - 1); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/QueueProgressIndicators.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/QueueProgressIndicators.java new file mode 100644 index 000000000..6418cc947 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/QueueProgressIndicators.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.internal.jctools.queues; + +/** + * This interface is provided for monitoring purposes only and is only available on queues where it + * is easy to provide it. The producer/consumer progress indicators usually correspond with the + * number of elements offered/polled, but they are not guaranteed to maintain that semantic. + * + * @author nitsanw + */ +public interface QueueProgressIndicators { + + /** + * This method has no concurrent visibility semantics. The value returned may be negative. Under + * normal circumstances 2 consecutive calls to this method can offer an idea of progress made by + * producer threads by subtracting the 2 results though in extreme cases (if producers have + * progressed by more than 2^64) this may also fail.
+ * This value will normally indicate number of elements passed into the queue, but may under some + * circumstances be a derivative of that figure. This method should not be used to derive size or + * emptiness. + * + * @return the current value of the producer progress index + */ + long currentProducerIndex(); + + /** + * This method has no concurrent visibility semantics. The value returned may be negative. Under + * normal circumstances 2 consecutive calls to this method can offer an idea of progress made by + * consumer threads by subtracting the 2 results though in extreme cases (if consumers have + * progressed by more than 2^64) this may also fail.
+ * This value will normally indicate number of elements taken out of the queue, but may under some + * circumstances be a derivative of that figure. This method should not be used to derive size or + * emptiness. + * + * @return the current value of the consumer progress index + */ + long currentConsumerIndex(); +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/RangeUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/RangeUtil.java new file mode 100644 index 000000000..3adcb2f3c --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/RangeUtil.java @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.internal.jctools.queues; + +final class RangeUtil { + public static long checkPositive(long n, String name) { + if (n <= 0) { + throw new IllegalArgumentException(name + ": " + n + " (expected: > 0)"); + } + + return n; + } + + public static int checkPositiveOrZero(int n, String name) { + if (n < 0) { + throw new IllegalArgumentException(name + ": " + n + " (expected: >= 0)"); + } + + return n; + } + + public static int checkLessThan(int n, int expected, String name) { + if (n >= expected) { + throw new IllegalArgumentException(name + ": " + n + " (expected: < " + expected + ')'); + } + + return n; + } + + public static int checkLessThanOrEqual(int n, long expected, String name) { + if (n > expected) { + throw new IllegalArgumentException(name + ": " + n + " (expected: <= " + expected + ')'); + } + + return n; + } + + public static int checkGreaterThanOrEqual(int n, int expected, String name) { + if (n < expected) { + throw new IllegalArgumentException(name + ": " + n + " (expected: >= " + expected + ')'); + } + + return n; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/UnsafeAccess.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/UnsafeAccess.java new file mode 100644 index 000000000..c99aeb689 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/UnsafeAccess.java @@ -0,0 +1,95 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.internal.jctools.queues; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicReferenceArray; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import sun.misc.Unsafe; + +/** + * Why should we resort to using Unsafe?
+ * + *

    + *
  1. To construct class fields which allow volatile/ordered/plain access: This requirement is + * covered by {@link AtomicReferenceFieldUpdater} and similar but their performance is + * arguably worse than the DIY approach (depending on JVM version) while Unsafe + * intrinsification is a far lesser challenge for JIT compilers. + *
  2. To construct flavors of {@link AtomicReferenceArray}. + *
  3. Other use cases exist but are not present in this library yet. + *
+ * + * @author nitsanw + */ +class UnsafeAccess { + public static final boolean SUPPORTS_GET_AND_SET_REF; + public static final boolean SUPPORTS_GET_AND_ADD_LONG; + public static final Unsafe UNSAFE; + + static { + UNSAFE = getUnsafe(); + SUPPORTS_GET_AND_SET_REF = hasGetAndSetSupport(); + SUPPORTS_GET_AND_ADD_LONG = hasGetAndAddLongSupport(); + } + + private static Unsafe getUnsafe() { + Unsafe instance; + try { + final Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + instance = (Unsafe) field.get(null); + } catch (Exception ignored) { + // Some platforms, notably Android, might not have a sun.misc.Unsafe implementation with a + // private + // `theUnsafe` static instance. In this case we can try to call the default constructor, which + // is sufficient + // for Android usage. + try { + Constructor c = Unsafe.class.getDeclaredConstructor(); + c.setAccessible(true); + instance = c.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return instance; + } + + private static boolean hasGetAndSetSupport() { + try { + Unsafe.class.getMethod("getAndSetObject", Object.class, Long.TYPE, Object.class); + return true; + } catch (Exception ignored) { + } + return false; + } + + private static boolean hasGetAndAddLongSupport() { + try { + Unsafe.class.getMethod("getAndAddLong", Object.class, Long.TYPE, Long.TYPE); + return true; + } catch (Exception ignored) { + } + return false; + } + + public static long fieldOffset(Class clz, String fieldName) throws RuntimeException { + try { + return UNSAFE.objectFieldOffset(clz.getDeclaredField(fieldName)); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/UnsafeRefArrayAccess.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/UnsafeRefArrayAccess.java new file mode 100644 index 000000000..c734a9914 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/UnsafeRefArrayAccess.java @@ -0,0 +1,104 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.internal.jctools.queues; + +import static io.rsocket.internal.jctools.queues.UnsafeAccess.UNSAFE; + +final class UnsafeRefArrayAccess { + public static final long REF_ARRAY_BASE; + public static final int REF_ELEMENT_SHIFT; + + static { + final int scale = UNSAFE.arrayIndexScale(Object[].class); + if (4 == scale) { + REF_ELEMENT_SHIFT = 2; + } else if (8 == scale) { + REF_ELEMENT_SHIFT = 3; + } else { + throw new IllegalStateException("Unknown pointer size: " + scale); + } + REF_ARRAY_BASE = UNSAFE.arrayBaseOffset(Object[].class); + } + + /** + * A plain store (no ordering/fences) of an element to a given offset + * + * @param buffer this.buffer + * @param offset computed via {@link UnsafeRefArrayAccess#calcRefElementOffset(long)} + * @param e an orderly kitty + */ + public static void spRefElement(E[] buffer, long offset, E e) { + UNSAFE.putObject(buffer, offset, e); + } + + /** + * An ordered store of an element to a given offset + * + * @param buffer this.buffer + * @param offset computed via {@link UnsafeRefArrayAccess#calcCircularRefElementOffset} + * @param e an orderly kitty + */ + public static void soRefElement(E[] buffer, long offset, E e) { + UNSAFE.putOrderedObject(buffer, offset, e); + } + + /** + * A plain load (no ordering/fences) of an element from a given offset. + * + * @param buffer this.buffer + * @param offset computed via {@link UnsafeRefArrayAccess#calcRefElementOffset(long)} + * @return the element at the offset + */ + @SuppressWarnings("unchecked") + public static E lpRefElement(E[] buffer, long offset) { + return (E) UNSAFE.getObject(buffer, offset); + } + + /** + * A volatile load of an element from a given offset. + * + * @param buffer this.buffer + * @param offset computed via {@link UnsafeRefArrayAccess#calcRefElementOffset(long)} + * @return the element at the offset + */ + @SuppressWarnings("unchecked") + public static E lvRefElement(E[] buffer, long offset) { + return (E) UNSAFE.getObjectVolatile(buffer, offset); + } + + /** + * @param index desirable element index + * @return the offset in bytes within the array for a given index + */ + public static long calcRefElementOffset(long index) { + return REF_ARRAY_BASE + (index << REF_ELEMENT_SHIFT); + } + + /** + * Note: circular arrays are assumed a power of 2 in length and the `mask` is (length - 1). + * + * @param index desirable element index + * @param mask (length - 1) + * @return the offset in bytes within the circular array for a given index + */ + public static long calcCircularRefElementOffset(long index, long mask) { + return REF_ARRAY_BASE + ((index & mask) << REF_ELEMENT_SHIFT); + } + + /** This makes for an easier time generating the atomic queues, and removes some warnings. */ + @SuppressWarnings("unchecked") + public static E[] allocateRefArray(int capacity) { + return (E[]) new Object[capacity]; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/package-info.java b/rsocket-core/src/main/java/io/rsocket/internal/package-info.java index c1ed71ce8..07ddfab41 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/package-info.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -18,5 +18,7 @@ * Internal package and must not be used outside this project. There are no guarantees for * API compatibility. */ -@javax.annotation.ParametersAreNonnullByDefault +@NonNullApi package io.rsocket.internal; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveFramesAcceptor.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveFramesAcceptor.java new file mode 100644 index 000000000..8fb918dc6 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveFramesAcceptor.java @@ -0,0 +1,9 @@ +package io.rsocket.keepalive; + +import io.netty.buffer.ByteBuf; +import reactor.core.Disposable; + +public interface KeepAliveFramesAcceptor extends Disposable { + + void receive(ByteBuf keepAliveFrame); +} diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java new file mode 100644 index 000000000..2535c342b --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java @@ -0,0 +1,57 @@ +package io.rsocket.keepalive; + +import io.netty.buffer.ByteBuf; +import io.rsocket.Closeable; +import io.rsocket.keepalive.KeepAliveSupport.KeepAlive; +import io.rsocket.resume.ResumableDuplexConnection; +import java.util.function.Consumer; + +public interface KeepAliveHandler { + + KeepAliveFramesAcceptor start( + KeepAliveSupport keepAliveSupport, + Consumer onFrameSent, + 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) + .start(); + } + } + + class ResumableKeepAliveHandler implements KeepAliveHandler { + private final ResumableDuplexConnection resumableDuplexConnection; + + public ResumableKeepAliveHandler(ResumableDuplexConnection resumableDuplexConnection) { + this.resumableDuplexConnection = resumableDuplexConnection; + } + + @Override + public KeepAliveFramesAcceptor start( + KeepAliveSupport keepAliveSupport, + Consumer onSendKeepAliveFrame, + Consumer onTimeout) { + resumableDuplexConnection.onResume(keepAliveSupport::start); + resumableDuplexConnection.onDisconnect(keepAliveSupport::stop); + return keepAliveSupport + .resumeState(resumableDuplexConnection) + .onSendKeepAliveFrame(onSendKeepAliveFrame) + .onTimeout(keepAlive -> resumableDuplexConnection.disconnect()) + .start(); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java new file mode 100644 index 000000000..a67226ada --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java @@ -0,0 +1,194 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.keepalive; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.frame.KeepAliveFrameCodec; +import io.rsocket.resume.ResumeStateHolder; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +public abstract class KeepAliveSupport implements KeepAliveFramesAcceptor { + + final ByteBufAllocator allocator; + final Scheduler scheduler; + final Duration keepAliveInterval; + final Duration keepAliveTimeout; + final long keepAliveTimeoutMillis; + + final AtomicBoolean started = new AtomicBoolean(); + + volatile Consumer onTimeout; + volatile Consumer onFrameSent; + volatile Disposable ticksDisposable; + + volatile ResumeStateHolder resumeStateHolder; + volatile long lastReceivedMillis; + + private KeepAliveSupport( + ByteBufAllocator allocator, int keepAliveInterval, int keepAliveTimeout) { + this.allocator = allocator; + this.scheduler = Schedulers.parallel(); + this.keepAliveInterval = Duration.ofMillis(keepAliveInterval); + this.keepAliveTimeout = Duration.ofMillis(keepAliveTimeout); + this.keepAliveTimeoutMillis = keepAliveTimeout; + } + + public KeepAliveSupport start() { + this.lastReceivedMillis = scheduler.now(TimeUnit.MILLISECONDS); + if (started.compareAndSet(false, true)) { + ticksDisposable = + Flux.interval(keepAliveInterval, scheduler).subscribe(v -> onIntervalTick()); + } + return this; + } + + public void stop() { + if (started.compareAndSet(true, false)) { + ticksDisposable.dispose(); + } + } + + @Override + public void receive(ByteBuf keepAliveFrame) { + this.lastReceivedMillis = scheduler.now(TimeUnit.MILLISECONDS); + if (resumeStateHolder != null) { + long remoteLastReceivedPos = remoteLastReceivedPosition(keepAliveFrame); + resumeStateHolder.onImpliedPosition(remoteLastReceivedPos); + } + if (KeepAliveFrameCodec.respondFlag(keepAliveFrame)) { + long localLastReceivedPos = localLastReceivedPosition(); + send( + KeepAliveFrameCodec.encode( + allocator, + false, + localLastReceivedPos, + KeepAliveFrameCodec.data(keepAliveFrame).retain())); + } + } + + public KeepAliveSupport resumeState(ResumeStateHolder resumeStateHolder) { + this.resumeStateHolder = resumeStateHolder; + return this; + } + + public KeepAliveSupport onSendKeepAliveFrame(Consumer onFrameSent) { + this.onFrameSent = onFrameSent; + return this; + } + + public KeepAliveSupport onTimeout(Consumer onTimeout) { + this.onTimeout = onTimeout; + return this; + } + + abstract void onIntervalTick(); + + void send(ByteBuf frame) { + if (onFrameSent != null) { + onFrameSent.accept(frame); + } + } + + void tryTimeout() { + long now = scheduler.now(TimeUnit.MILLISECONDS); + if (now - lastReceivedMillis >= keepAliveTimeoutMillis) { + if (onTimeout != null) { + onTimeout.accept(new KeepAlive(keepAliveInterval, keepAliveTimeout)); + } + stop(); + } + } + + long localLastReceivedPosition() { + return resumeStateHolder != null ? resumeStateHolder.impliedPosition() : 0; + } + + long remoteLastReceivedPosition(ByteBuf keepAliveFrame) { + return KeepAliveFrameCodec.lastPosition(keepAliveFrame); + } + + @Override + public void dispose() { + stop(); + } + + @Override + public boolean isDisposed() { + return ticksDisposable.isDisposed(); + } + + /** + * @deprecated since it should not be used anymore and will be completely removed in 1.1. + * Keepalive is symmetric on both side and implemented as a part of RSocketRequester + */ + @Deprecated + public static final class ServerKeepAliveSupport extends KeepAliveSupport { + + public ServerKeepAliveSupport( + ByteBufAllocator allocator, int keepAlivePeriod, int keepAliveTimeout) { + super(allocator, keepAlivePeriod, keepAliveTimeout); + } + + @Override + void onIntervalTick() { + tryTimeout(); + } + } + + public static final class ClientKeepAliveSupport extends KeepAliveSupport { + + public ClientKeepAliveSupport( + ByteBufAllocator allocator, int keepAliveInterval, int keepAliveTimeout) { + super(allocator, keepAliveInterval, keepAliveTimeout); + } + + @Override + void onIntervalTick() { + tryTimeout(); + send( + KeepAliveFrameCodec.encode( + allocator, true, localLastReceivedPosition(), Unpooled.EMPTY_BUFFER)); + } + } + + public static final class KeepAlive { + private final Duration tickPeriod; + private final Duration timeoutMillis; + + public KeepAlive(Duration tickPeriod, Duration timeoutMillis) { + this.tickPeriod = tickPeriod; + this.timeoutMillis = timeoutMillis; + } + + public Duration getTickPeriod() { + return tickPeriod; + } + + public Duration getTimeout() { + return timeoutMillis; + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/package-info.java b/rsocket-core/src/main/java/io/rsocket/keepalive/package-info.java new file mode 100644 index 000000000..d94a93cad --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Support classes for sending and keeping track of KEEPALIVE frames from the remote. */ +@NonNullApi +package io.rsocket.keepalive; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/lease/Lease.java b/rsocket-core/src/main/java/io/rsocket/lease/Lease.java index 1523f48d6..673b4a480 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/Lease.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/Lease.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,11 +16,21 @@ package io.rsocket.lease; -import java.nio.ByteBuffer; -import javax.annotation.Nullable; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.rsocket.Availability; +import reactor.util.annotation.Nullable; /** A contract for RSocket lease, which is sent by a request acceptor and is time bound. */ -public interface Lease { +public interface Lease extends Availability { + + static Lease create(int timeToLiveMillis, int numberOfRequests, @Nullable ByteBuf metadata) { + return LeaseImpl.create(timeToLiveMillis, numberOfRequests, metadata); + } + + static Lease create(int timeToLiveMillis, int numberOfRequests) { + return create(timeToLiveMillis, numberOfRequests, Unpooled.EMPTY_BUFFER); + } /** * Number of requests allowed by this lease. @@ -30,11 +40,30 @@ public interface Lease { int getAllowedRequests(); /** - * Number of seconds that this lease is valid from the time it is received. + * Initial number of requests allowed by this lease. + * + * @return initial number of requests allowed by this lease. + */ + default int getStartingAllowedRequests() { + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Number of milliseconds that this lease is valid from the time it is received. * - * @return Number of seconds that this lease is valid from the time it is received. + * @return Number of milliseconds that this lease is valid from the time it is received. */ - int getTtl(); + int getTimeToLiveMillis(); + + /** + * Number of milliseconds that this lease is still valid from now. + * + * @param now millis since epoch + * @return Number of milliseconds that this lease is still valid from now, or 0 if expired. + */ + default int getRemainingTimeToLiveMillis(long now) { + return isEmpty() ? 0 : (int) Math.max(0, expiry() - now); + } /** * Absolute time since epoch at which this lease will expire. @@ -48,8 +77,7 @@ public interface Lease { * * @return Metadata for the lease. */ - @Nullable - ByteBuffer getMetadata(); + ByteBuf getMetadata(); /** * Checks if the lease is expired now. @@ -63,9 +91,20 @@ default boolean isExpired() { /** * Checks if the lease is expired for the passed {@code now}. * + * @param now current time in millis. * @return {@code true} if the lease has expired. */ default boolean isExpired(long now) { return now > expiry(); } + + /** Checks if the lease has not expired and there are allowed requests available */ + default boolean isValid() { + return !isExpired() && getAllowedRequests() > 0; + } + + /** Checks if the lease is empty(default value if no lease was received yet) */ + default boolean isEmpty() { + return getAllowedRequests() == 0 && getTimeToLiveMillis() == 0; + } } diff --git a/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java b/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java index 0f99092fb..7abb8aab9 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,43 +16,52 @@ package io.rsocket.lease; -import io.rsocket.Frame; -import java.nio.ByteBuffer; -import javax.annotation.Nullable; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.concurrent.atomic.AtomicInteger; +import reactor.util.annotation.Nullable; -public final class LeaseImpl implements Lease { - - private final int allowedRequests; - private final int ttl; +public class LeaseImpl implements Lease { + private final int timeToLiveMillis; + private final AtomicInteger allowedRequests; + private final int startingAllowedRequests; + private final ByteBuf metadata; private final long expiry; - private final @Nullable ByteBuffer metadata; - public LeaseImpl(int allowedRequests, int ttl) { - this(allowedRequests, ttl, null); + static LeaseImpl create(int timeToLiveMillis, int numberOfRequests, @Nullable ByteBuf metadata) { + assertLease(timeToLiveMillis, numberOfRequests); + return new LeaseImpl(timeToLiveMillis, numberOfRequests, metadata); + } + + static LeaseImpl empty() { + return new LeaseImpl(0, 0, null); } - public LeaseImpl(int allowedRequests, int ttl, ByteBuffer metadata) { - this.allowedRequests = allowedRequests; - this.ttl = ttl; - expiry = System.currentTimeMillis() + ttl; - this.metadata = metadata; + private LeaseImpl(int timeToLiveMillis, int allowedRequests, @Nullable ByteBuf metadata) { + this.allowedRequests = new AtomicInteger(allowedRequests); + this.startingAllowedRequests = allowedRequests; + this.timeToLiveMillis = timeToLiveMillis; + this.metadata = metadata == null ? Unpooled.EMPTY_BUFFER : metadata; + this.expiry = timeToLiveMillis == 0 ? 0 : now() + timeToLiveMillis; } - public LeaseImpl(Frame leaseFrame) { - this( - Frame.Lease.numberOfRequests(leaseFrame), - Frame.Lease.ttl(leaseFrame), - leaseFrame.getMetadata()); + public int getTimeToLiveMillis() { + return timeToLiveMillis; } @Override public int getAllowedRequests() { - return allowedRequests; + return Math.max(0, allowedRequests.get()); } @Override - public int getTtl() { - return ttl; + public int getStartingAllowedRequests() { + return startingAllowedRequests; + } + + @Override + public ByteBuf getMetadata() { + return metadata; } @Override @@ -61,19 +70,56 @@ public long expiry() { } @Override - public ByteBuffer getMetadata() { - return metadata; + public boolean isValid() { + return !isEmpty() && getAllowedRequests() > 0 && !isExpired(); + } + + /** + * try use 1 allowed request of Lease + * + * @return true if used successfully, false if Lease is expired or no allowed requests available + */ + public boolean use() { + if (isExpired()) { + return false; + } + int remaining = + allowedRequests.accumulateAndGet(1, (cur, update) -> Math.max(-1, cur - update)); + return remaining >= 0; + } + + @Override + public double availability() { + return isValid() ? getAllowedRequests() / (double) getStartingAllowedRequests() : 0.0; } @Override public String toString() { + long now = now(); return "LeaseImpl{" - + "allowedRequests=" - + allowedRequests - + ", ttl=" - + ttl - + ", expiry=" - + expiry + + "timeToLiveMillis=" + + timeToLiveMillis + + ", allowedRequests=" + + getAllowedRequests() + + ", startingAllowedRequests=" + + startingAllowedRequests + + ", expired=" + + isExpired(now) + + ", remainingTimeToLiveMillis=" + + getRemainingTimeToLiveMillis(now) + '}'; } + + private static long now() { + return System.currentTimeMillis(); + } + + private static void assertLease(int timeToLiveMillis, int numberOfRequests) { + if (numberOfRequests <= 0) { + throw new IllegalArgumentException("Number of requests must be positive"); + } + if (timeToLiveMillis <= 0) { + throw new IllegalArgumentException("Time-to-live must be positive"); + } + } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionException.java b/rsocket-core/src/main/java/io/rsocket/lease/LeaseStats.java similarity index 66% rename from rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionException.java rename to rsocket-core/src/main/java/io/rsocket/lease/LeaseStats.java index b67933feb..791f5a023 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionException.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/LeaseStats.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,10 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.exceptions; -public class ConnectionException extends RuntimeException implements Retryable { - public ConnectionException(String message) { - super(message); +package io.rsocket.lease; + +public interface LeaseStats { + + void onEvent(EventType eventType); + + enum EventType { + ACCEPT, + REJECT, + TERMINATE } } diff --git a/rsocket-core/src/main/java/io/rsocket/lease/Leases.java b/rsocket-core/src/main/java/io/rsocket/lease/Leases.java new file mode 100644 index 000000000..4c90e38ce --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/lease/Leases.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.lease; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import reactor.core.publisher.Flux; + +public class Leases { + private static final Function> noopLeaseSender = leaseStats -> Flux.never(); + private static final Consumer> noopLeaseReceiver = leases -> {}; + + private Function> leaseSender = noopLeaseSender; + private Consumer> leaseReceiver = noopLeaseReceiver; + private Optional stats = Optional.empty(); + + public static Leases create() { + return new Leases<>(); + } + + public Leases sender(Function, Flux> leaseSender) { + this.leaseSender = leaseSender; + return this; + } + + public Leases receiver(Consumer> leaseReceiver) { + this.leaseReceiver = leaseReceiver; + return this; + } + + public Leases stats(T stats) { + this.stats = Optional.of(Objects.requireNonNull(stats)); + return this; + } + + @SuppressWarnings("unchecked") + public Function, Flux> sender() { + return (Function, Flux>) leaseSender; + } + + public Consumer> receiver() { + return leaseReceiver; + } + + @SuppressWarnings("unchecked") + public Optional stats() { + return (Optional) stats; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/lease/MissingLeaseException.java b/rsocket-core/src/main/java/io/rsocket/lease/MissingLeaseException.java new file mode 100644 index 000000000..3b6cec62c --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/lease/MissingLeaseException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.lease; + +import io.rsocket.exceptions.RejectedException; +import java.util.Objects; +import reactor.util.annotation.Nullable; + +public class MissingLeaseException extends RejectedException { + private static final long serialVersionUID = -6169748673403858959L; + + public MissingLeaseException(Lease lease, String tag) { + super(leaseMessage(Objects.requireNonNull(lease), Objects.requireNonNull(tag))); + } + + public MissingLeaseException(String tag) { + super(leaseMessage(null, Objects.requireNonNull(tag))); + } + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + + static String leaseMessage(@Nullable Lease lease, String tag) { + if (lease == null) { + return String.format("[%s] Missing leases", tag); + } + if (lease.isEmpty()) { + return String.format("[%s] Lease was not received yet", tag); + } + boolean expired = lease.isExpired(); + int allowedRequests = lease.getAllowedRequests(); + return String.format( + "[%s] Missing leases. Expired: %b, allowedRequests: %d", tag, expired, allowedRequests); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java b/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java new file mode 100644 index 000000000..fd569a2c8 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java @@ -0,0 +1,113 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.lease; + +import io.netty.buffer.ByteBuf; +import io.rsocket.Availability; +import io.rsocket.frame.LeaseFrameCodec; +import java.util.function.Consumer; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.ReplayProcessor; + +public interface RequesterLeaseHandler extends Availability, Disposable { + + boolean useLease(); + + Exception leaseError(); + + void receive(ByteBuf leaseFrame); + + void dispose(); + + final class Impl implements RequesterLeaseHandler { + private final String tag; + private final ReplayProcessor receivedLease; + private volatile LeaseImpl currentLease = LeaseImpl.empty(); + + public Impl(String tag, Consumer> leaseReceiver) { + this.tag = tag; + receivedLease = ReplayProcessor.create(1); + leaseReceiver.accept(receivedLease); + } + + @Override + public boolean useLease() { + return currentLease.use(); + } + + @Override + public Exception leaseError() { + LeaseImpl l = this.currentLease; + String t = this.tag; + if (!l.isValid()) { + return new MissingLeaseException(l, t); + } else { + return new MissingLeaseException(t); + } + } + + @Override + public void receive(ByteBuf leaseFrame) { + int numberOfRequests = LeaseFrameCodec.numRequests(leaseFrame); + int timeToLiveMillis = LeaseFrameCodec.ttl(leaseFrame); + ByteBuf metadata = LeaseFrameCodec.metadata(leaseFrame); + LeaseImpl lease = LeaseImpl.create(timeToLiveMillis, numberOfRequests, metadata); + currentLease = lease; + receivedLease.onNext(lease); + } + + @Override + public void dispose() { + receivedLease.onComplete(); + } + + @Override + public boolean isDisposed() { + return receivedLease.isTerminated(); + } + + @Override + public double availability() { + return currentLease.availability(); + } + } + + RequesterLeaseHandler None = + new RequesterLeaseHandler() { + @Override + public boolean useLease() { + return true; + } + + @Override + public Exception leaseError() { + throw new AssertionError("Error not possible with NOOP leases handler"); + } + + @Override + public void receive(ByteBuf leaseFrame) {} + + @Override + public void dispose() {} + + @Override + public double availability() { + return 1.0; + } + }; +} diff --git a/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java b/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java new file mode 100644 index 000000000..df8787cb7 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java @@ -0,0 +1,146 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.lease; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Availability; +import io.rsocket.frame.LeaseFrameCodec; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.publisher.Flux; +import reactor.util.annotation.Nullable; + +public interface ResponderLeaseHandler extends Availability { + + boolean useLease(); + + Exception leaseError(); + + Disposable send(Consumer leaseFrameSender); + + final class Impl implements ResponderLeaseHandler { + private volatile LeaseImpl currentLease = LeaseImpl.empty(); + private final String tag; + private final ByteBufAllocator allocator; + private final Function, Flux> leaseSender; + private final Optional leaseStatsOption; + private final T leaseStats; + + public Impl( + String tag, + ByteBufAllocator allocator, + Function, Flux> leaseSender, + Optional leaseStatsOption) { + this.tag = tag; + this.allocator = allocator; + this.leaseSender = leaseSender; + this.leaseStatsOption = leaseStatsOption; + this.leaseStats = leaseStatsOption.orElse(null); + } + + @Override + public boolean useLease() { + boolean success = currentLease.use(); + onUseEvent(success, leaseStats); + return success; + } + + @Override + public Exception leaseError() { + LeaseImpl l = currentLease; + String t = tag; + if (!l.isValid()) { + return new MissingLeaseException(l, t); + } else { + return new MissingLeaseException(t); + } + } + + @Override + public Disposable send(Consumer leaseFrameSender) { + return leaseSender + .apply(leaseStatsOption) + .doOnTerminate(this::onTerminateEvent) + .subscribe( + lease -> { + currentLease = create(lease); + leaseFrameSender.accept(createLeaseFrame(lease)); + }); + } + + @Override + public double availability() { + return currentLease.availability(); + } + + private ByteBuf createLeaseFrame(Lease lease) { + return LeaseFrameCodec.encode( + allocator, lease.getTimeToLiveMillis(), lease.getAllowedRequests(), lease.getMetadata()); + } + + private void onTerminateEvent() { + T ls = leaseStats; + if (ls != null) { + ls.onEvent(LeaseStats.EventType.TERMINATE); + } + } + + private void onUseEvent(boolean success, @Nullable T ls) { + if (ls != null) { + LeaseStats.EventType eventType = + success ? LeaseStats.EventType.ACCEPT : LeaseStats.EventType.REJECT; + ls.onEvent(eventType); + } + } + + private static LeaseImpl create(Lease lease) { + if (lease instanceof LeaseImpl) { + return (LeaseImpl) lease; + } else { + return LeaseImpl.create( + lease.getTimeToLiveMillis(), lease.getAllowedRequests(), lease.getMetadata()); + } + } + } + + ResponderLeaseHandler None = + new ResponderLeaseHandler() { + @Override + public boolean useLease() { + return true; + } + + @Override + public Exception leaseError() { + throw new AssertionError("Error not possible with NOOP leases handler"); + } + + @Override + public Disposable send(Consumer leaseFrameSender) { + return Disposables.disposed(); + } + + @Override + public double availability() { + return 1.0; + } + }; +} diff --git a/rsocket-core/src/main/java/io/rsocket/lease/package-info.java b/rsocket-core/src/main/java/io/rsocket/lease/package-info.java index c0109b3e5..342ab27f7 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/package-info.java @@ -1,18 +1,27 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -@javax.annotation.ParametersAreNonnullByDefault +/** + * Contains support classes for the Lease feature of the RSocket protocol. + * + * @see Resuming + * Operation + */ +@NonNullApi package io.rsocket.lease; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java b/rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java new file mode 100644 index 000000000..c16c4dc52 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java @@ -0,0 +1,334 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.rsocket.util.CharByteBufUtil; + +public class AuthMetadataCodec { + + static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 + static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 + + static final int USERNAME_BYTES_LENGTH = 2; + static final int AUTH_TYPE_ID_LENGTH = 1; + + static final char[] EMPTY_CHARS_ARRAY = new char[0]; + + private AuthMetadataCodec() {} + + /** + * Encode a Authentication CompositeMetadata payload using custom authentication type + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param customAuthType the custom mime type to encode. + * @param metadata the metadata value to encode. + * @throws IllegalArgumentException in case of {@code customAuthType} is non US_ASCII string or + * empty string or its length is greater than 128 bytes + */ + public static ByteBuf encodeMetadata( + ByteBufAllocator allocator, String customAuthType, ByteBuf metadata) { + + int actualASCIILength = ByteBufUtil.utf8Bytes(customAuthType); + if (actualASCIILength != customAuthType.length()) { + throw new IllegalArgumentException("custom auth type must be US_ASCII characters only"); + } + if (actualASCIILength < 1 || actualASCIILength > 128) { + throw new IllegalArgumentException( + "custom auth type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + int capacity = 1 + actualASCIILength; + ByteBuf headerBuffer = allocator.buffer(capacity, capacity); + // encoded length is one less than actual length, since 0 is never a valid length, which gives + // wider representation range + headerBuffer.writeByte(actualASCIILength - 1); + + ByteBufUtil.reserveAndWriteUtf8(headerBuffer, customAuthType, actualASCIILength); + + return allocator.compositeBuffer(2).addComponents(true, headerBuffer, metadata); + } + + /** + * Encode a Authentication CompositeMetadata payload using custom authentication type + * + * @param allocator the {@link ByteBufAllocator} to create intermediate buffers as needed. + * @param authType the well-known mime type to encode. + * @param metadata the metadata value to encode. + * @throws IllegalArgumentException in case of {@code authType} is {@link + * WellKnownAuthType#UNPARSEABLE_AUTH_TYPE} or {@link + * WellKnownAuthType#UNKNOWN_RESERVED_AUTH_TYPE} + */ + public static ByteBuf encodeMetadata( + ByteBufAllocator allocator, WellKnownAuthType authType, ByteBuf metadata) { + + if (authType == WellKnownAuthType.UNPARSEABLE_AUTH_TYPE + || authType == WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE) { + throw new IllegalArgumentException("only allowed AuthType should be used"); + } + + int capacity = AUTH_TYPE_ID_LENGTH; + ByteBuf headerBuffer = + allocator + .buffer(capacity, capacity) + .writeByte(authType.getIdentifier() | STREAM_METADATA_KNOWN_MASK); + + return allocator.compositeBuffer(2).addComponents(true, headerBuffer, metadata); + } + + /** + * Encode a Authentication CompositeMetadata payload using Simple Authentication format + * + * @throws IllegalArgumentException if the username length is greater than 65535 + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param username the char sequence which represents user name. + * @param password the char sequence which represents user password. + */ + public static ByteBuf encodeSimpleMetadata( + ByteBufAllocator allocator, char[] username, char[] password) { + + int usernameLength = CharByteBufUtil.utf8Bytes(username); + if (usernameLength > 65535) { + throw new IllegalArgumentException( + "Username should be shorter than or equal to 65535 bytes length in UTF-8 encoding"); + } + + int passwordLength = CharByteBufUtil.utf8Bytes(password); + int capacity = AUTH_TYPE_ID_LENGTH + USERNAME_BYTES_LENGTH + usernameLength + passwordLength; + final ByteBuf buffer = + allocator + .buffer(capacity, capacity) + .writeByte(WellKnownAuthType.SIMPLE.getIdentifier() | STREAM_METADATA_KNOWN_MASK) + .writeShort(usernameLength); + + CharByteBufUtil.writeUtf8(buffer, username); + CharByteBufUtil.writeUtf8(buffer, password); + + return buffer; + } + + /** + * Encode a Authentication CompositeMetadata payload using Bearer Authentication format + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param token the char sequence which represents BEARER token. + */ + public static ByteBuf encodeBearerMetadata(ByteBufAllocator allocator, char[] token) { + + int tokenLength = CharByteBufUtil.utf8Bytes(token); + int capacity = AUTH_TYPE_ID_LENGTH + tokenLength; + final ByteBuf buffer = + allocator + .buffer(capacity, capacity) + .writeByte(WellKnownAuthType.BEARER.getIdentifier() | STREAM_METADATA_KNOWN_MASK); + + CharByteBufUtil.writeUtf8(buffer, token); + + return buffer; + } + + /** + * Encode a new Authentication Metadata payload information, first verifying if the passed {@link + * String} matches a {@link WellKnownAuthType} (in which case it will be encoded in a compressed + * fashion using the mime id of that type). + * + *

Prefer using {@link #encodeMetadata(ByteBufAllocator, String, ByteBuf)} if you already know + * that the mime type is not a {@link WellKnownAuthType}. + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param authType the mime type to encode, as a {@link String}. well known mime types are + * compressed. + * @param metadata the metadata value to encode. + * @see #encodeMetadata(ByteBufAllocator, WellKnownAuthType, ByteBuf) + * @see #encodeMetadata(ByteBufAllocator, String, ByteBuf) + */ + public static ByteBuf encodeMetadataWithCompression( + ByteBufAllocator allocator, String authType, ByteBuf metadata) { + WellKnownAuthType wkn = WellKnownAuthType.fromString(authType); + if (wkn == WellKnownAuthType.UNPARSEABLE_AUTH_TYPE) { + return AuthMetadataCodec.encodeMetadata(allocator, authType, metadata); + } else { + return AuthMetadataCodec.encodeMetadata(allocator, wkn, metadata); + } + } + + /** + * Get the first {@code byte} from a {@link ByteBuf} and check whether it is length or {@link + * WellKnownAuthType}. Assuming said buffer properly contains such a {@code byte} + * + * @param metadata byteBuf used to get information from + */ + public static boolean isWellKnownAuthType(ByteBuf metadata) { + byte lengthOrId = metadata.getByte(0); + return (lengthOrId & STREAM_METADATA_LENGTH_MASK) != lengthOrId; + } + + /** + * Read first byte from the given {@code metadata} and tries to convert it's value to {@link + * WellKnownAuthType}. + * + * @param metadata given metadata buffer to read from + * @return Return on of the know Auth types or {@link WellKnownAuthType#UNPARSEABLE_AUTH_TYPE} if + * field's value is length or unknown auth type + * @throws IllegalStateException if not enough readable bytes in the given {@link ByteBuf} + */ + public static WellKnownAuthType readWellKnownAuthType(ByteBuf metadata) { + if (metadata.readableBytes() < 1) { + throw new IllegalStateException( + "Unable to decode Well Know Auth type. Not enough readable bytes"); + } + byte lengthOrId = metadata.readByte(); + int normalizedId = (byte) (lengthOrId & STREAM_METADATA_LENGTH_MASK); + + if (normalizedId != lengthOrId) { + return WellKnownAuthType.fromIdentifier(normalizedId); + } + + return WellKnownAuthType.UNPARSEABLE_AUTH_TYPE; + } + + /** + * Read up to 129 bytes from the given metadata in order to get the custom Auth Type + * + * @param metadata + * @return + */ + public static CharSequence readCustomAuthType(ByteBuf metadata) { + if (metadata.readableBytes() < 2) { + throw new IllegalStateException( + "Unable to decode custom Auth type. Not enough readable bytes"); + } + + byte encodedLength = metadata.readByte(); + if (encodedLength < 0) { + throw new IllegalStateException( + "Unable to decode custom Auth type. Incorrect auth type length"); + } + + // encoded length is realLength - 1 in order to avoid intersection with 0x00 authtype + int realLength = encodedLength + 1; + if (metadata.readableBytes() < realLength) { + throw new IllegalArgumentException( + "Unable to decode custom Auth type. Malformed length or auth type string"); + } + + return metadata.readCharSequence(realLength, CharsetUtil.US_ASCII); + } + + /** + * Read all remaining {@code bytes} from the given {@link ByteBuf} and return sliced + * representation of a payload + * + * @param metadata metadata to get payload from. Please note, the {@code metadata#readIndex} + * should be set to the beginning of the payload bytes + * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if no bytes readable in the + * given one + */ + public static ByteBuf readPayload(ByteBuf metadata) { + if (metadata.readableBytes() == 0) { + return Unpooled.EMPTY_BUFFER; + } + + return metadata.readSlice(metadata.readableBytes()); + } + + /** + * Read up to 65537 {@code bytes} from the given {@link ByteBuf} where the first two bytes + * represent username length and the subsequent number of bytes equal to read length + * + * @param simpleAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the username length position + * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if username length is zero + */ + public static ByteBuf readUsername(ByteBuf simpleAuthMetadata) { + int usernameLength = readUsernameLength(simpleAuthMetadata); + + if (usernameLength == 0) { + return Unpooled.EMPTY_BUFFER; + } + + return simpleAuthMetadata.readSlice(usernameLength); + } + + /** + * Read all the remaining {@code byte}s from the given {@link ByteBuf} which represents user's + * password + * + * @param simpleAuthMetadata the given metadata to read password from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes + * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if password length is zero + */ + public static ByteBuf readPassword(ByteBuf simpleAuthMetadata) { + if (simpleAuthMetadata.readableBytes() == 0) { + return Unpooled.EMPTY_BUFFER; + } + + return simpleAuthMetadata.readSlice(simpleAuthMetadata.readableBytes()); + } + /** + * Read up to 65537 {@code bytes} from the given {@link ByteBuf} where the first two bytes + * represent username length and the subsequent number of bytes equal to read length + * + * @param simpleAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the username length byte + * @return {@code char[]} which represents UTF-8 username + */ + public static char[] readUsernameAsCharArray(ByteBuf simpleAuthMetadata) { + int usernameLength = readUsernameLength(simpleAuthMetadata); + + if (usernameLength == 0) { + return EMPTY_CHARS_ARRAY; + } + + return CharByteBufUtil.readUtf8(simpleAuthMetadata, usernameLength); + } + + /** + * Read all the remaining {@code byte}s from the given {@link ByteBuf} which represents user's + * password + * + * @param simpleAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes + * @return {@code char[]} which represents UTF-8 password + */ + public static char[] readPasswordAsCharArray(ByteBuf simpleAuthMetadata) { + if (simpleAuthMetadata.readableBytes() == 0) { + return EMPTY_CHARS_ARRAY; + } + + return CharByteBufUtil.readUtf8(simpleAuthMetadata, simpleAuthMetadata.readableBytes()); + } + + /** + * Read all the remaining {@code bytes} from the given {@link ByteBuf} + * + * @param bearerAuthMetadata the given metadata to read username from. Please note, the {@code + * bearerAuthMetadata#readIndex} should be set to the beginning of the password bytes + * @return {@code char[]} which represents UTF-8 password + */ + public static char[] readBearerTokenAsCharArray(ByteBuf bearerAuthMetadata) { + if (bearerAuthMetadata.readableBytes() == 0) { + return EMPTY_CHARS_ARRAY; + } + + return CharByteBufUtil.readUtf8(bearerAuthMetadata, bearerAuthMetadata.readableBytes()); + } + + private static int readUsernameLength(ByteBuf simpleAuthMetadata) { + if (simpleAuthMetadata.readableBytes() < 2) { + throw new IllegalStateException( + "Unable to decode custom username. Not enough readable bytes"); + } + + int usernameLength = simpleAuthMetadata.readUnsignedShort(); + + if (simpleAuthMetadata.readableBytes() < usernameLength) { + throw new IllegalArgumentException( + "Unable to decode username. Malformed username length or content"); + } + + return usernameLength; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java new file mode 100644 index 000000000..4a48921b1 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java @@ -0,0 +1,241 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.metadata; + +import static io.rsocket.metadata.CompositeMetadataFlyweight.computeNextEntryIndex; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeAndContentBuffersSlices; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeIdFromMimeBuffer; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer; +import static io.rsocket.metadata.CompositeMetadataFlyweight.hasEntry; +import static io.rsocket.metadata.CompositeMetadataFlyweight.isWellKnownMimeType; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.metadata.CompositeMetadata.Entry; +import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import reactor.util.annotation.Nullable; + +/** + * An {@link Iterable} wrapper around a {@link ByteBuf} that exposes metadata entry information at + * each decoding step. This is only possible on frame types used to initiate interactions, if the + * SETUP metadata mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. + * + *

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

A note on future-proofness: it is possible to come across a compressed mime type that this + * implementation doesn't recognize. This is likely to be due to the use of a byte id that is merely + * reserved in this implementation, but maps to a {@link WellKnownMimeType} in the implementation + * that encoded the metadata. This can be detected by detecting that an entry is a {@link + * ReservedMimeTypeEntry}. In this case {@link Entry#getMimeType()} will return {@code null}. The + * encoded id can be retrieved using {@link ReservedMimeTypeEntry#getType()}. The byte and content + * buffer should be kept around and re-encoded using {@link + * CompositeMetadataFlyweight#encodeAndAddMetadata(CompositeByteBuf, ByteBufAllocator, byte, + * ByteBuf)} in case passing that entry through is required. + */ +public final class CompositeMetadata implements Iterable { + + private final boolean retainSlices; + + private final ByteBuf source; + + public CompositeMetadata(ByteBuf source, boolean retainSlices) { + this.source = source; + this.retainSlices = retainSlices; + } + + /** + * Turn this {@link CompositeMetadata} into a sequential {@link Stream}. + * + * @return the composite metadata sequential {@link Stream} + */ + public Stream stream() { + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + iterator(), Spliterator.DISTINCT | Spliterator.NONNULL | Spliterator.ORDERED), + false); + } + + /** + * An {@link Iterator} that lazily decodes {@link Entry} in this composite metadata. + * + * @return the composite metadata {@link Iterator} + */ + @Override + public Iterator iterator() { + return new Iterator() { + + private int entryIndex = 0; + + @Override + public boolean hasNext() { + return hasEntry(CompositeMetadata.this.source, this.entryIndex); + } + + @Override + public Entry next() { + ByteBuf[] headerAndData = + decodeMimeAndContentBuffersSlices( + CompositeMetadata.this.source, + this.entryIndex, + CompositeMetadata.this.retainSlices); + + ByteBuf header = headerAndData[0]; + ByteBuf data = headerAndData[1]; + + this.entryIndex = computeNextEntryIndex(this.entryIndex, header, data); + + if (!isWellKnownMimeType(header)) { + CharSequence typeString = decodeMimeTypeFromMimeBuffer(header); + if (typeString == null) { + throw new IllegalStateException("MIME type cannot be null"); + } + + return new ExplicitMimeTimeEntry(data, typeString.toString()); + } + + byte id = decodeMimeIdFromMimeBuffer(header); + WellKnownMimeType type = WellKnownMimeType.fromIdentifier(id); + + if (WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE == type) { + return new ReservedMimeTypeEntry(data, id); + } + + return new WellKnownMimeTypeEntry(data, type); + } + }; + } + + /** An entry in the {@link CompositeMetadata}. */ + public interface Entry { + + /** + * Returns the un-decoded content of the {@link Entry}. + * + * @return the un-decoded content of the {@link Entry} + */ + ByteBuf getContent(); + + /** + * Returns the MIME type of the entry, if it can be decoded. + * + * @return the MIME type of the entry, if it can be decoded, otherwise {@code null}. + */ + @Nullable + String getMimeType(); + } + + /** An {@link Entry} backed by an explicitly declared MIME type. */ + public static final class ExplicitMimeTimeEntry implements Entry { + + private final ByteBuf content; + + private final String type; + + public ExplicitMimeTimeEntry(ByteBuf content, String type) { + this.content = content; + this.type = type; + } + + @Override + public ByteBuf getContent() { + return this.content; + } + + @Override + public String getMimeType() { + return this.type; + } + } + + /** + * An {@link Entry} backed by a {@link WellKnownMimeType} entry, but one that is not understood by + * this implementation. + */ + public static final class ReservedMimeTypeEntry implements Entry { + private final ByteBuf content; + private final int type; + + public ReservedMimeTypeEntry(ByteBuf content, int type) { + this.content = content; + this.type = type; + } + + @Override + public ByteBuf getContent() { + return this.content; + } + + /** + * {@inheritDoc} Since this entry represents a compressed id that couldn't be decoded, this is + * always {@code null}. + */ + @Override + public String getMimeType() { + return null; + } + + /** + * Returns the reserved, but unknown {@link WellKnownMimeType} for this entry. Range is 0-127 + * (inclusive). + * + * @return the reserved, but unknown {@link WellKnownMimeType} for this entry + */ + public int getType() { + return this.type; + } + } + + /** An {@link Entry} backed by a {@link WellKnownMimeType}. */ + public static final class WellKnownMimeTypeEntry implements Entry { + + private final ByteBuf content; + private final WellKnownMimeType type; + + public WellKnownMimeTypeEntry(ByteBuf content, WellKnownMimeType type) { + this.content = content; + this.type = type; + } + + @Override + public ByteBuf getContent() { + return this.content; + } + + @Override + public String getMimeType() { + return this.type.getString(); + } + + /** + * Returns the {@link WellKnownMimeType} for this entry. + * + * @return the {@link WellKnownMimeType} for this entry + */ + public WellKnownMimeType getType() { + return this.type; + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataCodec.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataCodec.java new file mode 100644 index 000000000..5e00abba8 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataCodec.java @@ -0,0 +1,385 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.netty.util.CharsetUtil; +import io.rsocket.util.NumberUtils; +import reactor.util.annotation.Nullable; + +/** + * A flyweight class that can be used to encode/decode composite metadata information to/from {@link + * ByteBuf}. This is intended for low-level efficient manipulation of such buffers. See {@link + * CompositeMetadata} for an Iterator-like approach to decoding entries. + */ +public class CompositeMetadataCodec { + + static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 + + static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 + + private CompositeMetadataCodec() {} + + public static int computeNextEntryIndex( + int currentEntryIndex, ByteBuf headerSlice, ByteBuf contentSlice) { + return currentEntryIndex + + headerSlice.readableBytes() // this includes the mime length byte + + 3 // 3 bytes of the content length, which are excluded from the slice + + contentSlice.readableBytes(); + } + + /** + * Decode the next metadata entry (a mime header + content pair of {@link ByteBuf}) from a {@link + * ByteBuf} that contains at least enough bytes for one more such entry. These buffers are + * actually slices of the full metadata buffer, and this method doesn't move the full metadata + * buffer's {@link ByteBuf#readerIndex()}. As such, it requires the user to provide an {@code + * index} to read from. The next index is computed by calling {@link #computeNextEntryIndex(int, + * ByteBuf, ByteBuf)}. Size of the first buffer (the "header buffer") drives which decoding method + * should be further applied to it. + * + *

The header buffer is either: + * + *

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

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

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

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

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

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

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

This compact representation encodes the mime type via its ID on a single byte, and the + * unsigned value length on 3 additional bytes. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer. + * @param mimeType a byte identifier of a {@link WellKnownMimeType} to encode. + * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits + * integer. + * @return the encoded mime and metadata length information + */ + static ByteBuf encodeMetadataHeader( + ByteBufAllocator allocator, byte mimeType, int metadataLength) { + ByteBuf buffer = allocator.buffer(4, 4).writeByte(mimeType | STREAM_METADATA_KNOWN_MASK); + + NumberUtils.encodeUnsignedMedium(buffer, metadataLength); + + return buffer; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java new file mode 100644 index 000000000..9916dfd3b --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -0,0 +1,262 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import reactor.util.annotation.Nullable; + +/** + * A flyweight class that can be used to encode/decode composite metadata information to/from {@link + * ByteBuf}. This is intended for low-level efficient manipulation of such buffers. See {@link + * CompositeMetadata} for an Iterator-like approach to decoding entries. + * + * @deprecated in favor of {@link CompositeMetadataCodec} + */ +@Deprecated +public class CompositeMetadataFlyweight { + + private CompositeMetadataFlyweight() {} + + public static int computeNextEntryIndex( + int currentEntryIndex, ByteBuf headerSlice, ByteBuf contentSlice) { + return CompositeMetadataCodec.computeNextEntryIndex( + currentEntryIndex, headerSlice, contentSlice); + } + + /** + * Decode the next metadata entry (a mime header + content pair of {@link ByteBuf}) from a {@link + * ByteBuf} that contains at least enough bytes for one more such entry. These buffers are + * actually slices of the full metadata buffer, and this method doesn't move the full metadata + * buffer's {@link ByteBuf#readerIndex()}. As such, it requires the user to provide an {@code + * index} to read from. The next index is computed by calling {@link #computeNextEntryIndex(int, + * ByteBuf, ByteBuf)}. Size of the first buffer (the "header buffer") drives which decoding method + * should be further applied to it. + * + *

The header buffer is either: + * + *

    + *
  • made up of a single byte: this represents an encoded mime id, which can be further + * decoded using {@link #decodeMimeIdFromMimeBuffer(ByteBuf)} + *
  • made up of 2 or more bytes: this represents an encoded mime String + its length, which + * can be further decoded using {@link #decodeMimeTypeFromMimeBuffer(ByteBuf)}. Note the + * encoded length, in the first byte, is skipped by this decoding method because the + * remaining length of the buffer is that of the mime string. + *
+ * + * @param compositeMetadata the source {@link ByteBuf} that originally contains one or more + * metadata entries + * @param entryIndex the {@link ByteBuf#readerIndex()} to start decoding from. original reader + * index is kept on the source buffer + * @param retainSlices should produced metadata entry buffers {@link ByteBuf#slice() slices} be + * {@link ByteBuf#retainedSlice() retained}? + * @return a {@link ByteBuf} array of length 2 containing the mime header buffer + * slice and the content buffer slice, or one of the + * zero-length error constant arrays + */ + public static ByteBuf[] decodeMimeAndContentBuffersSlices( + ByteBuf compositeMetadata, int entryIndex, boolean retainSlices) { + return CompositeMetadataCodec.decodeMimeAndContentBuffersSlices( + compositeMetadata, entryIndex, retainSlices); + } + + /** + * Decode a {@code byte} compressed mime id from a {@link ByteBuf}, assuming said buffer properly + * contains such an id. + * + *

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

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

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

If the mime header buffer is less than 2 bytes long, returns {@code null}. + * + * @param flyweightMimeBuffer the mime header {@link ByteBuf} that contains length + custom mime + * type + * @return the decoded custom mime type, as a {@link CharSequence}, or null if the input is + * invalid + * @see #decodeMimeIdFromMimeBuffer(ByteBuf) + */ + @Nullable + public static CharSequence decodeMimeTypeFromMimeBuffer(ByteBuf flyweightMimeBuffer) { + return CompositeMetadataCodec.decodeMimeTypeFromMimeBuffer(flyweightMimeBuffer); + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}, without checking if the {@link String} can be matched with a well known compressable + * mime type. Prefer using this method and {@link #encodeAndAddMetadata(CompositeByteBuf, + * ByteBufAllocator, WellKnownMimeType, ByteBuf)} if you know in advance whether or not the mime + * is well known. Otherwise use {@link #encodeAndAddMetadataWithCompression(CompositeByteBuf, + * ByteBufAllocator, String, ByteBuf)} + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param customMimeType the custom mime type to encode. + * @param metadata the metadata value to encode. + */ + // see #encodeMetadataHeader(ByteBufAllocator, String, int) + public static void encodeAndAddMetadata( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + String customMimeType, + ByteBuf metadata) { + CompositeMetadataCodec.encodeAndAddMetadata( + compositeMetaData, allocator, customMimeType, metadata); + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}. + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param knownMimeType the {@link WellKnownMimeType} to encode. + * @param metadata the metadata value to encode. + */ + // see #encodeMetadataHeader(ByteBufAllocator, byte, int) + public static void encodeAndAddMetadata( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + WellKnownMimeType knownMimeType, + ByteBuf metadata) { + CompositeMetadataCodec.encodeAndAddMetadata( + compositeMetaData, allocator, knownMimeType, metadata); + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}, first verifying if the passed {@link String} matches a {@link WellKnownMimeType} (in + * which case it will be encoded in a compressed fashion using the mime id of that type). + * + *

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

This larger representation encodes the mime type representation's length on a single byte, + * then the representation itself, then the unsigned metadata value length on 3 additional bytes. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer. + * @param customMime a custom mime type to encode. + * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits + * integer. + * @return the encoded mime and metadata length information + */ + static ByteBuf encodeMetadataHeader( + ByteBufAllocator allocator, String customMime, int metadataLength) { + return CompositeMetadataCodec.encodeMetadataHeader(allocator, customMime, metadataLength); + } + + /** + * Encode a {@link WellKnownMimeType well known mime type} and a metadata value length into a + * newly allocated {@link ByteBuf}. + * + *

This compact representation encodes the mime type via its ID on a single byte, and the + * unsigned value length on 3 additional bytes. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer. + * @param mimeType a byte identifier of a {@link WellKnownMimeType} to encode. + * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits + * integer. + * @return the encoded mime and metadata length information + */ + static ByteBuf encodeMetadataHeader( + ByteBufAllocator allocator, byte mimeType, int metadataLength) { + return CompositeMetadataCodec.encodeMetadataHeader(allocator, mimeType, metadataLength); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/RoutingMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/RoutingMetadata.java new file mode 100644 index 000000000..d1f2643dc --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/RoutingMetadata.java @@ -0,0 +1,18 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; + +/** + * Routing Metadata extension from + * https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md + * + * @author linux_china + */ +public class RoutingMetadata extends TaggingMetadata { + private static final WellKnownMimeType ROUTING_MIME_TYPE = + WellKnownMimeType.MESSAGE_RSOCKET_ROUTING; + + public RoutingMetadata(ByteBuf content) { + super(ROUTING_MIME_TYPE.getString(), content); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadata.java new file mode 100644 index 000000000..e22d97106 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadata.java @@ -0,0 +1,64 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Tagging metadata from https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md + * + * @author linux_china + */ +public class TaggingMetadata implements Iterable, CompositeMetadata.Entry { + /** Tag max length in bytes */ + private static int TAG_LENGTH_MAX = 0xFF; + + private String mimeType; + private ByteBuf content; + + public TaggingMetadata(String mimeType, ByteBuf content) { + this.mimeType = mimeType; + this.content = content; + } + + public Stream stream() { + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + iterator(), Spliterator.DISTINCT | Spliterator.NONNULL | Spliterator.ORDERED), + false); + } + + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return content.readerIndex() < content.capacity(); + } + + @Override + public String next() { + int tagLength = TAG_LENGTH_MAX & content.readByte(); + if (tagLength > 0) { + return content.readSlice(tagLength).toString(StandardCharsets.UTF_8); + } else { + return ""; + } + } + }; + } + + @Override + public ByteBuf getContent() { + return this.content; + } + + @Override + public String getMimeType() { + return this.mimeType; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataCodec.java b/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataCodec.java new file mode 100644 index 000000000..d766cf59f --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataCodec.java @@ -0,0 +1,76 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import java.nio.charset.StandardCharsets; +import java.util.Collection; + +/** + * A flyweight class that can be used to encode/decode tagging metadata information to/from {@link + * ByteBuf}. This is intended for low-level efficient manipulation of such buffers. See {@link + * TaggingMetadata} for an Iterator-like approach to decoding entries. + * + * @author linux_china + */ +public class TaggingMetadataCodec { + /** Tag max length in bytes */ + private static int TAG_LENGTH_MAX = 0xFF; + + /** + * create routing metadata + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param tags tag values + * @return routing metadata + */ + public static RoutingMetadata createRoutingMetadata( + ByteBufAllocator allocator, Collection tags) { + return new RoutingMetadata(createTaggingContent(allocator, tags)); + } + + /** + * create tagging metadata from composite metadata entry + * + * @param entry composite metadata entry + * @return tagging metadata + */ + public static TaggingMetadata createTaggingMetadata(CompositeMetadata.Entry entry) { + return new TaggingMetadata(entry.getMimeType(), entry.getContent()); + } + + /** + * create tagging metadata + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param knownMimeType the {@link WellKnownMimeType} to encode. + * @param tags tag values + * @return Tagging Metadata + */ + public static TaggingMetadata createTaggingMetadata( + ByteBufAllocator allocator, String knownMimeType, Collection tags) { + return new TaggingMetadata(knownMimeType, createTaggingContent(allocator, tags)); + } + + /** + * create tagging content + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param tags tag values + * @return tagging content + */ + public static ByteBuf createTaggingContent(ByteBufAllocator allocator, Collection tags) { + CompositeByteBuf taggingContent = allocator.compositeBuffer(); + for (String key : tags) { + int length = ByteBufUtil.utf8Bytes(key); + if (length == 0 || length > TAG_LENGTH_MAX) { + continue; + } + ByteBuf byteBuf = allocator.buffer().writeByte(length); + byteBuf.writeCharSequence(key, StandardCharsets.UTF_8); + taggingContent.addComponent(true, byteBuf); + } + return taggingContent; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataFlyweight.java new file mode 100644 index 000000000..718528358 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataFlyweight.java @@ -0,0 +1,62 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import java.util.Collection; + +/** + * A flyweight class that can be used to encode/decode tagging metadata information to/from {@link + * ByteBuf}. This is intended for low-level efficient manipulation of such buffers. See {@link + * TaggingMetadata} for an Iterator-like approach to decoding entries. + * + * @deprecated in favor of {@link TaggingMetadataCodec} + * @author linux_china + */ +@Deprecated +public class TaggingMetadataFlyweight { + /** + * create routing metadata + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param tags tag values + * @return routing metadata + */ + public static RoutingMetadata createRoutingMetadata( + ByteBufAllocator allocator, Collection tags) { + return TaggingMetadataCodec.createRoutingMetadata(allocator, tags); + } + + /** + * create tagging metadata from composite metadata entry + * + * @param entry composite metadata entry + * @return tagging metadata + */ + public static TaggingMetadata createTaggingMetadata(CompositeMetadata.Entry entry) { + return TaggingMetadataCodec.createTaggingMetadata(entry); + } + + /** + * create tagging metadata + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param knownMimeType the {@link WellKnownMimeType} to encode. + * @param tags tag values + * @return Tagging Metadata + */ + public static TaggingMetadata createTaggingMetadata( + ByteBufAllocator allocator, String knownMimeType, Collection tags) { + return TaggingMetadataCodec.createTaggingMetadata(allocator, knownMimeType, tags); + } + + /** + * create tagging content + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param tags tag values + * @return tagging content + */ + public static ByteBuf createTaggingContent(ByteBufAllocator allocator, Collection tags) { + return TaggingMetadataCodec.createTaggingContent(allocator, tags); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/TracingMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/TracingMetadata.java new file mode 100644 index 000000000..d276a9436 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/TracingMetadata.java @@ -0,0 +1,110 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.metadata; + +/** + * Represents decoded tracing metadata which is fully compatible with Zipkin B3 propagation + * + * @since 1.0 + */ +public final class TracingMetadata { + + final long traceIdHigh; + final long traceId; + private final boolean hasParentId; + final long parentId; + final long spanId; + final boolean isEmpty; + final boolean isNotSampled; + final boolean isSampled; + final boolean isDebug; + + TracingMetadata( + long traceIdHigh, + long traceId, + long spanId, + boolean hasParentId, + long parentId, + boolean isEmpty, + boolean isNotSampled, + boolean isSampled, + boolean isDebug) { + this.traceIdHigh = traceIdHigh; + this.traceId = traceId; + this.spanId = spanId; + this.hasParentId = hasParentId; + this.parentId = parentId; + this.isEmpty = isEmpty; + this.isNotSampled = isNotSampled; + this.isSampled = isSampled; + this.isDebug = isDebug; + } + + /** When non-zero, the trace containing this span uses 128-bit trace identifiers. */ + public long traceIdHigh() { + return traceIdHigh; + } + + /** Unique 8-byte identifier for a trace, set on all spans within it. */ + public long traceId() { + return traceId; + } + + /** Indicates if the parent's {@link #spanId} or if this the root span in a trace. */ + public final boolean hasParent() { + return hasParentId; + } + + /** Returns the parent's {@link #spanId} where zero implies absent. */ + public long parentId() { + return parentId; + } + + /** + * Unique 8-byte identifier of this span within a trace. + * + *

A span is uniquely identified in storage by ({@linkplain #traceId}, {@linkplain #spanId}). + */ + public long spanId() { + return spanId; + } + + /** Indicates that trace IDs should be accepted for tracing. */ + public boolean isSampled() { + return isSampled; + } + + /** Indicates that trace IDs should be force traced. */ + public boolean isDebug() { + return isDebug; + } + + /** Includes that there is sampling information and no trace IDs. */ + public boolean isEmpty() { + return isEmpty; + } + + /** + * Indicated that sampling decision is present. If {@code false} means that decision is unknown + * and says explicitly that {@link #isDebug()} and {@link #isSampled()} also returns {@code + * false}. + */ + public boolean isDecided() { + return isNotSampled || isDebug || isSampled; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/TracingMetadataCodec.java b/rsocket-core/src/main/java/io/rsocket/metadata/TracingMetadataCodec.java new file mode 100644 index 000000000..eb44956f6 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/TracingMetadataCodec.java @@ -0,0 +1,172 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; + +/** + * Represents codes for tracing metadata which is fully compatible with Zipkin B3 propagation + * + * @since 1.0 + */ +public class TracingMetadataCodec { + + static final int FLAG_EXTENDED_TRACE_ID_SIZE = 0b0000_1000; + static final int FLAG_INCLUDE_PARENT_ID = 0b0000_0100; + static final int FLAG_NOT_SAMPLED = 0b0001_0000; + static final int FLAG_SAMPLED = 0b0010_0000; + static final int FLAG_DEBUG = 0b0100_0000; + static final int FLAG_IDS_SET = 0b1000_0000; + + public static ByteBuf encodeEmpty(ByteBufAllocator allocator, Flags flag) { + + return encode(allocator, true, 0, 0, false, 0, 0, false, flag); + } + + public static ByteBuf encode128( + ByteBufAllocator allocator, + long traceIdHigh, + long traceId, + long spanId, + long parentId, + Flags flag) { + + return encode(allocator, false, traceIdHigh, traceId, true, spanId, parentId, true, flag); + } + + public static ByteBuf encode128( + ByteBufAllocator allocator, long traceIdHigh, long traceId, long spanId, Flags flag) { + + return encode(allocator, false, traceIdHigh, traceId, true, spanId, 0, false, flag); + } + + public static ByteBuf encode64( + ByteBufAllocator allocator, long traceId, long spanId, long parentId, Flags flag) { + + return encode(allocator, false, 0, traceId, false, spanId, parentId, true, flag); + } + + public static ByteBuf encode64( + ByteBufAllocator allocator, long traceId, long spanId, Flags flag) { + return encode(allocator, false, 0, traceId, false, spanId, 0, false, flag); + } + + static ByteBuf encode( + ByteBufAllocator allocator, + boolean isEmpty, + long traceIdHigh, + long traceId, + boolean extendedTraceId, + long spanId, + long parentId, + boolean includesParent, + Flags flag) { + int size = + 1 + + (isEmpty + ? 0 + : (Long.BYTES + + Long.BYTES + + (extendedTraceId ? Long.BYTES : 0) + + (includesParent ? Long.BYTES : 0))); + final ByteBuf buffer = allocator.buffer(size); + + int byteFlags = 0; + switch (flag) { + case NOT_SAMPLE: + byteFlags |= FLAG_NOT_SAMPLED; + break; + case SAMPLE: + byteFlags |= FLAG_SAMPLED; + break; + case DEBUG: + byteFlags |= FLAG_DEBUG; + break; + } + + if (isEmpty) { + return buffer.writeByte(byteFlags); + } + + byteFlags |= FLAG_IDS_SET; + + if (extendedTraceId) { + byteFlags |= FLAG_EXTENDED_TRACE_ID_SIZE; + } + + if (includesParent) { + byteFlags |= FLAG_INCLUDE_PARENT_ID; + } + + buffer.writeByte(byteFlags); + + if (extendedTraceId) { + buffer.writeLong(traceIdHigh); + } + + buffer.writeLong(traceId).writeLong(spanId); + + if (includesParent) { + buffer.writeLong(parentId); + } + + return buffer; + } + + public static TracingMetadata decode(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + try { + byte flags = byteBuf.readByte(); + boolean isNotSampled = (flags & FLAG_NOT_SAMPLED) == FLAG_NOT_SAMPLED; + boolean isSampled = (flags & FLAG_SAMPLED) == FLAG_SAMPLED; + boolean isDebug = (flags & FLAG_DEBUG) == FLAG_DEBUG; + boolean isIDSet = (flags & FLAG_IDS_SET) == FLAG_IDS_SET; + + if (!isIDSet) { + return new TracingMetadata(0, 0, 0, false, 0, true, isNotSampled, isSampled, isDebug); + } + + boolean extendedTraceId = + (flags & FLAG_EXTENDED_TRACE_ID_SIZE) == FLAG_EXTENDED_TRACE_ID_SIZE; + + long traceIdHigh; + if (extendedTraceId) { + traceIdHigh = byteBuf.readLong(); + } else { + traceIdHigh = 0; + } + + long traceId = byteBuf.readLong(); + long spanId = byteBuf.readLong(); + + boolean includesParent = (flags & FLAG_INCLUDE_PARENT_ID) == FLAG_INCLUDE_PARENT_ID; + + long parentId; + if (includesParent) { + parentId = byteBuf.readLong(); + } else { + parentId = 0; + } + + return new TracingMetadata( + traceIdHigh, + traceId, + spanId, + includesParent, + parentId, + false, + isNotSampled, + isSampled, + isDebug); + } finally { + byteBuf.resetReaderIndex(); + } + } + + public enum Flags { + UNDECIDED, + NOT_SAMPLE, + SAMPLE, + DEBUG + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownAuthType.java b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownAuthType.java new file mode 100644 index 000000000..66c98701c --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownAuthType.java @@ -0,0 +1,121 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.metadata; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Enumeration of Well Known Auth Types, as defined in the eponymous extension. Such auth types are + * used in composite metadata (which can include routing and/or tracing metadata). Per + * specification, identifiers are between 0 and 127 (inclusive). + */ +public enum WellKnownAuthType { + UNPARSEABLE_AUTH_TYPE("UNPARSEABLE_AUTH_TYPE_DO_NOT_USE", (byte) -2), + UNKNOWN_RESERVED_AUTH_TYPE("UNKNOWN_YET_RESERVED_DO_NOT_USE", (byte) -1), + + SIMPLE("simple", (byte) 0x00), + BEARER("bearer", (byte) 0x01); + // ... reserved for future use ... + + static final WellKnownAuthType[] TYPES_BY_AUTH_ID; + static final Map TYPES_BY_AUTH_STRING; + + static { + // precompute an array of all valid auth ids, filling the blanks with the RESERVED enum + TYPES_BY_AUTH_ID = new WellKnownAuthType[128]; // 0-127 inclusive + Arrays.fill(TYPES_BY_AUTH_ID, UNKNOWN_RESERVED_AUTH_TYPE); + // also prepare a Map of the types by auth string + TYPES_BY_AUTH_STRING = new LinkedHashMap<>(128); + + for (WellKnownAuthType value : values()) { + if (value.getIdentifier() >= 0) { + TYPES_BY_AUTH_ID[value.getIdentifier()] = value; + TYPES_BY_AUTH_STRING.put(value.getString(), value); + } + } + } + + private final byte identifier; + private final String str; + + WellKnownAuthType(String str, byte identifier) { + this.str = str; + this.identifier = identifier; + } + + /** + * Find the {@link WellKnownAuthType} for the given identifier (as an {@code int}). Valid + * identifiers are defined to be integers between 0 and 127, inclusive. Identifiers outside of + * this range will produce the {@link #UNPARSEABLE_AUTH_TYPE}. Additionally, some identifiers in + * that range are still only reserved and don't have a type associated yet: this method returns + * the {@link #UNKNOWN_RESERVED_AUTH_TYPE} when passing such an identifier, which lets call sites + * potentially detect this and keep the original representation when transmitting the associated + * metadata buffer. + * + * @param id the looked up identifier + * @return the {@link WellKnownAuthType}, or {@link #UNKNOWN_RESERVED_AUTH_TYPE} if the id is out + * of the specification's range, or {@link #UNKNOWN_RESERVED_AUTH_TYPE} if the id is one that + * is merely reserved but unknown to this implementation. + */ + public static WellKnownAuthType fromIdentifier(int id) { + if (id < 0x00 || id > 0x7F) { + return UNPARSEABLE_AUTH_TYPE; + } + return TYPES_BY_AUTH_ID[id]; + } + + /** + * Find the {@link WellKnownAuthType} for the given {@link String} representation. If the + * representation is {@code null} or doesn't match a {@link WellKnownAuthType}, the {@link + * #UNPARSEABLE_AUTH_TYPE} is returned. + * + * @param authType the looked up auth type + * @return the matching {@link WellKnownAuthType}, or {@link #UNPARSEABLE_AUTH_TYPE} if none + * matches + */ + public static WellKnownAuthType fromString(String authType) { + if (authType == null) throw new IllegalArgumentException("type must be non-null"); + + // force UNPARSEABLE if by chance UNKNOWN_RESERVED_AUTH_TYPE's text has been used + if (authType.equals(UNKNOWN_RESERVED_AUTH_TYPE.str)) { + return UNPARSEABLE_AUTH_TYPE; + } + + return TYPES_BY_AUTH_STRING.getOrDefault(authType, UNPARSEABLE_AUTH_TYPE); + } + + /** @return the byte identifier of the auth type, guaranteed to be positive or zero. */ + public byte getIdentifier() { + return identifier; + } + + /** + * @return the auth type represented as a {@link String}, which is made of US_ASCII compatible + * characters only + */ + public String getString() { + return str; + } + + /** @see #getString() */ + @Override + public String toString() { + return str; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java new file mode 100644 index 000000000..e78e87629 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java @@ -0,0 +1,167 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.metadata; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Enumeration of Well Known Mime Types, as defined in the eponymous extension. Such mime types are + * used in composite metadata (which can include routing and/or tracing metadata). Per + * specification, identifiers are between 0 and 127 (inclusive). + */ +public enum WellKnownMimeType { + UNPARSEABLE_MIME_TYPE("UNPARSEABLE_MIME_TYPE_DO_NOT_USE", (byte) -2), + UNKNOWN_RESERVED_MIME_TYPE("UNKNOWN_YET_RESERVED_DO_NOT_USE", (byte) -1), + + APPLICATION_AVRO("application/avro", (byte) 0x00), + APPLICATION_CBOR("application/cbor", (byte) 0x01), + APPLICATION_GRAPHQL("application/graphql", (byte) 0x02), + APPLICATION_GZIP("application/gzip", (byte) 0x03), + APPLICATION_JAVASCRIPT("application/javascript", (byte) 0x04), + APPLICATION_JSON("application/json", (byte) 0x05), + APPLICATION_OCTET_STREAM("application/octet-stream", (byte) 0x06), + APPLICATION_PDF("application/pdf", (byte) 0x07), + APPLICATION_THRIFT("application/vnd.apache.thrift.binary", (byte) 0x08), + APPLICATION_PROTOBUF("application/vnd.google.protobuf", (byte) 0x09), + APPLICATION_XML("application/xml", (byte) 0x0A), + APPLICATION_ZIP("application/zip", (byte) 0x0B), + AUDIO_AAC("audio/aac", (byte) 0x0C), + AUDIO_MP3("audio/mp3", (byte) 0x0D), + AUDIO_MP4("audio/mp4", (byte) 0x0E), + AUDIO_MPEG3("audio/mpeg3", (byte) 0x0F), + AUDIO_MPEG("audio/mpeg", (byte) 0x10), + AUDIO_OGG("audio/ogg", (byte) 0x11), + AUDIO_OPUS("audio/opus", (byte) 0x12), + AUDIO_VORBIS("audio/vorbis", (byte) 0x13), + IMAGE_BMP("image/bmp", (byte) 0x14), + IMAGE_GIF("image/gif", (byte) 0x15), + IMAGE_HEIC_SEQUENCE("image/heic-sequence", (byte) 0x16), + IMAGE_HEIC("image/heic", (byte) 0x17), + IMAGE_HEIF_SEQUENCE("image/heif-sequence", (byte) 0x18), + IMAGE_HEIF("image/heif", (byte) 0x19), + IMAGE_JPEG("image/jpeg", (byte) 0x1A), + IMAGE_PNG("image/png", (byte) 0x1B), + IMAGE_TIFF("image/tiff", (byte) 0x1C), + MULTIPART_MIXED("multipart/mixed", (byte) 0x1D), + TEXT_CSS("text/css", (byte) 0x1E), + TEXT_CSV("text/csv", (byte) 0x1F), + TEXT_HTML("text/html", (byte) 0x20), + TEXT_PLAIN("text/plain", (byte) 0x21), + TEXT_XML("text/xml", (byte) 0x22), + VIDEO_H264("video/H264", (byte) 0x23), + VIDEO_H265("video/H265", (byte) 0x24), + VIDEO_VP8("video/VP8", (byte) 0x25), + APPLICATION_HESSIAN("application/x-hessian", (byte) 0x26), + APPLICATION_JAVA_OBJECT("application/x-java-object", (byte) 0x27), + APPLICATION_CLOUDEVENTS_JSON("application/cloudevents+json", (byte) 0x28), + + // ... reserved for future use ... + MESSAGE_RSOCKET_MIMETYPE("message/x.rsocket.mime-type.v0", (byte) 0x7A), + MESSAGE_RSOCKET_ACCEPT_MIMETYPES("message/x.rsocket.accept-mime-types.v0", (byte) 0x7B), + MESSAGE_RSOCKET_AUTHENTICATION("message/x.rsocket.authentication.v0", (byte) 0x7C), + MESSAGE_RSOCKET_TRACING_ZIPKIN("message/x.rsocket.tracing-zipkin.v0", (byte) 0x7D), + MESSAGE_RSOCKET_ROUTING("message/x.rsocket.routing.v0", (byte) 0x7E), + MESSAGE_RSOCKET_COMPOSITE_METADATA("message/x.rsocket.composite-metadata.v0", (byte) 0x7F); + + static final WellKnownMimeType[] TYPES_BY_MIME_ID; + static final Map TYPES_BY_MIME_STRING; + + static { + // precompute an array of all valid mime ids, filling the blanks with the RESERVED enum + TYPES_BY_MIME_ID = new WellKnownMimeType[128]; // 0-127 inclusive + Arrays.fill(TYPES_BY_MIME_ID, UNKNOWN_RESERVED_MIME_TYPE); + // also prepare a Map of the types by mime string + TYPES_BY_MIME_STRING = new HashMap<>(128); + + for (WellKnownMimeType value : values()) { + if (value.getIdentifier() >= 0) { + TYPES_BY_MIME_ID[value.getIdentifier()] = value; + TYPES_BY_MIME_STRING.put(value.getString(), value); + } + } + } + + private final byte identifier; + private final String str; + + WellKnownMimeType(String str, byte identifier) { + this.str = str; + this.identifier = identifier; + } + + /** + * Find the {@link WellKnownMimeType} for the given identifier (as an {@code int}). Valid + * identifiers are defined to be integers between 0 and 127, inclusive. Identifiers outside of + * this range will produce the {@link #UNPARSEABLE_MIME_TYPE}. Additionally, some identifiers in + * that range are still only reserved and don't have a type associated yet: this method returns + * the {@link #UNKNOWN_RESERVED_MIME_TYPE} when passing such an identifier, which lets call sites + * potentially detect this and keep the original representation when transmitting the associated + * metadata buffer. + * + * @param id the looked up identifier + * @return the {@link WellKnownMimeType}, or {@link #UNKNOWN_RESERVED_MIME_TYPE} if the id is out + * of the specification's range, or {@link #UNKNOWN_RESERVED_MIME_TYPE} if the id is one that + * is merely reserved but unknown to this implementation. + */ + public static WellKnownMimeType fromIdentifier(int id) { + if (id < 0x00 || id > 0x7F) { + return UNPARSEABLE_MIME_TYPE; + } + return TYPES_BY_MIME_ID[id]; + } + + /** + * Find the {@link WellKnownMimeType} for the given {@link String} representation. If the + * representation is {@code null} or doesn't match a {@link WellKnownMimeType}, the {@link + * #UNPARSEABLE_MIME_TYPE} is returned. + * + * @param mimeType the looked up mime type + * @return the matching {@link WellKnownMimeType}, or {@link #UNPARSEABLE_MIME_TYPE} if none + * matches + */ + public static WellKnownMimeType fromString(String mimeType) { + if (mimeType == null) throw new IllegalArgumentException("type must be non-null"); + + // force UNPARSEABLE if by chance UNKNOWN_RESERVED_MIME_TYPE's text has been used + if (mimeType.equals(UNKNOWN_RESERVED_MIME_TYPE.str)) { + return UNPARSEABLE_MIME_TYPE; + } + + return TYPES_BY_MIME_STRING.getOrDefault(mimeType, UNPARSEABLE_MIME_TYPE); + } + + /** @return the byte identifier of the mime type, guaranteed to be positive or zero. */ + public byte getIdentifier() { + return identifier; + } + + /** + * @return the mime type represented as a {@link String}, which is made of US_ASCII compatible + * characters only + */ + public String getString() { + return str; + } + + /** @see #getString() */ + @Override + public String toString() { + return str; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/package-info.java b/rsocket-core/src/main/java/io/rsocket/metadata/package-info.java new file mode 100644 index 000000000..3fb9ae1d6 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Contains implementations of RSocket protocol extensions related + * to the use of metadata. + */ +@NonNullApi +package io.rsocket.metadata; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java new file mode 100644 index 000000000..e1a8ba449 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java @@ -0,0 +1,194 @@ +package io.rsocket.metadata.security; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.metadata.AuthMetadataCodec; + +/** @deprecated in favor of {@link io.rsocket.metadata.AuthMetadataCodec} */ +@Deprecated +public class AuthMetadataFlyweight { + + static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 + + private AuthMetadataFlyweight() {} + + /** + * Encode a Authentication CompositeMetadata payload using custom authentication type + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param customAuthType the custom mime type to encode. + * @param metadata the metadata value to encode. + * @throws IllegalArgumentException in case of {@code customAuthType} is non US_ASCII string or + * empty string or its length is greater than 128 bytes + */ + public static ByteBuf encodeMetadata( + ByteBufAllocator allocator, String customAuthType, ByteBuf metadata) { + + return AuthMetadataCodec.encodeMetadata(allocator, customAuthType, metadata); + } + + /** + * Encode a Authentication CompositeMetadata payload using custom authentication type + * + * @param allocator the {@link ByteBufAllocator} to create intermediate buffers as needed. + * @param authType the well-known mime type to encode. + * @param metadata the metadata value to encode. + * @throws IllegalArgumentException in case of {@code authType} is {@link + * WellKnownAuthType#UNPARSEABLE_AUTH_TYPE} or {@link + * WellKnownAuthType#UNKNOWN_RESERVED_AUTH_TYPE} + */ + public static ByteBuf encodeMetadata( + ByteBufAllocator allocator, WellKnownAuthType authType, ByteBuf metadata) { + + return AuthMetadataCodec.encodeMetadata(allocator, WellKnownAuthType.cast(authType), metadata); + } + + /** + * Encode a Authentication CompositeMetadata payload using Simple Authentication format + * + * @throws IllegalArgumentException if the username length is greater than 255 + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param username the char sequence which represents user name. + * @param password the char sequence which represents user password. + */ + public static ByteBuf encodeSimpleMetadata( + ByteBufAllocator allocator, char[] username, char[] password) { + return AuthMetadataCodec.encodeSimpleMetadata(allocator, username, password); + } + + /** + * Encode a Authentication CompositeMetadata payload using Bearer Authentication format + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param token the char sequence which represents BEARER token. + */ + public static ByteBuf encodeBearerMetadata(ByteBufAllocator allocator, char[] token) { + return AuthMetadataCodec.encodeBearerMetadata(allocator, token); + } + + /** + * Encode a new Authentication Metadata payload information, first verifying if the passed {@link + * String} matches a {@link WellKnownAuthType} (in which case it will be encoded in a compressed + * fashion using the mime id of that type). + * + *

Prefer using {@link #encodeMetadata(ByteBufAllocator, String, ByteBuf)} if you already know + * that the mime type is not a {@link WellKnownAuthType}. + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param authType the mime type to encode, as a {@link String}. well known mime types are + * compressed. + * @param metadata the metadata value to encode. + * @see #encodeMetadata(ByteBufAllocator, WellKnownAuthType, ByteBuf) + * @see #encodeMetadata(ByteBufAllocator, String, ByteBuf) + */ + public static ByteBuf encodeMetadataWithCompression( + ByteBufAllocator allocator, String authType, ByteBuf metadata) { + return AuthMetadataCodec.encodeMetadataWithCompression(allocator, authType, metadata); + } + + /** + * Get the first {@code byte} from a {@link ByteBuf} and check whether it is length or {@link + * WellKnownAuthType}. Assuming said buffer properly contains such a {@code byte} + * + * @param metadata byteBuf used to get information from + */ + public static boolean isWellKnownAuthType(ByteBuf metadata) { + return AuthMetadataCodec.isWellKnownAuthType(metadata); + } + + /** + * Read first byte from the given {@code metadata} and tries to convert it's value to {@link + * WellKnownAuthType}. + * + * @param metadata given metadata buffer to read from + * @return Return on of the know Auth types or {@link WellKnownAuthType#UNPARSEABLE_AUTH_TYPE} if + * field's value is length or unknown auth type + * @throws IllegalStateException if not enough readable bytes in the given {@link ByteBuf} + */ + public static WellKnownAuthType decodeWellKnownAuthType(ByteBuf metadata) { + return WellKnownAuthType.cast(AuthMetadataCodec.readWellKnownAuthType(metadata)); + } + + /** + * Read up to 129 bytes from the given metadata in order to get the custom Auth Type + * + * @param metadata + * @return + */ + public static CharSequence decodeCustomAuthType(ByteBuf metadata) { + return AuthMetadataCodec.readCustomAuthType(metadata); + } + + /** + * Read all remaining {@code bytes} from the given {@link ByteBuf} and return sliced + * representation of a payload + * + * @param metadata metadata to get payload from. Please note, the {@code metadata#readIndex} + * should be set to the beginning of the payload bytes + * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if no bytes readable in the + * given one + */ + public static ByteBuf decodePayload(ByteBuf metadata) { + return AuthMetadataCodec.readPayload(metadata); + } + + /** + * Read up to 257 {@code bytes} from the given {@link ByteBuf} where the first byte is username + * length and the subsequent number of bytes equal to decoded length + * + * @param simpleAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the username length byte + * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if username length is zero + */ + public static ByteBuf decodeUsername(ByteBuf simpleAuthMetadata) { + return AuthMetadataCodec.readUsername(simpleAuthMetadata); + } + + /** + * Read all the remaining {@code byte}s from the given {@link ByteBuf} which represents user's + * password + * + * @param simpleAuthMetadata the given metadata to read password from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes + * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if password length is zero + */ + public static ByteBuf decodePassword(ByteBuf simpleAuthMetadata) { + return AuthMetadataCodec.readPassword(simpleAuthMetadata); + } + /** + * Read up to 257 {@code bytes} from the given {@link ByteBuf} where the first byte is username + * length and the subsequent number of bytes equal to decoded length + * + * @param simpleAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the username length byte + * @return {@code char[]} which represents UTF-8 username + */ + public static char[] decodeUsernameAsCharArray(ByteBuf simpleAuthMetadata) { + return AuthMetadataCodec.readUsernameAsCharArray(simpleAuthMetadata); + } + + /** + * Read all the remaining {@code byte}s from the given {@link ByteBuf} which represents user's + * password + * + * @param simpleAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes + * @return {@code char[]} which represents UTF-8 password + */ + public static char[] decodePasswordAsCharArray(ByteBuf simpleAuthMetadata) { + return AuthMetadataCodec.readPasswordAsCharArray(simpleAuthMetadata); + } + + /** + * Read all the remaining {@code bytes} from the given {@link ByteBuf} where the first byte is + * username length and the subsequent number of bytes equal to decoded length + * + * @param bearerAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes + * @return {@code char[]} which represents UTF-8 password + */ + public static char[] decodeBearerTokenAsCharArray(ByteBuf bearerAuthMetadata) { + return AuthMetadataCodec.readBearerTokenAsCharArray(bearerAuthMetadata); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/security/WellKnownAuthType.java b/rsocket-core/src/main/java/io/rsocket/metadata/security/WellKnownAuthType.java new file mode 100644 index 000000000..24e5ff0db --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/security/WellKnownAuthType.java @@ -0,0 +1,147 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.metadata.security; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Enumeration of Well Known Auth Types, as defined in the eponymous extension. Such auth types are + * used in composite metadata (which can include routing and/or tracing metadata). Per + * specification, identifiers are between 0 and 127 (inclusive). + * + * @deprecated in favor of {@link io.rsocket.metadata.WellKnownAuthType} + */ +@Deprecated +public enum WellKnownAuthType { + UNPARSEABLE_AUTH_TYPE("UNPARSEABLE_AUTH_TYPE_DO_NOT_USE", (byte) -2), + UNKNOWN_RESERVED_AUTH_TYPE("UNKNOWN_YET_RESERVED_DO_NOT_USE", (byte) -1), + + SIMPLE("simple", (byte) 0x00), + BEARER("bearer", (byte) 0x01); + // ... reserved for future use ... + + static final WellKnownAuthType[] TYPES_BY_AUTH_ID; + static final Map TYPES_BY_AUTH_STRING; + + static { + // precompute an array of all valid auth ids, filling the blanks with the RESERVED enum + TYPES_BY_AUTH_ID = new WellKnownAuthType[128]; // 0-127 inclusive + Arrays.fill(TYPES_BY_AUTH_ID, UNKNOWN_RESERVED_AUTH_TYPE); + // also prepare a Map of the types by auth string + TYPES_BY_AUTH_STRING = new LinkedHashMap<>(128); + + for (WellKnownAuthType value : values()) { + if (value.getIdentifier() >= 0) { + TYPES_BY_AUTH_ID[value.getIdentifier()] = value; + TYPES_BY_AUTH_STRING.put(value.getString(), value); + } + } + } + + private final byte identifier; + private final String str; + + WellKnownAuthType(String str, byte identifier) { + this.str = str; + this.identifier = identifier; + } + + static io.rsocket.metadata.WellKnownAuthType cast(WellKnownAuthType wellKnownAuthType) { + byte identifier = wellKnownAuthType.identifier; + if (identifier == io.rsocket.metadata.WellKnownAuthType.UNPARSEABLE_AUTH_TYPE.getIdentifier()) { + return io.rsocket.metadata.WellKnownAuthType.UNPARSEABLE_AUTH_TYPE; + } else if (identifier + == io.rsocket.metadata.WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE.getIdentifier()) { + return io.rsocket.metadata.WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE; + } else { + return io.rsocket.metadata.WellKnownAuthType.fromIdentifier(identifier); + } + } + + static WellKnownAuthType cast(io.rsocket.metadata.WellKnownAuthType wellKnownAuthType) { + byte identifier = wellKnownAuthType.getIdentifier(); + if (identifier == WellKnownAuthType.UNPARSEABLE_AUTH_TYPE.identifier) { + return WellKnownAuthType.UNPARSEABLE_AUTH_TYPE; + } else if (identifier == WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE.identifier) { + return WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE; + } else { + return TYPES_BY_AUTH_ID[identifier]; + } + } + + /** + * Find the {@link WellKnownAuthType} for the given identifier (as an {@code int}). Valid + * identifiers are defined to be integers between 0 and 127, inclusive. Identifiers outside of + * this range will produce the {@link #UNPARSEABLE_AUTH_TYPE}. Additionally, some identifiers in + * that range are still only reserved and don't have a type associated yet: this method returns + * the {@link #UNKNOWN_RESERVED_AUTH_TYPE} when passing such an identifier, which lets call sites + * potentially detect this and keep the original representation when transmitting the associated + * metadata buffer. + * + * @param id the looked up identifier + * @return the {@link WellKnownAuthType}, or {@link #UNKNOWN_RESERVED_AUTH_TYPE} if the id is out + * of the specification's range, or {@link #UNKNOWN_RESERVED_AUTH_TYPE} if the id is one that + * is merely reserved but unknown to this implementation. + */ + public static WellKnownAuthType fromIdentifier(int id) { + if (id < 0x00 || id > 0x7F) { + return UNPARSEABLE_AUTH_TYPE; + } + return TYPES_BY_AUTH_ID[id]; + } + + /** + * Find the {@link WellKnownAuthType} for the given {@link String} representation. If the + * representation is {@code null} or doesn't match a {@link WellKnownAuthType}, the {@link + * #UNPARSEABLE_AUTH_TYPE} is returned. + * + * @param authType the looked up auth type + * @return the matching {@link WellKnownAuthType}, or {@link #UNPARSEABLE_AUTH_TYPE} if none + * matches + */ + public static WellKnownAuthType fromString(String authType) { + if (authType == null) throw new IllegalArgumentException("type must be non-null"); + + // force UNPARSEABLE if by chance UNKNOWN_RESERVED_AUTH_TYPE's text has been used + if (authType.equals(UNKNOWN_RESERVED_AUTH_TYPE.str)) { + return UNPARSEABLE_AUTH_TYPE; + } + + return TYPES_BY_AUTH_STRING.getOrDefault(authType, UNPARSEABLE_AUTH_TYPE); + } + + /** @return the byte identifier of the auth type, guaranteed to be positive or zero. */ + public byte getIdentifier() { + return identifier; + } + + /** + * @return the auth type represented as a {@link String}, which is made of US_ASCII compatible + * characters only + */ + public String getString() { + return str; + } + + /** @see #getString() */ + @Override + public String toString() { + return str; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/package-info.java b/rsocket-core/src/main/java/io/rsocket/package-info.java index 3e23c5ff1..6fe74fb38 100644 --- a/rsocket-core/src/main/java/io/rsocket/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/package-info.java @@ -1,17 +1,29 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ +/** + * Contains key contracts of the RSocket programming model including {@link io.rsocket.RSocket + * RSocket} for performing or handling RSocket interactions, {@link io.rsocket.SocketAcceptor + * SocketAcceptor} for declaring responders, {@link io.rsocket.Payload Payload} for access to the + * content of a payload, and others. + * + *

To connect to or start a server see {@link io.rsocket.core.RSocketConnector RSocketConnector} + * and {@link io.rsocket.core.RSocketServer RSocketServer} in {@link io.rsocket.core}. + */ +@NonNullApi package io.rsocket; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java index 98a0d364c..6b2a7a71b 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java @@ -1,17 +1,17 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket.plugins; @@ -19,11 +19,15 @@ import io.rsocket.DuplexConnection; import java.util.function.BiFunction; -/** */ +/** + * Contract to decorate a {@link DuplexConnection} and intercept the sending and receiving of + * RSocket frames at the transport level. + */ public @FunctionalInterface interface DuplexConnectionInterceptor extends BiFunction { + enum Type { - STREAM_ZERO, + SETUP, CLIENT, SERVER, SOURCE diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java b/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java new file mode 100644 index 000000000..fc032847c --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java @@ -0,0 +1,56 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.plugins; + +import io.rsocket.DuplexConnection; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; + +/** + * Extends {@link InterceptorRegistry} with methods for building a chain of registered interceptors. + * This is not intended for direct use by applications. + */ +public class InitializingInterceptorRegistry extends InterceptorRegistry { + + public DuplexConnection initConnection( + DuplexConnectionInterceptor.Type type, DuplexConnection connection) { + for (DuplexConnectionInterceptor interceptor : getConnectionInterceptors()) { + connection = interceptor.apply(type, connection); + } + return connection; + } + + public RSocket initRequester(RSocket rsocket) { + for (RSocketInterceptor interceptor : getRequesterInteceptors()) { + rsocket = interceptor.apply(rsocket); + } + return rsocket; + } + + public RSocket initResponder(RSocket rsocket) { + for (RSocketInterceptor interceptor : getResponderInterceptors()) { + rsocket = interceptor.apply(rsocket); + } + return rsocket; + } + + public SocketAcceptor initSocketAcceptor(SocketAcceptor acceptor) { + for (SocketAcceptorInterceptor interceptor : getSocketAcceptorInterceptors()) { + acceptor = interceptor.apply(acceptor); + } + return acceptor; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java b/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java new file mode 100644 index 000000000..427fa15ae --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java @@ -0,0 +1,120 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.plugins; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * Provides support for registering interceptors at the following levels: + * + *

    + *
  • {@link #forConnection(DuplexConnectionInterceptor)} -- transport level + *
  • {@link #forSocketAcceptor(SocketAcceptorInterceptor)} -- for accepting new connections + *
  • {@link #forRequester(RSocketInterceptor)} -- for performing of requests + *
  • {@link #forResponder(RSocketInterceptor)} -- for responding to requests + *
+ */ +public class InterceptorRegistry { + private List requesterInteceptors = new ArrayList<>(); + private List responderInterceptors = new ArrayList<>(); + private List socketAcceptorInterceptors = new ArrayList<>(); + private List connectionInterceptors = new ArrayList<>(); + + /** + * Add an {@link RSocketInterceptor} that will decorate the RSocket used for performing requests. + */ + public InterceptorRegistry forRequester(RSocketInterceptor interceptor) { + requesterInteceptors.add(interceptor); + return this; + } + + /** + * Variant of {@link #forRequester(RSocketInterceptor)} with access to the list of existing + * registrations. + */ + public InterceptorRegistry forRequester(Consumer> consumer) { + consumer.accept(requesterInteceptors); + return this; + } + + /** + * Add an {@link RSocketInterceptor} that will decorate the RSocket used for resonding to + * requests. + */ + public InterceptorRegistry forResponder(RSocketInterceptor interceptor) { + responderInterceptors.add(interceptor); + return this; + } + + /** + * Variant of {@link #forResponder(RSocketInterceptor)} with access to the list of existing + * registrations. + */ + public InterceptorRegistry forResponder(Consumer> consumer) { + consumer.accept(responderInterceptors); + return this; + } + + /** + * Add a {@link SocketAcceptorInterceptor} that will intercept the accepting of new connections. + */ + public InterceptorRegistry forSocketAcceptor(SocketAcceptorInterceptor interceptor) { + socketAcceptorInterceptors.add(interceptor); + return this; + } + + /** + * Variant of {@link #forSocketAcceptor(SocketAcceptorInterceptor)} with access to the list of + * existing registrations. + */ + public InterceptorRegistry forSocketAcceptor(Consumer> consumer) { + consumer.accept(socketAcceptorInterceptors); + return this; + } + + /** Add a {@link DuplexConnectionInterceptor}. */ + public InterceptorRegistry forConnection(DuplexConnectionInterceptor interceptor) { + connectionInterceptors.add(interceptor); + return this; + } + + /** + * Variant of {@link #forConnection(DuplexConnectionInterceptor)} with access to the list of + * existing registrations. + */ + public InterceptorRegistry forConnection(Consumer> consumer) { + consumer.accept(connectionInterceptors); + return this; + } + + List getRequesterInteceptors() { + return requesterInteceptors; + } + + List getResponderInterceptors() { + return responderInterceptors; + } + + List getConnectionInterceptors() { + return connectionInterceptors; + } + + List getSocketAcceptorInterceptors() { + return socketAcceptorInterceptors; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/LimitRateInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/LimitRateInterceptor.java new file mode 100644 index 000000000..d7d9742d0 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/plugins/LimitRateInterceptor.java @@ -0,0 +1,133 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.plugins; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.util.RSocketProxy; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +/** + * Interceptor that adds {@link Flux#limitRate(int, int)} to publishers of outbound streams that + * breaks down or aggregates demand values from the remote end (i.e. {@code REQUEST_N} frames) into + * batches of a uniform size. For example the remote may request {@code Long.MAXVALUE} or it may + * start requesting one at a time, in both cases with the limit set to 64, the publisher will see a + * demand of 64 to start and subsequent batches of 48, i.e. continuing to prefetch and refill an + * internal queue when it falls to 75% full. The high and low tide marks are configurable. + * + *

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

Note: keep in mind that the {@code limitRate} operator always uses requests + * the same request values, even if the remote requests less than the limit. For example given a + * limit of 64, if the remote requests 4, 64 will be prefetched of which 4 will be sent and 60 will + * be cached. + * + * @since 1.0 + */ +public class LimitRateInterceptor implements RSocketInterceptor { + + private final int highTide; + private final int lowTide; + private final boolean requesterProxy; + + private LimitRateInterceptor(int highTide, int lowTide, boolean requesterProxy) { + this.highTide = highTide; + this.lowTide = lowTide; + this.requesterProxy = requesterProxy; + } + + @Override + public RSocket apply(RSocket socket) { + return requesterProxy ? new RequesterProxy(socket) : new ResponderProxy(socket); + } + + /** + * Create an interceptor for an {@code RSocket} that handles request-stream and/or request-channel + * interactions. + * + * @param prefetchRate the prefetch rate to pass to {@link Flux#limitRate(int)} + * @return the created interceptor + */ + public static LimitRateInterceptor forResponder(int prefetchRate) { + return forResponder(prefetchRate, prefetchRate); + } + + /** + * Create an interceptor for an {@code RSocket} that handles request-stream and/or request-channel + * interactions with more control over the overall prefetch rate and replenish threshold. + * + * @param highTide the high tide value to pass to {@link Flux#limitRate(int, int)} + * @param lowTide the low tide value to pass to {@link Flux#limitRate(int, int)} + * @return the created interceptor + */ + public static LimitRateInterceptor forResponder(int highTide, int lowTide) { + return new LimitRateInterceptor(highTide, lowTide, false); + } + + /** + * Create an interceptor for an {@code RSocket} that performs request-channel interactions. + * + * @param prefetchRate the prefetch rate to pass to {@link Flux#limitRate(int)} + * @return the created interceptor + */ + public static LimitRateInterceptor forRequester(int prefetchRate) { + return forRequester(prefetchRate, prefetchRate); + } + + /** + * Create an interceptor for an {@code RSocket} that performs request-channel interactions with + * more control over the overall prefetch rate and replenish threshold. + * + * @param highTide the high tide value to pass to {@link Flux#limitRate(int, int)} + * @param lowTide the low tide value to pass to {@link Flux#limitRate(int, int)} + * @return the created interceptor + */ + public static LimitRateInterceptor forRequester(int highTide, int lowTide) { + return new LimitRateInterceptor(highTide, lowTide, true); + } + + /** Responder side proxy, limits response streams. */ + private class ResponderProxy extends RSocketProxy { + + ResponderProxy(RSocket source) { + super(source); + } + + @Override + public Flux requestStream(Payload payload) { + return super.requestStream(payload).limitRate(highTide, lowTide); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return super.requestChannel(payloads).limitRate(highTide, lowTide); + } + } + + /** Requester side proxy, limits channel request stream. */ + private class RequesterProxy extends RSocketProxy { + + RequesterProxy(RSocket source) { + super(source); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return super.requestChannel(Flux.from(payloads).limitRate(highTide, lowTide)); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java b/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java deleted file mode 100644 index 2f00cf95f..000000000 --- a/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.plugins; - -import io.rsocket.DuplexConnection; -import io.rsocket.RSocket; -import java.util.ArrayList; -import java.util.List; - -public class PluginRegistry { - private List connections = new ArrayList<>(); - private List clients = new ArrayList<>(); - private List servers = new ArrayList<>(); - - public PluginRegistry() {} - - public PluginRegistry(PluginRegistry defaults) { - this.connections.addAll(defaults.connections); - this.clients.addAll(defaults.clients); - this.servers.addAll(defaults.servers); - } - - public void addConnectionPlugin(DuplexConnectionInterceptor interceptor) { - connections.add(interceptor); - } - - public void addClientPlugin(RSocketInterceptor interceptor) { - clients.add(interceptor); - } - - public void addServerPlugin(RSocketInterceptor interceptor) { - servers.add(interceptor); - } - - public RSocket applyClient(RSocket rSocket) { - for (RSocketInterceptor i : clients) { - rSocket = i.apply(rSocket); - } - - return rSocket; - } - - public RSocket applyServer(RSocket rSocket) { - for (RSocketInterceptor i : servers) { - rSocket = i.apply(rSocket); - } - - return rSocket; - } - - public DuplexConnection applyConnection( - DuplexConnectionInterceptor.Type type, DuplexConnection connection) { - for (DuplexConnectionInterceptor i : connections) { - connection = i.apply(type, connection); - } - - return connection; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/Plugins.java b/rsocket-core/src/main/java/io/rsocket/plugins/Plugins.java deleted file mode 100644 index 13d5db33b..000000000 --- a/rsocket-core/src/main/java/io/rsocket/plugins/Plugins.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.plugins; - -/** JVM wide plugins for RSocket */ -public class Plugins { - private static PluginRegistry DEFAULT = new PluginRegistry(); - - private Plugins() {} - - public static void interceptConnection(DuplexConnectionInterceptor interceptor) { - DEFAULT.addConnectionPlugin(interceptor); - } - - public static void interceptClient(RSocketInterceptor interceptor) { - DEFAULT.addClientPlugin(interceptor); - } - - public static void interceptServer(RSocketInterceptor interceptor) { - DEFAULT.addServerPlugin(interceptor); - } - - public static PluginRegistry defaultPlugins() { - return DEFAULT; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/RSocketInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/RSocketInterceptor.java index 8be627880..0cd4bb8f6 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/RSocketInterceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/RSocketInterceptor.java @@ -1,17 +1,17 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket.plugins; @@ -19,5 +19,10 @@ import io.rsocket.RSocket; import java.util.function.Function; -/** */ +/** + * Contract to decorate an {@link RSocket}, providing a way to intercept interactions. This can be + * applied to a {@link InterceptorRegistry#forRequester(RSocketInterceptor) requester} or {@link + * InterceptorRegistry#forResponder(RSocketInterceptor) responder} {@code RSocket} of a client or + * server. + */ public @FunctionalInterface interface RSocketInterceptor extends Function {} diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java new file mode 100644 index 000000000..6dd850ba9 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java @@ -0,0 +1,29 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.plugins; + +import io.rsocket.SocketAcceptor; +import java.util.function.Function; + +/** + * Contract to decorate a {@link SocketAcceptor}, providing access to connection {@code setup} + * information and the ability to also decorate the sockets for requesting and responding. + * + *

This could be used as an alternative to registering an individual "requester" {@code + * RSocketInterceptor} and "responder" {@code RSocketInterceptor}. + */ +public @FunctionalInterface interface SocketAcceptorInterceptor + extends Function {} diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/package-info.java b/rsocket-core/src/main/java/io/rsocket/plugins/package-info.java new file mode 100644 index 000000000..fd9e1f01a --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/plugins/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Contracts for interception of transports, connections, and requests in in RSocket Java. */ +@NonNullApi +package io.rsocket.plugins; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java new file mode 100644 index 000000000..ed9450357 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -0,0 +1,194 @@ +/* + * 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.resume; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.exceptions.ConnectionErrorException; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.ResumeFrameCodec; +import io.rsocket.frame.ResumeOkFrameCodec; +import io.rsocket.internal.ClientServerInputMultiplexer; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +public class ClientRSocketSession implements RSocketSession> { + private static final Logger logger = LoggerFactory.getLogger(ClientRSocketSession.class); + + private final ResumableDuplexConnection resumableConnection; + private volatile Mono newConnection; + private volatile ByteBuf resumeToken; + private final ByteBufAllocator allocator; + + public ClientRSocketSession( + DuplexConnection duplexConnection, + Duration resumeSessionDuration, + Retry retry, + ResumableFramesStore resumableFramesStore, + Duration resumeStreamTimeout, + boolean cleanupStoreOnKeepAlive) { + this.allocator = duplexConnection.alloc(); + this.resumableConnection = + new ResumableDuplexConnection( + "client", + duplexConnection, + resumableFramesStore, + resumeStreamTimeout, + cleanupStoreOnKeepAlive); + + /*session completed: release token initially retained in resumeToken(ByteBuf)*/ + onClose().doFinally(s -> resumeToken.release()).subscribe(); + + resumableConnection + .connectionErrors() + .flatMap( + err -> { + logger.debug("Client session connection error. Starting new connection"); + AtomicBoolean once = new AtomicBoolean(); + return newConnection + .delaySubscription( + once.compareAndSet(false, true) + ? retry.generateCompanion(Flux.just(new RetrySignal(err))) + : Mono.empty()) + .retryWhen(retry) + .timeout(resumeSessionDuration); + }) + .map(ClientServerInputMultiplexer::new) + .subscribe( + multiplexer -> { + /*reconnect resumable connection*/ + reconnect(multiplexer.asClientServerConnection()); + long impliedPosition = resumableConnection.impliedPosition(); + long position = resumableConnection.position(); + logger.debug( + "Client ResumableConnection reconnected. Sending RESUME frame with state: [impliedPos: {}, pos: {}]", + impliedPosition, + position); + /*Connection is established again: send RESUME frame to server, listen for RESUME_OK*/ + sendFrame( + ResumeFrameCodec.encode( + allocator, + /*retain so token is not released once sent as part of resume frame*/ + resumeToken.retain(), + impliedPosition, + position)) + .then(multiplexer.asSetupConnection().receive().next()) + .subscribe(this::resumeWith); + }, + err -> { + logger.debug("Client ResumableConnection reconnect timeout"); + resumableConnection.dispose(); + }); + } + + @Override + public ClientRSocketSession continueWith(Mono connectionFactory) { + this.newConnection = connectionFactory; + return this; + } + + @Override + public ClientRSocketSession resumeWith(ByteBuf resumeOkFrame) { + logger.debug("ResumeOK FRAME received"); + long remotePos = remotePos(resumeOkFrame); + long remoteImpliedPos = remoteImpliedPos(resumeOkFrame); + resumeOkFrame.release(); + + resumableConnection.resume( + remotePos, + remoteImpliedPos, + pos -> + pos.then() + /*Resumption is impossible: send CONNECTION_ERROR*/ + .onErrorResume( + err -> + sendFrame( + ErrorFrameCodec.encode( + allocator, 0, errorFrameThrowable(remoteImpliedPos))) + .then(Mono.fromRunnable(resumableConnection::dispose)) + /*Resumption is impossible: no need to return control to ResumableConnection*/ + .then(Mono.never()))); + return this; + } + + public ClientRSocketSession resumeToken(ByteBuf resumeToken) { + /*retain so token is not released once sent as part of setup frame*/ + this.resumeToken = resumeToken.retain(); + return this; + } + + @Override + public void reconnect(DuplexConnection connection) { + resumableConnection.reconnect(connection); + } + + @Override + public ResumableDuplexConnection resumableConnection() { + return resumableConnection; + } + + @Override + public ByteBuf token() { + return resumeToken; + } + + private Mono sendFrame(ByteBuf frame) { + return resumableConnection.sendOne(frame).onErrorResume(err -> Mono.empty()); + } + + private static long remoteImpliedPos(ByteBuf resumeOkFrame) { + return ResumeOkFrameCodec.lastReceivedClientPos(resumeOkFrame); + } + + private static long remotePos(ByteBuf resumeOkFrame) { + return -1; + } + + private static ConnectionErrorException errorFrameThrowable(long impliedPos) { + return new ConnectionErrorException("resumption_server_pos=[" + impliedPos + "]"); + } + + private static class RetrySignal implements Retry.RetrySignal { + + private final Throwable ex; + + RetrySignal(Throwable ex) { + this.ex = ex; + } + + @Override + public long totalRetries() { + return 0; + } + + @Override + public long totalRetriesInARow() { + return 0; + } + + @Override + public Throwable failure() { + return ex; + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java new file mode 100644 index 000000000..415a77f92 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.resume; + +import io.netty.buffer.ByteBuf; +import java.time.Duration; + +public class ClientResume { + private final Duration sessionDuration; + private final ByteBuf resumeToken; + + public ClientResume(Duration sessionDuration, ByteBuf resumeToken) { + this.sessionDuration = sessionDuration; + this.resumeToken = resumeToken; + } + + public Duration sessionDuration() { + return sessionDuration; + } + + public ByteBuf resumeToken() { + return resumeToken; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ExponentialBackoffResumeStrategy.java b/rsocket-core/src/main/java/io/rsocket/resume/ExponentialBackoffResumeStrategy.java new file mode 100644 index 000000000..461be02d2 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ExponentialBackoffResumeStrategy.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.resume; + +import java.time.Duration; +import java.util.Objects; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +/** + * @deprecated as of 1.0 RC7 in favor of passing {@link Retry#backoff(long, Duration)} to {@link + * io.rsocket.core.Resume#retry(Retry)}. + */ +@Deprecated +public class ExponentialBackoffResumeStrategy implements ResumeStrategy { + private volatile Duration next; + private final Duration firstBackoff; + private final Duration maxBackoff; + private final int factor; + + public ExponentialBackoffResumeStrategy(Duration firstBackoff, Duration maxBackoff, int factor) { + this.firstBackoff = Objects.requireNonNull(firstBackoff, "firstBackoff"); + this.maxBackoff = Objects.requireNonNull(maxBackoff, "maxBackoff"); + this.factor = requirePositive(factor); + } + + @Override + public Publisher apply(ClientResume clientResume, Throwable throwable) { + return Flux.defer(() -> Mono.delay(next()).thenReturn(toString())); + } + + Duration next() { + next = + next == null + ? firstBackoff + : Duration.ofMillis(Math.min(maxBackoff.toMillis(), next.toMillis() * factor)); + return next; + } + + private static int requirePositive(int value) { + if (value <= 0) { + throw new IllegalArgumentException("Value must be positive: " + value); + } else { + return value; + } + } + + @Override + public String toString() { + return "ExponentialBackoffResumeStrategy{" + + "next=" + + next + + ", firstBackoff=" + + firstBackoff + + ", maxBackoff=" + + maxBackoff + + ", factor=" + + factor + + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java new file mode 100644 index 000000000..1875b7eac --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java @@ -0,0 +1,240 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.resume; + +import io.netty.buffer.ByteBuf; +import java.util.Queue; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.util.concurrent.Queues; + +public class InMemoryResumableFramesStore implements ResumableFramesStore { + private static final Logger logger = LoggerFactory.getLogger(InMemoryResumableFramesStore.class); + private static final long SAVE_REQUEST_SIZE = Long.MAX_VALUE; + + private final MonoProcessor disposed = MonoProcessor.create(); + volatile long position; + volatile long impliedPosition; + volatile int cacheSize; + final Queue cachedFrames; + private final String tag; + private final int cacheLimit; + private volatile int upstreamFrameRefCnt; + + public InMemoryResumableFramesStore(String tag, int cacheSizeBytes) { + this.tag = tag; + this.cacheLimit = cacheSizeBytes; + this.cachedFrames = cachedFramesQueue(cacheSizeBytes); + } + + public Mono saveFrames(Flux frames) { + MonoProcessor completed = MonoProcessor.create(); + frames + .doFinally(s -> completed.onComplete()) + .subscribe(new FramesSubscriber(SAVE_REQUEST_SIZE)); + return completed; + } + + @Override + public void releaseFrames(long remoteImpliedPos) { + long pos = position; + logger.debug( + "{} Removing frames for local: {}, remote implied: {}", tag, pos, remoteImpliedPos); + long removeSize = Math.max(0, remoteImpliedPos - pos); + while (removeSize > 0) { + ByteBuf cachedFrame = cachedFrames.poll(); + if (cachedFrame != null) { + removeSize -= releaseTailFrame(cachedFrame); + } else { + break; + } + } + if (removeSize > 0) { + throw new IllegalStateException( + String.format( + "Local and remote state disagreement: " + + "need to remove additional %d bytes, but cache is empty", + removeSize)); + } else if (removeSize < 0) { + throw new IllegalStateException( + "Local and remote state disagreement: " + "local and remote frame sizes are not equal"); + } else { + logger.debug("{} Removed frames. Current cache size: {}", tag, cacheSize); + } + } + + @Override + public Flux resumeStream() { + return Flux.generate( + () -> new ResumeStreamState(cachedFrames.size(), upstreamFrameRefCnt), + (state, sink) -> { + if (state.next()) { + /*spsc queue has no iterator - iterating by consuming*/ + ByteBuf frame = cachedFrames.poll(); + if (state.shouldRetain(frame)) { + frame.retain(); + } + cachedFrames.offer(frame); + sink.next(frame); + } else { + sink.complete(); + logger.debug("{} Resuming stream completed", tag); + } + return state; + }); + } + + @Override + public long framePosition() { + return position; + } + + @Override + public long frameImpliedPosition() { + return impliedPosition; + } + + @Override + public void resumableFrameReceived(ByteBuf frame) { + /*called on transport thread so non-atomic on volatile is safe*/ + impliedPosition += frame.readableBytes(); + } + + @Override + public Mono onClose() { + return disposed; + } + + @Override + public void dispose() { + cacheSize = 0; + ByteBuf frame = cachedFrames.poll(); + while (frame != null) { + frame.release(); + frame = cachedFrames.poll(); + } + disposed.onComplete(); + } + + @Override + public boolean isDisposed() { + return disposed.isTerminated(); + } + + /* this method and saveFrame() won't be called concurrently, + * so non-atomic on volatile is safe*/ + private int releaseTailFrame(ByteBuf content) { + int frameSize = content.readableBytes(); + cacheSize -= frameSize; + position += frameSize; + content.release(); + return frameSize; + } + + /*this method and releaseTailFrame() won't be called concurrently, + * so non-atomic on volatile is safe*/ + void saveFrame(ByteBuf frame) { + if (upstreamFrameRefCnt == 0) { + upstreamFrameRefCnt = frame.refCnt(); + } + + int frameSize = frame.readableBytes(); + long availableSize = cacheLimit - cacheSize; + while (availableSize < frameSize) { + ByteBuf cachedFrame = cachedFrames.poll(); + if (cachedFrame != null) { + availableSize += releaseTailFrame(cachedFrame); + } else { + break; + } + } + if (availableSize >= frameSize) { + cachedFrames.offer(frame.retain()); + cacheSize += frameSize; + } else { + position += frameSize; + } + } + + static class ResumeStreamState { + private final int cacheSize; + private final int expectedRefCnt; + private int cacheCounter; + + public ResumeStreamState(int cacheSize, int expectedRefCnt) { + this.cacheSize = cacheSize; + this.expectedRefCnt = expectedRefCnt; + } + + public boolean next() { + if (cacheCounter < cacheSize) { + cacheCounter++; + return true; + } else { + return false; + } + } + + public boolean shouldRetain(ByteBuf frame) { + return frame.refCnt() == expectedRefCnt; + } + } + + static Queue cachedFramesQueue(int size) { + return Queues.get(size).get(); + } + + class FramesSubscriber implements Subscriber { + private final long firstRequestSize; + private final long refillSize; + private int received; + private Subscription s; + + public FramesSubscriber(long requestSize) { + this.firstRequestSize = requestSize; + this.refillSize = firstRequestSize / 2; + } + + @Override + public void onSubscribe(Subscription s) { + this.s = s; + s.request(firstRequestSize); + } + + @Override + public void onNext(ByteBuf byteBuf) { + saveFrame(byteBuf); + if (firstRequestSize != Long.MAX_VALUE && ++received == refillSize) { + received = 0; + s.request(refillSize); + } + } + + @Override + public void onError(Throwable t) { + logger.info("unexpected onError signal: {}, {}", t.getClass(), t.getMessage()); + } + + @Override + public void onComplete() {} + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/PeriodicResumeStrategy.java b/rsocket-core/src/main/java/io/rsocket/resume/PeriodicResumeStrategy.java new file mode 100644 index 000000000..bd447c8a9 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/PeriodicResumeStrategy.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.resume; + +import java.time.Duration; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +/** + * @deprecated as of 1.0 RC7 in favor of passing {@link Retry#fixedDelay(long, Duration)} to {@link + * io.rsocket.core.Resume#retry(Retry)}. + */ +@Deprecated +public class PeriodicResumeStrategy implements ResumeStrategy { + private final Duration interval; + + public PeriodicResumeStrategy(Duration interval) { + this.interval = interval; + } + + @Override + public Publisher apply(ClientResume clientResumeConfiguration, Throwable throwable) { + return Mono.delay(interval).thenReturn(toString()); + } + + @Override + public String toString() { + return "PeriodicResumeStrategy{" + "interval=" + interval + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java new file mode 100644 index 000000000..7ec0abaee --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.resume; + +import io.netty.buffer.ByteBuf; +import io.rsocket.Closeable; +import io.rsocket.DuplexConnection; +import reactor.core.publisher.Mono; + +public interface RSocketSession extends Closeable { + + ByteBuf token(); + + ResumableDuplexConnection resumableConnection(); + + RSocketSession continueWith(T ConnectionFactory); + + RSocketSession resumeWith(ByteBuf resumeFrame); + + void reconnect(DuplexConnection connection); + + @Override + default Mono onClose() { + return resumableConnection().onClose(); + } + + @Override + default void dispose() { + resumableConnection().dispose(); + } + + @Override + default boolean isDisposed() { + return resumableConnection().isDisposed(); + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/SecureWebsocketClientServerTest.java b/rsocket-core/src/main/java/io/rsocket/resume/RequestListener.java similarity index 52% rename from rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/SecureWebsocketClientServerTest.java rename to rsocket-core/src/main/java/io/rsocket/resume/RequestListener.java index ff787dc90..6553e5ec5 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/SecureWebsocketClientServerTest.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/RequestListener.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,14 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.transport.netty; -import io.rsocket.test.BaseClientServerTest; +package io.rsocket.resume; -public class SecureWebsocketClientServerTest - extends BaseClientServerTest { - @Override - protected SecureWebsocketClientSetupRule createClientServer() { - return new SecureWebsocketClientSetupRule(); +import reactor.core.publisher.Flux; +import reactor.core.publisher.ReplayProcessor; + +class RequestListener { + private final ReplayProcessor requests = ReplayProcessor.create(1); + + public Flux apply(Flux flux) { + return flux.doOnRequest(requests::onNext); + } + + public Flux requests() { + return requests; } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java new file mode 100644 index 000000000..461d71228 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -0,0 +1,450 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.resume; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Closeable; +import io.rsocket.DuplexConnection; +import io.rsocket.frame.FrameHeaderCodec; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.publisher.*; +import reactor.util.concurrent.Queues; + +public class ResumableDuplexConnection implements DuplexConnection, ResumeStateHolder { + private static final Logger logger = LoggerFactory.getLogger(ResumableDuplexConnection.class); + private static final Throwable closedChannelException = new ClosedChannelException(); + + private final String tag; + private final ResumableFramesStore resumableFramesStore; + private final Duration resumeStreamTimeout; + private final boolean cleanupOnKeepAlive; + + private final ReplayProcessor connections = ReplayProcessor.create(1); + private final EmitterProcessor connectionErrors = EmitterProcessor.create(); + private volatile DuplexConnection curConnection; + /*used instead of EmitterProcessor because its autocancel=false capability had no expected effect*/ + private final FluxProcessor downStreamFrames = ReplayProcessor.create(0); + private final FluxProcessor resumeSaveFrames = EmitterProcessor.create(); + private final MonoProcessor resumeSaveCompleted = MonoProcessor.create(); + private final Queue actions = Queues.unboundedMultiproducer().get(); + private final AtomicInteger actionsWip = new AtomicInteger(); + private final AtomicBoolean disposed = new AtomicBoolean(); + + private final Mono framesSent; + private final RequestListener downStreamRequestListener = new RequestListener(); + private final RequestListener resumeSaveStreamRequestListener = new RequestListener(); + private final UnicastProcessor> upstreams = UnicastProcessor.create(); + private final UpstreamFramesSubscriber upstreamSubscriber = + new UpstreamFramesSubscriber( + Queues.SMALL_BUFFER_SIZE, + downStreamRequestListener.requests(), + resumeSaveStreamRequestListener.requests(), + this::dispatch); + + private volatile Runnable onResume; + private volatile Runnable onDisconnect; + private volatile int state; + private volatile Disposable resumedStreamDisposable = Disposables.disposed(); + + public ResumableDuplexConnection( + String tag, + DuplexConnection duplexConnection, + ResumableFramesStore resumableFramesStore, + Duration resumeStreamTimeout, + boolean cleanupOnKeepAlive) { + this.tag = tag; + this.resumableFramesStore = resumableFramesStore; + this.resumeStreamTimeout = resumeStreamTimeout; + this.cleanupOnKeepAlive = cleanupOnKeepAlive; + + resumableFramesStore + .saveFrames(resumeSaveStreamRequestListener.apply(resumeSaveFrames)) + .subscribe(resumeSaveCompleted); + + upstreams.flatMap(Function.identity()).subscribe(upstreamSubscriber); + + framesSent = + connections + .switchMap( + c -> { + logger.debug("Switching transport: {}", tag); + return c.send(downStreamRequestListener.apply(downStreamFrames)) + .doFinally( + s -> + logger.debug( + "{} Transport send completed: {}, {}", tag, s, c.toString())) + .onErrorResume(err -> Mono.never()); + }) + .then() + .cache(); + + reconnect(duplexConnection); + } + + @Override + public ByteBufAllocator alloc() { + return curConnection.alloc(); + } + + public void disconnect() { + DuplexConnection c = this.curConnection; + if (c != null) { + disconnect(c); + } + } + + public void onDisconnect(Runnable onDisconnectAction) { + this.onDisconnect = onDisconnectAction; + } + + public void onResume(Runnable onResumeAction) { + this.onResume = onResumeAction; + } + + /*reconnected by session after error. After this downstream can receive frames, + * but sending in suppressed until resume() is called*/ + public void reconnect(DuplexConnection connection) { + if (curConnection == null) { + logger.debug("{} Resumable duplex connection started with connection: {}", tag, connection); + state = State.CONNECTED; + onNewConnection(connection); + } else { + logger.debug( + "{} Resumable duplex connection reconnected with connection: {}", tag, connection); + /*race between sendFrame and doResumeStart may lead to ongoing upstream frames + written before resume complete*/ + dispatch(new ResumeStart(connection)); + } + } + + /*after receiving RESUME (Server) or RESUME_OK (Client) + calculate and send resume frames */ + public void resume( + long remotePos, long remoteImpliedPos, Function, Mono> resumeFrameSent) { + /*race between sendFrame and doResume may lead to duplicate frames on resume store*/ + dispatch(new Resume(remotePos, remoteImpliedPos, resumeFrameSent)); + } + + @Override + public Mono sendOne(ByteBuf frame) { + return curConnection.sendOne(frame); + } + + @Override + public Mono send(Publisher frames) { + upstreams.onNext(Flux.from(frames)); + return framesSent; + } + + @Override + public Flux receive() { + return connections.switchMap( + c -> + c.receive() + .doOnNext( + f -> { + if (isResumableFrame(f)) { + resumableFramesStore.resumableFrameReceived(f); + } + }) + .onErrorResume(err -> Mono.never())); + } + + public long position() { + return resumableFramesStore.framePosition(); + } + + @Override + public long impliedPosition() { + return resumableFramesStore.frameImpliedPosition(); + } + + @Override + public void onImpliedPosition(long remoteImpliedPos) { + logger.debug("Got remote position from keep-alive: {}", remoteImpliedPos); + if (cleanupOnKeepAlive) { + dispatch(new ReleaseFrames(remoteImpliedPos)); + } + } + + @Override + public Mono onClose() { + return Flux.merge(connections.last().flatMap(Closeable::onClose), resumeSaveCompleted).then(); + } + + @Override + public void dispose() { + if (disposed.compareAndSet(false, true)) { + logger.debug("Resumable connection disposed: {}, {}", tag, this); + upstreams.onComplete(); + connections.onComplete(); + connectionErrors.onComplete(); + resumeSaveFrames.onComplete(); + curConnection.dispose(); + upstreamSubscriber.dispose(); + resumedStreamDisposable.dispose(); + resumableFramesStore.dispose(); + } + } + + @Override + public double availability() { + return curConnection.availability(); + } + + @Override + public boolean isDisposed() { + return disposed.get(); + } + + private void sendFrame(ByteBuf f) { + if (disposed.get()) { + f.release(); + return; + } + /*resuming from store so no need to save again*/ + if (state != State.RESUME && isResumableFrame(f)) { + resumeSaveFrames.onNext(f); + } + /*filter frames coming from upstream before actual resumption began, + * to preserve frames ordering*/ + if (state != State.RESUME_STARTED) { + downStreamFrames.onNext(f); + } + } + + Flux connectionErrors() { + return connectionErrors; + } + + private void dispatch(Object action) { + actions.offer(action); + if (actionsWip.getAndIncrement() == 0) { + do { + Object a = actions.poll(); + if (a instanceof ByteBuf) { + sendFrame((ByteBuf) a); + } else { + ((Runnable) a).run(); + } + } while (actionsWip.decrementAndGet() != 0); + } + } + + private void doResumeStart(DuplexConnection connection) { + state = State.RESUME_STARTED; + resumedStreamDisposable.dispose(); + upstreamSubscriber.resumeStart(); + onNewConnection(connection); + } + + private void doResume( + long remotePosition, + long remoteImpliedPosition, + Function, Mono> sendResumeFrame) { + long localPosition = position(); + long localImpliedPosition = impliedPosition(); + + logger.debug("Resumption start"); + logger.debug( + "Resumption states. local: [pos: {}, impliedPos: {}], remote: [pos: {}, impliedPos: {}]", + localPosition, + localImpliedPosition, + remotePosition, + remoteImpliedPosition); + + long remoteImpliedPos = + calculateRemoteImpliedPos( + localPosition, localImpliedPosition, + remotePosition, remoteImpliedPosition); + + Mono impliedPositionOrError; + if (remoteImpliedPos >= 0) { + state = State.RESUME; + releaseFramesToPosition(remoteImpliedPos); + impliedPositionOrError = Mono.just(localImpliedPosition); + } else { + impliedPositionOrError = + Mono.error( + new ResumeStateException( + localPosition, localImpliedPosition, + remotePosition, remoteImpliedPosition)); + } + + sendResumeFrame + .apply(impliedPositionOrError) + .doOnSuccess( + v -> { + Runnable r = this.onResume; + if (r != null) { + r.run(); + } + }) + .then( + streamResumedFrames( + resumableFramesStore + .resumeStream() + .timeout(resumeStreamTimeout) + .doFinally(s -> dispatch(new ResumeComplete()))) + .doOnError(err -> dispose())) + .onErrorResume(err -> Mono.empty()) + .subscribe(); + } + + static long calculateRemoteImpliedPos( + long pos, long impliedPos, long remotePos, long remoteImpliedPos) { + if (remotePos <= impliedPos && pos <= remoteImpliedPos) { + return remoteImpliedPos; + } else { + return -1L; + } + } + + private void doResumeComplete() { + logger.debug("Completing resumption"); + state = State.RESUME_COMPLETED; + upstreamSubscriber.resumeComplete(); + } + + private Mono streamResumedFrames(Flux frames) { + return Mono.create( + s -> { + ResumeFramesSubscriber subscriber = + new ResumeFramesSubscriber( + downStreamRequestListener.requests(), this::dispatch, s::error, s::success); + s.onDispose(subscriber); + resumedStreamDisposable = subscriber; + frames.subscribe(subscriber); + }); + } + + private void onNewConnection(DuplexConnection connection) { + curConnection = connection; + connection.onClose().doFinally(v -> disconnect(connection)).subscribe(); + connections.onNext(connection); + } + + private void disconnect(DuplexConnection connection) { + /*do not report late disconnects on old connection if new one is available*/ + if (curConnection == connection && state != State.DISCONNECTED) { + connection.dispose(); + state = State.DISCONNECTED; + logger.debug( + "{} Inner connection disconnected: {}", + tag, + closedChannelException.getClass().getSimpleName()); + connectionErrors.onNext(closedChannelException); + Runnable r = this.onDisconnect; + if (r != null) { + r.run(); + } + } + } + + /*remove frames confirmed by implied pos, + set current pos accordingly*/ + private void releaseFramesToPosition(long remoteImpliedPos) { + resumableFramesStore.releaseFrames(remoteImpliedPos); + } + + static boolean isResumableFrame(ByteBuf frame) { + switch (FrameHeaderCodec.nativeFrameType(frame)) { + case REQUEST_CHANNEL: + case REQUEST_STREAM: + case REQUEST_RESPONSE: + case REQUEST_FNF: + case REQUEST_N: + case CANCEL: + case ERROR: + case PAYLOAD: + return true; + default: + return false; + } + } + + static class State { + static int CONNECTED = 0; + static int RESUME_STARTED = 1; + static int RESUME = 2; + static int RESUME_COMPLETED = 3; + static int DISCONNECTED = 4; + } + + class ResumeStart implements Runnable { + private final DuplexConnection connection; + + public ResumeStart(DuplexConnection connection) { + this.connection = connection; + } + + @Override + public void run() { + doResumeStart(connection); + } + } + + class Resume implements Runnable { + private final long remotePos; + private final long remoteImpliedPos; + private final Function, Mono> resumeFrameSent; + + public Resume( + long remotePos, long remoteImpliedPos, Function, Mono> resumeFrameSent) { + this.remotePos = remotePos; + this.remoteImpliedPos = remoteImpliedPos; + this.resumeFrameSent = resumeFrameSent; + } + + @Override + public void run() { + doResume(remotePos, remoteImpliedPos, resumeFrameSent); + } + } + + private class ResumeComplete implements Runnable { + + @Override + public void run() { + doResumeComplete(); + } + } + + private class ReleaseFrames implements Runnable { + private final long remoteImpliedPos; + + public ReleaseFrames(long remoteImpliedPos) { + this.remoteImpliedPos = remoteImpliedPos; + } + + @Override + public void run() { + releaseFramesToPosition(remoteImpliedPos); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java new file mode 100644 index 000000000..3a30544b6 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java @@ -0,0 +1,55 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.resume; + +import io.netty.buffer.ByteBuf; +import io.rsocket.Closeable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** Store for resumable frames */ +public interface ResumableFramesStore extends Closeable { + + /** + * Save resumable frames for potential resumption + * + * @param frames {@link Flux} of resumable frames + * @return {@link Mono} which completes once all resume frames are written + */ + Mono saveFrames(Flux frames); + + /** Release frames from tail of the store up to remote implied position */ + void releaseFrames(long remoteImpliedPos); + + /** + * @return {@link Flux} of frames from store tail to head. It should terminate with error if + * frames are not continuous + */ + Flux resumeStream(); + + /** @return Local frame position as defined by RSocket protocol */ + long framePosition(); + + /** @return Implied frame position as defined by RSocket protocol */ + long frameImpliedPosition(); + + /** + * Received resumable frame as defined by RSocket protocol. Implementation must increment frame + * implied position + */ + void resumableFrameReceived(ByteBuf frame); +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeFramesSubscriber.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeFramesSubscriber.java new file mode 100644 index 000000000..4facdd3c1 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeFramesSubscriber.java @@ -0,0 +1,88 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.resume; + +import io.netty.buffer.ByteBuf; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; + +class ResumeFramesSubscriber implements Subscriber, Disposable { + private final Flux requests; + private final Consumer onNext; + private final Consumer onError; + private final Runnable onComplete; + private final AtomicBoolean disposed = new AtomicBoolean(); + private volatile Disposable requestsDisposable; + private volatile Subscription subscription; + + public ResumeFramesSubscriber( + Flux requests, + Consumer onNext, + Consumer onError, + Runnable onComplete) { + this.requests = requests; + this.onNext = onNext; + this.onError = onError; + this.onComplete = onComplete; + } + + @Override + public void onSubscribe(Subscription s) { + if (isDisposed()) { + s.cancel(); + } else { + this.subscription = s; + this.requestsDisposable = requests.subscribe(s::request); + } + } + + @Override + public void onNext(ByteBuf frame) { + this.onNext.accept(frame); + } + + @Override + public void onError(Throwable t) { + this.onError.accept(t); + requestsDisposable.dispose(); + } + + @Override + public void onComplete() { + this.onComplete.run(); + requestsDisposable.dispose(); + } + + @Override + public void dispose() { + if (disposed.compareAndSet(false, true)) { + if (subscription != null) { + subscription.cancel(); + requestsDisposable.dispose(); + } + } + } + + @Override + public boolean isDisposed() { + return disposed.get(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateException.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateException.java new file mode 100644 index 000000000..1fae24b07 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.resume; + +class ResumeStateException extends RuntimeException { + private static final long serialVersionUID = -5393753463377588732L; + private final long localPos; + private final long localImpliedPos; + private final long remotePos; + private final long remoteImpliedPos; + + public ResumeStateException( + long localPos, long localImpliedPos, long remotePos, long remoteImpliedPos) { + this.localPos = localPos; + this.localImpliedPos = localImpliedPos; + this.remotePos = remotePos; + this.remoteImpliedPos = remoteImpliedPos; + } + + public long getLocalPos() { + return localPos; + } + + public long getLocalImpliedPos() { + return localImpliedPos; + } + + public long getRemotePos() { + return remotePos; + } + + public long getRemoteImpliedPos() { + return remoteImpliedPos; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/TimeoutException.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateHolder.java similarity index 67% rename from rsocket-core/src/main/java/io/rsocket/exceptions/TimeoutException.java rename to rsocket-core/src/main/java/io/rsocket/resume/ResumeStateHolder.java index 8198ee743..31687a24b 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/TimeoutException.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateHolder.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,9 +14,11 @@ * limitations under the License. */ -package io.rsocket.exceptions; +package io.rsocket.resume; -public class TimeoutException extends Exception { +public interface ResumeStateHolder { - private static final long serialVersionUID = -6352901497935205059L; + long impliedPosition(); + + void onImpliedPosition(long remoteImpliedPos); } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeStrategy.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStrategy.java new file mode 100644 index 000000000..d9dec9f54 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStrategy.java @@ -0,0 +1,29 @@ +/* + * 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.resume; + +import java.util.function.BiFunction; +import org.reactivestreams.Publisher; +import reactor.util.retry.Retry; + +/** + * @deprecated as of 1.0 RC7 in favor of using {@link io.rsocket.core.Resume#retry(Retry)} via + * {@link io.rsocket.core.RSocketConnector} or {@link io.rsocket.core.RSocketServer}. + */ +@Deprecated +@FunctionalInterface +public interface ResumeStrategy extends BiFunction> {} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java new file mode 100644 index 000000000..b54ce644f --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -0,0 +1,160 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.resume; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.exceptions.RejectedResumeException; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.ResumeFrameCodec; +import io.rsocket.frame.ResumeOkFrameCodec; +import java.time.Duration; +import java.util.function.Function; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.FluxProcessor; +import reactor.core.publisher.Mono; +import reactor.core.publisher.ReplayProcessor; + +public class ServerRSocketSession implements RSocketSession { + private static final Logger logger = LoggerFactory.getLogger(ServerRSocketSession.class); + + private final ResumableDuplexConnection resumableConnection; + /*used instead of EmitterProcessor because its autocancel=false capability had no expected effect*/ + private final FluxProcessor newConnections = + ReplayProcessor.create(0); + private final ByteBufAllocator allocator; + private final ByteBuf resumeToken; + + public ServerRSocketSession( + DuplexConnection duplexConnection, + Duration resumeSessionDuration, + Duration resumeStreamTimeout, + Function resumeStoreFactory, + ByteBuf resumeToken, + boolean cleanupStoreOnKeepAlive) { + this.allocator = duplexConnection.alloc(); + this.resumeToken = resumeToken; + this.resumableConnection = + new ResumableDuplexConnection( + "server", + duplexConnection, + resumeStoreFactory.apply(resumeToken), + resumeStreamTimeout, + cleanupStoreOnKeepAlive); + + Mono timeout = + resumableConnection + .connectionErrors() + .flatMap( + err -> { + logger.debug("Starting session timeout due to error", err); + return newConnections + .next() + .doOnNext(c -> logger.debug("Connection after error: {}", c)) + .timeout(resumeSessionDuration); + }) + .then() + .cast(DuplexConnection.class); + + newConnections + .mergeWith(timeout) + .subscribe( + connection -> { + reconnect(connection); + logger.debug("Server ResumableConnection reconnected: {}", connection); + }, + err -> { + logger.debug("Server ResumableConnection reconnect timeout"); + resumableConnection.dispose(); + }); + } + + @Override + public ServerRSocketSession continueWith(DuplexConnection connectionFactory) { + logger.debug("Server continued with connection: {}", connectionFactory); + newConnections.onNext(connectionFactory); + return this; + } + + @Override + public ServerRSocketSession resumeWith(ByteBuf resumeFrame) { + logger.debug("Resume FRAME received"); + long remotePos = remotePos(resumeFrame); + long remoteImpliedPos = remoteImpliedPos(resumeFrame); + resumeFrame.release(); + + resumableConnection.resume( + remotePos, + remoteImpliedPos, + pos -> + pos.flatMap(impliedPos -> sendFrame(ResumeOkFrameCodec.encode(allocator, impliedPos))) + .onErrorResume( + err -> + sendFrame(ErrorFrameCodec.encode(allocator, 0, errorFrameThrowable(err))) + .then(Mono.fromRunnable(resumableConnection::dispose)) + /*Resumption is impossible: no need to return control to ResumableConnection*/ + .then(Mono.never()))); + return this; + } + + @Override + public void reconnect(DuplexConnection connection) { + resumableConnection.reconnect(connection); + } + + @Override + public ResumableDuplexConnection resumableConnection() { + return resumableConnection; + } + + @Override + public ByteBuf token() { + return resumeToken; + } + + private Mono sendFrame(ByteBuf frame) { + logger.debug("Sending Resume frame: {}", frame); + return resumableConnection.sendOne(frame).onErrorResume(e -> Mono.empty()); + } + + private static long remotePos(ByteBuf resumeFrame) { + return ResumeFrameCodec.firstAvailableClientPos(resumeFrame); + } + + private static long remoteImpliedPos(ByteBuf resumeFrame) { + return ResumeFrameCodec.lastReceivedServerPos(resumeFrame); + } + + private static RejectedResumeException errorFrameThrowable(Throwable err) { + String msg; + if (err instanceof ResumeStateException) { + ResumeStateException resumeException = ((ResumeStateException) err); + msg = + String.format( + "resumption_pos=[ remote: { pos: %d, impliedPos: %d }, local: { pos: %d, impliedPos: %d }]", + resumeException.getRemotePos(), + resumeException.getRemoteImpliedPos(), + resumeException.getLocalPos(), + resumeException.getLocalImpliedPos()); + } else { + msg = String.format("resume_internal_error: %s", err.getMessage()); + } + return new RejectedResumeException(msg); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java b/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java new file mode 100644 index 000000000..1d5c23bd6 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java @@ -0,0 +1,61 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.resume; + +import io.netty.buffer.ByteBuf; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import reactor.util.annotation.Nullable; + +public class SessionManager { + private volatile boolean isDisposed; + private final Map sessions = new ConcurrentHashMap<>(); + + public ServerRSocketSession save(ServerRSocketSession session) { + if (isDisposed) { + session.dispose(); + } else { + ByteBuf token = session.token().retain(); + session + .onClose() + .doOnSuccess( + v -> { + if (isDisposed || sessions.get(token) == session) { + sessions.remove(token); + } + token.release(); + }) + .subscribe(); + ServerRSocketSession prevSession = sessions.remove(token); + if (prevSession != null) { + prevSession.dispose(); + } + sessions.put(token, session); + } + return session; + } + + @Nullable + public ServerRSocketSession get(ByteBuf resumeToken) { + return sessions.get(resumeToken); + } + + public void dispose() { + isDisposed = true; + sessions.values().forEach(ServerRSocketSession::dispose); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java b/rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java new file mode 100644 index 000000000..f010a05bd --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java @@ -0,0 +1,159 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.resume; + +import io.netty.buffer.ByteBuf; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Operators; +import reactor.util.concurrent.Queues; + +class UpstreamFramesSubscriber implements Subscriber, Disposable { + private static final Logger logger = LoggerFactory.getLogger(UpstreamFramesSubscriber.class); + + private final AtomicBoolean disposed = new AtomicBoolean(); + private final Consumer itemConsumer; + private final Disposable downstreamRequestDisposable; + private final Disposable resumeSaveStreamDisposable; + + private volatile Subscription subs; + private volatile boolean resumeStarted; + private final Queue framesCache; + private long request; + private long downStreamRequestN; + private long resumeSaveStreamRequestN; + + UpstreamFramesSubscriber( + int estimatedDownstreamRequest, + Flux downstreamRequests, + Flux resumeSaveStreamRequests, + Consumer itemConsumer) { + this.itemConsumer = itemConsumer; + this.framesCache = Queues.unbounded(estimatedDownstreamRequest).get(); + + downstreamRequestDisposable = downstreamRequests.subscribe(requestN -> requestN(0, requestN)); + + resumeSaveStreamDisposable = + resumeSaveStreamRequests.subscribe(requestN -> requestN(requestN, 0)); + } + + @Override + public void onSubscribe(Subscription s) { + this.subs = s; + if (!isDisposed()) { + doRequest(); + } else { + s.cancel(); + } + } + + @Override + public void onNext(ByteBuf item) { + processFrame(item); + } + + @Override + public void onError(Throwable t) { + dispose(); + } + + @Override + public void onComplete() { + dispose(); + } + + public void resumeStart() { + resumeStarted = true; + } + + public void resumeComplete() { + ByteBuf frame = framesCache.poll(); + while (frame != null) { + itemConsumer.accept(frame); + frame = framesCache.poll(); + } + resumeStarted = false; + doRequest(); + } + + @Override + public void dispose() { + if (disposed.compareAndSet(false, true)) { + releaseCache(); + if (subs != null) { + subs.cancel(); + } + resumeSaveStreamDisposable.dispose(); + downstreamRequestDisposable.dispose(); + } + } + + @Override + public boolean isDisposed() { + return disposed.get(); + } + + private void requestN(long resumeStreamRequest, long downStreamRequest) { + synchronized (this) { + downStreamRequestN = Operators.addCap(downStreamRequestN, downStreamRequest); + resumeSaveStreamRequestN = Operators.addCap(resumeSaveStreamRequestN, resumeStreamRequest); + + long requests = Math.min(downStreamRequestN, resumeSaveStreamRequestN); + if (requests > 0) { + downStreamRequestN -= requests; + resumeSaveStreamRequestN -= requests; + logger.debug("Upstream subscriber requestN: {}", requests); + request = Operators.addCap(request, requests); + } + } + doRequest(); + } + + private void doRequest() { + if (subs != null && !resumeStarted) { + synchronized (this) { + long r = request; + if (r > 0) { + subs.request(r); + request = 0; + } + } + } + } + + private void releaseCache() { + ByteBuf frame = framesCache.poll(); + while (frame != null && frame.refCnt() > 0) { + frame.release(); + } + } + + private void processFrame(ByteBuf item) { + if (resumeStarted) { + framesCache.offer(item); + } else { + itemConsumer.accept(item); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/package-info.java b/rsocket-core/src/main/java/io/rsocket/resume/package-info.java new file mode 100644 index 000000000..98744386a --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/package-info.java @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * Contains support classes for the RSocket resume capability. + * + * @see Resuming + * Operation + */ +@NonNullApi +package io.rsocket.resume; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/server/package-info.java b/rsocket-core/src/main/java/io/rsocket/server/package-info.java deleted file mode 100644 index f5a6faa15..000000000 --- a/rsocket-core/src/main/java/io/rsocket/server/package-info.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@javax.annotation.ParametersAreNonnullByDefault -package io.rsocket.server; diff --git a/rsocket-core/src/main/java/io/rsocket/transport/ClientTransport.java b/rsocket-core/src/main/java/io/rsocket/transport/ClientTransport.java index 7caee6299..3b8f624aa 100644 --- a/rsocket-core/src/main/java/io/rsocket/transport/ClientTransport.java +++ b/rsocket-core/src/main/java/io/rsocket/transport/ClientTransport.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -23,10 +23,9 @@ public interface ClientTransport extends Transport { /** - * Returns a {@code Publisher}, every subscription to which returns a single {@code - * DuplexConnection}. + * Return a {@code Mono} that connects for each subscriber. * - * @return {@code Publisher}, every subscription returns a single {@code DuplexConnection}. + * @since 1.0.1 */ Mono connect(); } diff --git a/rsocket-core/src/main/java/io/rsocket/transport/ServerTransport.java b/rsocket-core/src/main/java/io/rsocket/transport/ServerTransport.java index d89205540..92a9502a4 100644 --- a/rsocket-core/src/main/java/io/rsocket/transport/ServerTransport.java +++ b/rsocket-core/src/main/java/io/rsocket/transport/ServerTransport.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -26,10 +26,11 @@ public interface ServerTransport extends Transport { /** - * Starts this server. + * Start this server. * - * @param acceptor An acceptor to process a newly accepted {@code DuplexConnection} - * @return A handle to retrieve information about a started server. + * @param acceptor to process a newly accepted connections with + * @return A handle for information about and control over the server. + * @since 1.0.1 */ Mono start(ConnectionAcceptor acceptor); diff --git a/rsocket-core/src/main/java/io/rsocket/transport/Transport.java b/rsocket-core/src/main/java/io/rsocket/transport/Transport.java index efa997fb5..39386337c 100644 --- a/rsocket-core/src/main/java/io/rsocket/transport/Transport.java +++ b/rsocket-core/src/main/java/io/rsocket/transport/Transport.java @@ -1,20 +1,37 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + +import io.rsocket.DuplexConnection; + /** */ -public interface Transport {} +public interface Transport { + + /** + * Configurations that exposes the maximum frame size that a {@link DuplexConnection} can bring up + * to RSocket level. + * + *

This number should not exist the 16,777,215 (maximum frame size specified by RSocket spec) + * + * @return return maximum configured frame size limit + */ + default int maxFrameLength() { + return FRAME_LENGTH_MASK; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/transport/TransportHeaderAware.java b/rsocket-core/src/main/java/io/rsocket/transport/TransportHeaderAware.java new file mode 100644 index 000000000..16b863d9e --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/transport/TransportHeaderAware.java @@ -0,0 +1,38 @@ +/* + * 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.transport; + +import java.util.Map; +import java.util.function.Supplier; + +/** + * Extension interface to support Transports with headers at the transport layer, e.g. Websockets, + * Http2. + * + * @deprecated as of 1.0.1 in favor using properties on individual transports. + */ +@Deprecated +public interface TransportHeaderAware { + + /** + * Sets the transport headers + * + * @param transportHeaders the transport headers + * @throws NullPointerException if {@code transportHeaders} is {@code null} + */ + void setTransportHeaders(Supplier> transportHeaders); +} diff --git a/rsocket-core/src/main/java/io/rsocket/transport/package-info.java b/rsocket-core/src/main/java/io/rsocket/transport/package-info.java index af03823c1..00536122a 100644 --- a/rsocket-core/src/main/java/io/rsocket/transport/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/transport/package-info.java @@ -1,18 +1,21 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -@javax.annotation.ParametersAreNonnullByDefault +/** Client and server transport contracts for pluggable transports. */ +@NonNullApi package io.rsocket.transport; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/uri/UriHandler.java b/rsocket-core/src/main/java/io/rsocket/uri/UriHandler.java deleted file mode 100644 index 05d0c4add..000000000 --- a/rsocket-core/src/main/java/io/rsocket/uri/UriHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.uri; - -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import java.net.URI; -import java.util.Optional; -import java.util.ServiceLoader; - -/** - * URI to {@link ClientTransport} or {@link ServerTransport}. Should return a non empty value only - * when the URI is unambiguously mapped to a particular transport, either by a standardised - * implementation or via some flag in the URI to indicate a choice. - */ -public interface UriHandler { - static ServiceLoader loadServices() { - return ServiceLoader.load(UriHandler.class); - } - - default Optional buildClient(URI uri) { - return Optional.empty(); - } - - default Optional buildServer(URI uri) { - return Optional.empty(); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/uri/UriTransportRegistry.java b/rsocket-core/src/main/java/io/rsocket/uri/UriTransportRegistry.java deleted file mode 100644 index 276c87310..000000000 --- a/rsocket-core/src/main/java/io/rsocket/uri/UriTransportRegistry.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.uri; - -import static io.rsocket.uri.UriHandler.loadServices; - -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.ServiceLoader; -import reactor.core.publisher.Mono; - -/** - * Registry for looking up transports by URI. - * - *

Uses the Jar Services mechanism with services defined by {@link UriHandler}. - */ -public class UriTransportRegistry { - private static final ClientTransport FAILED_CLIENT_LOOKUP = - () -> Mono.error(new UnsupportedOperationException()); - private static final ServerTransport FAILED_SERVER_LOOKUP = - acceptor -> Mono.error(new UnsupportedOperationException()); - - private List handlers; - - public UriTransportRegistry(ServiceLoader services) { - handlers = new ArrayList<>(); - services.forEach(handlers::add); - } - - public static UriTransportRegistry fromServices() { - ServiceLoader services = loadServices(); - - return new UriTransportRegistry(services); - } - - public static ClientTransport clientForUri(String uri) { - return UriTransportRegistry.fromServices().findClient(uri); - } - - private ClientTransport findClient(String uriString) { - URI uri = URI.create(uriString); - - for (UriHandler h : handlers) { - Optional r = h.buildClient(uri); - if (r.isPresent()) { - return r.get(); - } - } - - return FAILED_CLIENT_LOOKUP; - } - - public static ServerTransport serverForUri(String uri) { - return UriTransportRegistry.fromServices().findServer(uri); - } - - private ServerTransport findServer(String uriString) { - URI uri = URI.create(uriString); - - for (UriHandler h : handlers) { - Optional r = h.buildServer(uri); - if (r.isPresent()) { - return r.get(); - } - } - - return FAILED_SERVER_LOOKUP; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java b/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java new file mode 100644 index 000000000..4cf33fa86 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java @@ -0,0 +1,219 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.util; + +import io.netty.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.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import io.rsocket.Payload; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import reactor.util.annotation.Nullable; + +public final class ByteBufPayload extends AbstractReferenceCounted implements Payload { + private static final Recycler RECYCLER = + new Recycler() { + protected ByteBufPayload newObject(Handle handle) { + return new ByteBufPayload(handle); + } + }; + + private final Handle handle; + private ByteBuf data; + private ByteBuf metadata; + + private ByteBufPayload(final Handle handle) { + this.handle = handle; + } + + /** + * 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(ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, 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( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, data), + metadata == null ? null : ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, metadata)); + } + + public static Payload create(CharSequence data, Charset dataCharset) { + return create( + ByteBufUtil.encodeString(ByteBufAllocator.DEFAULT, CharBuffer.wrap(data), dataCharset), + null); + } + + public static Payload create( + CharSequence data, + Charset dataCharset, + @Nullable CharSequence metadata, + Charset metadataCharset) { + return create( + ByteBufUtil.encodeString(ByteBufAllocator.DEFAULT, CharBuffer.wrap(data), dataCharset), + metadata == null + ? null + : ByteBufUtil.encodeString( + ByteBufAllocator.DEFAULT, CharBuffer.wrap(metadata), metadataCharset)); + } + + public static Payload create(byte[] data) { + return create(Unpooled.wrappedBuffer(data), null); + } + + public static Payload create(byte[] data, @Nullable byte[] metadata) { + return create( + Unpooled.wrappedBuffer(data), metadata == null ? null : Unpooled.wrappedBuffer(metadata)); + } + + public static Payload create(ByteBuffer data) { + return create(Unpooled.wrappedBuffer(data), null); + } + + public static Payload create(ByteBuffer data, @Nullable ByteBuffer metadata) { + return create( + Unpooled.wrappedBuffer(data), metadata == null ? null : Unpooled.wrappedBuffer(metadata)); + } + + public static Payload create(ByteBuf data) { + return create(data, null); + } + + public static Payload create(ByteBuf data, @Nullable ByteBuf metadata) { + ByteBufPayload payload = RECYCLER.get(); + payload.data = data; + payload.metadata = metadata; + // unsure data and metadata is set before refCnt change + payload.setRefCnt(1); + return payload; + } + + public static Payload create(Payload payload) { + return create( + payload.sliceData().retain(), + payload.hasMetadata() ? payload.sliceMetadata().retain() : null); + } + + @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 ByteBufPayload retain() { + super.retain(); + return this; + } + + @Override + public ByteBufPayload retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public ByteBufPayload touch() { + ensureAccessible(); + data.touch(); + if (metadata != null) { + metadata.touch(); + } + return this; + } + + @Override + public ByteBufPayload touch(Object hint) { + ensureAccessible(); + data.touch(hint); + if (metadata != null) { + metadata.touch(hint); + } + return this; + } + + @Override + protected void deallocate() { + data.release(); + data = null; + if (metadata != null) { + metadata.release(); + metadata = null; + } + handle.recycle(this); + } + + /** + * Should be called by every method that tries to access the buffers content to check if the + * buffer was released before. + */ + void ensureAccessible() { + if (!isAccessible()) { + throw new IllegalReferenceCountException(0); + } + } + + /** + * Used internally by {@link ByteBufPayload#ensureAccessible()} to try to guard against using the + * buffer after it was released (best-effort). + */ + boolean isAccessible() { + return refCnt() != 0; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java b/rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java new file mode 100644 index 000000000..328fb8435 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java @@ -0,0 +1,210 @@ +package io.rsocket.util; + +import static io.netty.util.internal.StringUtil.isSurrogate; + +import io.netty.buffer.ByteBuf; +import io.netty.util.CharsetUtil; +import io.netty.util.internal.MathUtil; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.util.Arrays; + +public class CharByteBufUtil { + + private static final byte WRITE_UTF_UNKNOWN = (byte) '?'; + + private CharByteBufUtil() {} + + /** + * Returns the exact bytes length of UTF8 character sequence. + * + *

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

This method returns the actual number of bytes written. + */ + public static int writeUtf8(ByteBuf buf, char[] seq) { + return writeUtf8(buf, seq, 0, seq.length); + } + + /** + * Equivalent to {@link #writeUtf8(ByteBuf, char[]) writeUtf8(buf, seq.subSequence(start, end), + * reserveBytes)} but avoids subsequence object allocation if possible. + * + * @return actual number of bytes written + */ + public static int writeUtf8(ByteBuf buf, char[] seq, int start, int end) { + return writeUtf8(buf, buf.writerIndex(), checkCharSequenceBounds(seq, start, end), start, end); + } + + // Fast-Path implementation + static int writeUtf8(ByteBuf buffer, int writerIndex, char[] seq, int start, int end) { + int oldWriterIndex = writerIndex; + + // We can use the _set methods as these not need to do any index checks and reference checks. + // This is possible as we called ensureWritable(...) before. + for (int i = start; i < end; i++) { + char c = seq[i]; + if (c < 0x80) { + buffer.setByte(writerIndex++, (byte) c); + } else if (c < 0x800) { + buffer.setByte(writerIndex++, (byte) (0xc0 | (c >> 6))); + buffer.setByte(writerIndex++, (byte) (0x80 | (c & 0x3f))); + } else if (isSurrogate(c)) { + if (!Character.isHighSurrogate(c)) { + buffer.setByte(writerIndex++, WRITE_UTF_UNKNOWN); + continue; + } + final char c2; + if (seq.length > ++i) { + // Surrogate Pair consumes 2 characters. Optimistically try to get the next character to + // avoid + // duplicate bounds checking with charAt. If an IndexOutOfBoundsException is thrown we + // will + // re-throw a more informative exception describing the problem. + c2 = seq[i]; + } else { + buffer.setByte(writerIndex++, WRITE_UTF_UNKNOWN); + break; + } + // Extra method to allow inlining the rest of writeUtf8 which is the most likely code path. + writerIndex = writeUtf8Surrogate(buffer, writerIndex, c, c2); + } else { + buffer.setByte(writerIndex++, (byte) (0xe0 | (c >> 12))); + buffer.setByte(writerIndex++, (byte) (0x80 | ((c >> 6) & 0x3f))); + buffer.setByte(writerIndex++, (byte) (0x80 | (c & 0x3f))); + } + } + buffer.writerIndex(writerIndex); + return writerIndex - oldWriterIndex; + } + + private static int writeUtf8Surrogate(ByteBuf buffer, int writerIndex, char c, char c2) { + if (!Character.isLowSurrogate(c2)) { + buffer.setByte(writerIndex++, WRITE_UTF_UNKNOWN); + buffer.setByte(writerIndex++, Character.isHighSurrogate(c2) ? WRITE_UTF_UNKNOWN : c2); + return writerIndex; + } + int codePoint = Character.toCodePoint(c, c2); + // See http://www.unicode.org/versions/Unicode7.0.0/ch03.pdf#G2630. + buffer.setByte(writerIndex++, (byte) (0xf0 | (codePoint >> 18))); + buffer.setByte(writerIndex++, (byte) (0x80 | ((codePoint >> 12) & 0x3f))); + buffer.setByte(writerIndex++, (byte) (0x80 | ((codePoint >> 6) & 0x3f))); + buffer.setByte(writerIndex++, (byte) (0x80 | (codePoint & 0x3f))); + return writerIndex; + } + + public static char[] readUtf8(ByteBuf byteBuf, int length) { + CharsetDecoder charsetDecoder = CharsetUtil.UTF_8.newDecoder(); + int en = (int) (length * (double) charsetDecoder.maxCharsPerByte()); + char[] ca = new char[en]; + + CharBuffer charBuffer = CharBuffer.wrap(ca); + ByteBuffer byteBuffer = + byteBuf.nioBufferCount() == 1 + ? byteBuf.internalNioBuffer(byteBuf.readerIndex(), length) + : byteBuf.nioBuffer(byteBuf.readerIndex(), length); + byteBuffer.mark(); + try { + CoderResult cr = charsetDecoder.decode(byteBuffer, charBuffer, true); + if (!cr.isUnderflow()) cr.throwException(); + cr = charsetDecoder.flush(charBuffer); + if (!cr.isUnderflow()) cr.throwException(); + + byteBuffer.reset(); + byteBuf.skipBytes(length); + + return safeTrim(charBuffer.array(), charBuffer.position()); + } catch (CharacterCodingException x) { + // Substitution is always enabled, + // so this shouldn't happen + throw new IllegalStateException("unable to decode char array from the given buffer", x); + } + } + + private static char[] safeTrim(char[] ca, int len) { + if (len == ca.length) return ca; + else return Arrays.copyOf(ca, len); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/util/Clock.java b/rsocket-core/src/main/java/io/rsocket/util/Clock.java index 9b7704554..4a34c988f 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/Clock.java +++ b/rsocket-core/src/main/java/io/rsocket/util/Clock.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.util; import java.util.concurrent.TimeUnit; diff --git a/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java b/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java new file mode 100644 index 000000000..08b8b2fb7 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java @@ -0,0 +1,194 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.util; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.rsocket.Payload; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import reactor.util.annotation.Nullable; + +/** + * An implementation of {@link Payload}. This implementation is not thread-safe, and hence + * any method can not be invoked concurrently. + */ +public final class DefaultPayload implements Payload { + public static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0); + + private final ByteBuffer data; + private final ByteBuffer metadata; + + private DefaultPayload(ByteBuffer data, @Nullable ByteBuffer metadata) { + this.data = data; + this.metadata = metadata; + } + + /** + * Static factory method for a text payload. Mainly looks better than "new DefaultPayload(data)" + * + * @param data the data of the payload. + * @return a payload. + */ + public static Payload create(CharSequence data) { + return create(StandardCharsets.UTF_8.encode(CharBuffer.wrap(data)), null); + } + + /** + * Static factory method for a text payload. Mainly looks better than "new DefaultPayload(data, + * metadata)" + * + * @param data the data of the payload. + * @param metadata the metadata for the payload. + * @return a payload. + */ + public static Payload create(CharSequence data, @Nullable CharSequence metadata) { + return create( + StandardCharsets.UTF_8.encode(CharBuffer.wrap(data)), + metadata == null ? null : StandardCharsets.UTF_8.encode(CharBuffer.wrap(metadata))); + } + + public static Payload create(CharSequence data, Charset dataCharset) { + return create(dataCharset.encode(CharBuffer.wrap(data)), null); + } + + public static Payload create( + CharSequence data, + Charset dataCharset, + @Nullable CharSequence metadata, + Charset metadataCharset) { + return create( + dataCharset.encode(CharBuffer.wrap(data)), + metadata == null ? null : metadataCharset.encode(CharBuffer.wrap(metadata))); + } + + public static Payload create(byte[] data) { + return create(ByteBuffer.wrap(data), null); + } + + public static Payload create(byte[] data, @Nullable byte[] metadata) { + return create(ByteBuffer.wrap(data), metadata == null ? null : ByteBuffer.wrap(metadata)); + } + + public static Payload create(ByteBuffer data) { + return create(data, null); + } + + public static Payload create(ByteBuffer data, @Nullable ByteBuffer metadata) { + return new DefaultPayload(data, metadata); + } + + public static Payload create(ByteBuf data) { + return create(data, null); + } + + public static Payload create(ByteBuf data, @Nullable ByteBuf metadata) { + try { + return create(toBytes(data), metadata != null ? toBytes(metadata) : null); + } finally { + data.release(); + if (metadata != null) { + metadata.release(); + } + } + } + + public static Payload create(Payload payload) { + return create( + toBytes(payload.data()), payload.hasMetadata() ? toBytes(payload.metadata()) : null); + } + + private static byte[] toBytes(ByteBuf byteBuf) { + byte[] bytes = new byte[byteBuf.readableBytes()]; + byteBuf.markReaderIndex(); + byteBuf.readBytes(bytes); + byteBuf.resetReaderIndex(); + return bytes; + } + + @Override + public boolean hasMetadata() { + return metadata != null; + } + + @Override + public ByteBuf sliceMetadata() { + return metadata == null ? Unpooled.EMPTY_BUFFER : Unpooled.wrappedBuffer(metadata); + } + + @Override + public ByteBuf sliceData() { + return Unpooled.wrappedBuffer(data); + } + + @Override + public ByteBuffer getMetadata() { + return metadata == null ? DefaultPayload.EMPTY_BUFFER : metadata.duplicate(); + } + + @Override + public ByteBuffer getData() { + return data.duplicate(); + } + + @Override + public ByteBuf data() { + return sliceData(); + } + + @Override + public ByteBuf metadata() { + return sliceMetadata(); + } + + @Override + public int refCnt() { + return 1; + } + + @Override + public DefaultPayload retain() { + return this; + } + + @Override + public DefaultPayload retain(int increment) { + return this; + } + + @Override + public DefaultPayload touch() { + return this; + } + + @Override + public DefaultPayload touch(Object hint) { + return this; + } + + @Override + public boolean release() { + return false; + } + + @Override + public boolean release(int decrement) { + return false; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/util/EmptyPayload.java b/rsocket-core/src/main/java/io/rsocket/util/EmptyPayload.java new file mode 100644 index 000000000..99df97d70 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/EmptyPayload.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.util; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.rsocket.Payload; + +public class EmptyPayload implements Payload { + public static final EmptyPayload INSTANCE = new EmptyPayload(); + + private EmptyPayload() {} + + @Override + public boolean hasMetadata() { + return false; + } + + @Override + public ByteBuf sliceMetadata() { + return Unpooled.EMPTY_BUFFER; + } + + @Override + public ByteBuf sliceData() { + return Unpooled.EMPTY_BUFFER; + } + + @Override + public ByteBuf data() { + return sliceData(); + } + + @Override + public ByteBuf metadata() { + return sliceMetadata(); + } + + @Override + public int refCnt() { + return 1; + } + + @Override + public EmptyPayload retain() { + return this; + } + + @Override + public EmptyPayload retain(int increment) { + return this; + } + + @Override + public EmptyPayload touch() { + return this; + } + + @Override + public EmptyPayload touch(Object hint) { + return this; + } + + @Override + public boolean release() { + return false; + } + + @Override + public boolean release(int decrement) { + return false; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java b/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java new file mode 100644 index 000000000..3ff720447 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java @@ -0,0 +1,164 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.util; + +import io.netty.buffer.ByteBuf; +import java.util.Objects; + +public final class NumberUtils { + + /** The size of a medium in {@code byte}s. */ + public static final int MEDIUM_BYTES = 3; + + private static final int UNSIGNED_BYTE_SIZE = 8; + + private static final int UNSIGNED_BYTE_MAX_VALUE = (1 << UNSIGNED_BYTE_SIZE) - 1; + + private static final int UNSIGNED_MEDIUM_SIZE = 24; + + private static final int UNSIGNED_MEDIUM_MAX_VALUE = (1 << UNSIGNED_MEDIUM_SIZE) - 1; + + private static final int UNSIGNED_SHORT_SIZE = 16; + + private static final int UNSIGNED_SHORT_MAX_VALUE = (1 << UNSIGNED_SHORT_SIZE) - 1; + + private NumberUtils() {} + + /** + * Requires that an {@code int} is greater than or equal to zero. + * + * @param i the {@code int} to test + * @param message detail message to be used in the event that a {@link IllegalArgumentException} + * is thrown + * @return the {@code int} if greater than or equal to zero + * @throws IllegalArgumentException if {@code i} is less than zero + */ + public static int requireNonNegative(int i, String message) { + Objects.requireNonNull(message, "message must not be null"); + + if (i < 0) { + throw new IllegalArgumentException(message); + } + + return i; + } + + /** + * Requires that a {@code long} is greater than zero. + * + * @param l the {@code long} to test + * @param message detail message to be used in the event that a {@link IllegalArgumentException} + * is thrown + * @return the {@code long} if greater than zero + * @throws IllegalArgumentException if {@code l} is less than or equal to zero + */ + public static long requirePositive(long l, String message) { + Objects.requireNonNull(message, "message must not be null"); + + if (l <= 0) { + throw new IllegalArgumentException(message); + } + + return l; + } + + /** + * Requires that an {@code int} is greater than zero. + * + * @param i the {@code int} to test + * @param message detail message to be used in the event that a {@link IllegalArgumentException} + * is thrown + * @return the {@code int} if greater than zero + * @throws IllegalArgumentException if {@code i} is less than or equal to zero + */ + public static int requirePositive(int i, String message) { + Objects.requireNonNull(message, "message must not be null"); + + if (i <= 0) { + throw new IllegalArgumentException(message); + } + + return i; + } + + /** + * Requires that an {@code int} can be represented as an unsigned {@code byte}. + * + * @param i the {@code int} to test + * @return the {@code int} if it can be represented as an unsigned {@code byte} + * @throws IllegalArgumentException if {@code i} cannot be represented as an unsigned {@code byte} + */ + public static int requireUnsignedByte(int i) { + if (i > UNSIGNED_BYTE_MAX_VALUE) { + throw new IllegalArgumentException( + String.format("%d is larger than %d bits", i, UNSIGNED_BYTE_SIZE)); + } + + return i; + } + + /** + * Requires that an {@code int} can be represented as an unsigned {@code medium}. + * + * @param i the {@code int} to test + * @return the {@code int} if it can be represented as an unsigned {@code medium} + * @throws IllegalArgumentException if {@code i} cannot be represented as an unsigned {@code + * medium} + */ + public static int requireUnsignedMedium(int i) { + if (i > UNSIGNED_MEDIUM_MAX_VALUE) { + throw new IllegalArgumentException( + String.format("%d is larger than %d bits", i, UNSIGNED_MEDIUM_SIZE)); + } + + return i; + } + + /** + * Requires that an {@code int} can be represented as an unsigned {@code short}. + * + * @param i the {@code int} to test + * @return the {@code int} if it can be represented as an unsigned {@code short} + * @throws IllegalArgumentException if {@code i} cannot be represented as an unsigned {@code + * short} + */ + public static int requireUnsignedShort(int i) { + if (i > UNSIGNED_SHORT_MAX_VALUE) { + throw new IllegalArgumentException( + String.format("%d is larger than %d bits", i, UNSIGNED_SHORT_SIZE)); + } + + return i; + } + + /** + * Encode an unsigned medium integer on 3 bytes / 24 bits. This can be decoded directly by the + * {@link ByteBuf#readUnsignedMedium()} method. + * + * @param byteBuf the {@link ByteBuf} into which to write the bits + * @param i the medium integer to encode + * @see #requireUnsignedMedium(int) + */ + public static void encodeUnsignedMedium(ByteBuf byteBuf, int i) { + requireUnsignedMedium(i); + // Write each byte separately in reverse order, this mean we can write 1 << 23 without + // overflowing. + byteBuf.writeByte(i >> 16); + byteBuf.writeByte(i >> 8); + byteBuf.writeByte(i); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/util/PayloadImpl.java b/rsocket-core/src/main/java/io/rsocket/util/PayloadImpl.java deleted file mode 100644 index ce7c76025..000000000 --- a/rsocket-core/src/main/java/io/rsocket/util/PayloadImpl.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.util; - -import io.rsocket.Frame; -import io.rsocket.Payload; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; - -/** - * An implementation of {@link Payload}. This implementation is not thread-safe, and hence - * any method can not be invoked concurrently. - */ -public class PayloadImpl implements Payload { - - public static final PayloadImpl EMPTY = - new PayloadImpl(Frame.NULL_BYTEBUFFER, Frame.NULL_BYTEBUFFER, false); - - private final ByteBuffer data; - private final ByteBuffer metadata; - private final int dataStartPosition; - private final int metadataStartPosition; - private final boolean reusable; - - public PayloadImpl(Frame frame) { - this(frame.getData(), frame.getMetadata()); - } - - public PayloadImpl(String data) { - this(data, Charset.defaultCharset()); - } - - public PayloadImpl(String data, String metadata) { - this(data, StandardCharsets.UTF_8, metadata, StandardCharsets.UTF_8); - } - - public PayloadImpl(String data, Charset dataCharset) { - this(dataCharset.encode(data), Frame.NULL_BYTEBUFFER); - } - - public PayloadImpl(String data, Charset dataCharset, String metadata, Charset metaDataCharset) { - this(dataCharset.encode(data), metaDataCharset.encode(metadata)); - } - - public PayloadImpl(byte[] data) { - this(ByteBuffer.wrap(data), Frame.NULL_BYTEBUFFER); - } - - public PayloadImpl(byte[] data, byte[] metadata) { - this(ByteBuffer.wrap(data), ByteBuffer.wrap(metadata)); - } - - public PayloadImpl(ByteBuffer data) { - this(data, Frame.NULL_BYTEBUFFER); - } - - public PayloadImpl(ByteBuffer data, ByteBuffer metadata) { - this(data, metadata, true); - } - - public PayloadImpl(ByteBuffer data, ByteBuffer metadata, boolean reusable) { - this.data = data; - this.metadata = metadata; - this.reusable = reusable; - this.dataStartPosition = reusable ? this.data.position() : 0; - this.metadataStartPosition = reusable ? this.metadata.position() : 0; - } - - @Override - public ByteBuffer getData() { - if (reusable) { - data.position(dataStartPosition); - } - return data; - } - - @Override - public ByteBuffer getMetadata() { - if (reusable) { - metadata.position(metadataStartPosition); - } - return metadata; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/util/RSocketProxy.java b/rsocket-core/src/main/java/io/rsocket/util/RSocketProxy.java index 29aefa742..518b727c1 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/RSocketProxy.java +++ b/rsocket-core/src/main/java/io/rsocket/util/RSocketProxy.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.util; import io.rsocket.Payload; @@ -60,8 +61,13 @@ public double availability() { } @Override - public Mono close() { - return source.close(); + public void dispose() { + source.dispose(); + } + + @Override + public boolean isDisposed() { + return source.isDisposed(); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/util/package-info.java b/rsocket-core/src/main/java/io/rsocket/util/package-info.java index 435eef5c1..2fac3327f 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/util/package-info.java @@ -1,18 +1,21 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -@javax.annotation.ParametersAreNonnullByDefault +/** Shared utility classes and {@link io.rsocket.Payload} implementations. */ +@NonNullApi package io.rsocket.util; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/test/java/io/rsocket/FrameTest.java b/rsocket-core/src/test/java/io/rsocket/FrameTest.java new file mode 100644 index 000000000..82af5f53c --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/FrameTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket; + +public class FrameTest { + /*@Test + public void testFrameToString() { + final io.rsocket.Frame requestFrame = + io.rsocket.Frame.Request.from( + 1, FrameType.REQUEST_RESPONSE, DefaultPayload.create("streaming in -> 0"), 1); + assertEquals( + "Frame => Stream ID: 1 Type: REQUEST_RESPONSE Payload: data: \"streaming in -> 0\" ", + requestFrame.toString()); + } + + @Test + public void testFrameWithMetadataToString() { + final io.rsocket.Frame requestFrame = + io.rsocket.Frame.Request.from( + 1, + FrameType.REQUEST_RESPONSE, + DefaultPayload.create("streaming in -> 0", "metadata"), + 1); + assertEquals( + "Frame => Stream ID: 1 Type: REQUEST_RESPONSE Payload: metadata: \"metadata\" data: \"streaming in -> 0\" ", + requestFrame.toString()); + } + + @Test + public void testPayload() { + io.rsocket.Frame frame = + io.rsocket.Frame.PayloadFrame.from( + 1, + FrameType.NEXT_COMPLETE, + DefaultPayload.create("Hello"), + FrameHeaderFlyweight.FLAGS_C); + frame.toString(); + }*/ +} diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java deleted file mode 100644 index 328bb8a7e..000000000 --- a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import static io.rsocket.FrameType.CANCEL; -import static io.rsocket.FrameType.KEEPALIVE; -import static io.rsocket.FrameType.NEXT_COMPLETE; -import static io.rsocket.FrameType.REQUEST_RESPONSE; -import static io.rsocket.test.util.TestSubscriber.anyPayload; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.verify; - -import io.rsocket.exceptions.ApplicationException; -import io.rsocket.exceptions.RejectedSetupException; -import io.rsocket.test.util.TestSubscriber; -import io.rsocket.util.PayloadImpl; -import java.util.ArrayList; -import java.util.List; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.publisher.DirectProcessor; -import reactor.core.publisher.Mono; - -public class RSocketClientTest { - - @Rule public final ClientSocketRule rule = new ClientSocketRule(); - - /** TODO reenable */ - @Test(timeout = 2_000) - @Ignore - public void testKeepAlive() throws Exception { - rule.keepAliveTicks.onNext(1L); - assertThat("Unexpected frame sent.", rule.connection.awaitSend().getType(), is(KEEPALIVE)); - } - - @Test(timeout = 2_000) - public void testInvalidFrameOnStream0() throws Throwable { - rule.connection.addToReceivedBuffer(Frame.RequestN.from(0, 10)); - assertThat("Unexpected errors.", rule.errors, hasSize(1)); - assertThat( - "Unexpected error received.", - rule.errors, - contains(instanceOf(IllegalStateException.class))); - } - - @Test(timeout = 2_000) - public void testHandleSetupException() throws Throwable { - rule.connection.addToReceivedBuffer(Frame.Error.from(0, new RejectedSetupException("boom"))); - assertThat("Unexpected errors.", rule.errors, hasSize(1)); - assertThat( - "Unexpected error received.", - rule.errors, - contains(instanceOf(RejectedSetupException.class))); - } - - @Test(timeout = 2_000) - public void testHandleApplicationException() throws Throwable { - rule.connection.clearSendReceiveBuffers(); - Publisher response = rule.socket.requestResponse(PayloadImpl.EMPTY); - Subscriber responseSub = TestSubscriber.create(); - response.subscribe(responseSub); - - int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); - rule.connection.addToReceivedBuffer( - Frame.Error.from(streamId, new ApplicationException(PayloadImpl.EMPTY))); - - verify(responseSub).onError(any(ApplicationException.class)); - } - - @Test(timeout = 2_000) - public void testHandleValidFrame() throws Throwable { - Publisher response = rule.socket.requestResponse(PayloadImpl.EMPTY); - Subscriber sub = TestSubscriber.create(); - response.subscribe(sub); - - int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); - rule.connection.addToReceivedBuffer( - Frame.PayloadFrame.from(streamId, NEXT_COMPLETE, PayloadImpl.EMPTY)); - - verify(sub).onNext(anyPayload()); - verify(sub).onComplete(); - } - - /** - * TODO test case shows a bug where a 0 initial request, that was cancelled is causing a network - * call. - */ - @Test(timeout = 2_000) - @Ignore - public void testRequestReplyWithCancel() throws Throwable { - rule.connection.clearSendReceiveBuffers(); // clear setup frame - Publisher response = rule.socket.requestResponse(PayloadImpl.EMPTY); - Subscriber sub = TestSubscriber.createCancelling(); - response.subscribe(sub); - - verify(sub).onSubscribe(any(Subscription.class)); - - assertThat( - "Unexpected frame sent on the connection.", - rule.connection.awaitSend().getType(), - is(REQUEST_RESPONSE)); - assertThat( - "Unexpected frame sent on the connection.", - rule.connection.awaitSend().getType(), - is(CANCEL)); - } - - @Test(timeout = 2_000) - public void testRequestReplyErrorOnSend() throws Throwable { - rule.connection.setAvailability(0); // Fails send - Mono response = rule.socket.requestResponse(PayloadImpl.EMPTY); - Subscriber responseSub = TestSubscriber.create(); - response.subscribe(responseSub); - - verify(responseSub).onError(any(RuntimeException.class)); - } - - @Test - public void testLazyRequestResponse() throws Exception { - Publisher response = rule.socket.requestResponse(PayloadImpl.EMPTY); - int streamId = sendRequestResponse(response); - rule.connection.clearSendReceiveBuffers(); - int streamId2 = sendRequestResponse(response); - assertThat("Stream ID reused.", streamId2, not(equalTo(streamId))); - } - - public int sendRequestResponse(Publisher response) { - Subscriber sub = TestSubscriber.create(); - response.subscribe(sub); - int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); - rule.connection.addToReceivedBuffer( - Frame.PayloadFrame.from(streamId, NEXT_COMPLETE, PayloadImpl.EMPTY)); - verify(sub).onNext(anyPayload()); - verify(sub).onComplete(); - return streamId; - } - - public static class ClientSocketRule extends AbstractSocketRule { - - private final DirectProcessor keepAliveTicks = DirectProcessor.create(); - - @Override - protected RSocketClient newRSocket() { - return new RSocketClient( - connection, throwable -> errors.add(throwable), StreamIdSupplier.clientSupplier()); - } - - public int getStreamIdForRequestType(FrameType expectedFrameType) { - assertThat("Unexpected frames sent.", connection.getSent(), hasSize(greaterThanOrEqualTo(1))); - List framesFound = new ArrayList<>(); - for (Frame frame : connection.getSent()) { - if (frame.getType() == expectedFrameType) { - return frame.getStreamId(); - } - framesFound.add(frame.getType()); - } - throw new AssertionError( - "No frames sent with frame type: " - + expectedFrameType - + ", frames found: " - + framesFound); - } - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketFactoryTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketFactoryTest.java deleted file mode 100644 index 7d585fb55..000000000 --- a/rsocket-core/src/test/java/io/rsocket/RSocketFactoryTest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import static org.junit.Assert.fail; - -import org.junit.Ignore; -import org.junit.Test; - -public class RSocketFactoryTest { - - @Test - @Ignore - public void testPluginsAppliedToClient() { - // TODO implement test - fail(); - } - - @Test - @Ignore - public void testPluginsAppliedToServer() { - // TODO implement test - fail(); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java deleted file mode 100644 index f7fac327f..000000000 --- a/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.anyOf; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; - -import io.netty.buffer.Unpooled; -import io.rsocket.test.util.TestDuplexConnection; -import io.rsocket.test.util.TestSubscriber; -import io.rsocket.util.PayloadImpl; -import java.util.Collection; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.Rule; -import org.junit.Test; -import org.reactivestreams.Subscriber; -import reactor.core.publisher.Mono; - -public class RSocketServerTest { - - @Rule public final ServerSocketRule rule = new ServerSocketRule(); - - @Test(timeout = 2000) - public void testHandleKeepAlive() throws Exception { - rule.connection.addToReceivedBuffer(Frame.Keepalive.from(Unpooled.EMPTY_BUFFER, true)); - Frame sent = rule.connection.awaitSend(); - assertThat("Unexpected frame sent.", sent.getType(), is(FrameType.KEEPALIVE)); - /*Keep alive ack must not have respond flag else, it will result in infinite ping-pong of keep alive frames.*/ - assertThat( - "Unexpected keep-alive frame respond flag.", - Frame.Keepalive.hasRespondFlag(sent), - is(false)); - } - - @Test(timeout = 2000) - public void testHandleResponseFrameNoError() throws Exception { - final int streamId = 4; - rule.connection.clearSendReceiveBuffers(); - - rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE); - - Collection> sendSubscribers = rule.connection.getSendSubscribers(); - assertThat("Request not sent.", sendSubscribers, hasSize(1)); - assertThat("Unexpected error.", rule.errors, is(empty())); - Subscriber sendSub = sendSubscribers.iterator().next(); - assertThat( - "Unexpected frame sent.", - rule.connection.awaitSend().getType(), - anyOf(is(FrameType.COMPLETE), is(FrameType.NEXT_COMPLETE))); - } - - @Test(timeout = 2000) - public void testHandlerEmitsError() throws Exception { - final int streamId = 4; - rule.sendRequest(streamId, FrameType.REQUEST_STREAM); - assertThat("Unexpected error.", rule.errors, is(empty())); - assertThat( - "Unexpected frame sent.", rule.connection.awaitSend().getType(), is(FrameType.ERROR)); - } - - @Test(timeout = 2_0000) - public void testCancel() throws Exception { - final int streamId = 4; - final AtomicBoolean cancelled = new AtomicBoolean(); - rule.setAcceptingSocket( - new AbstractRSocket() { - @Override - public Mono requestResponse(Payload payload) { - return Mono.never().doOnCancel(() -> cancelled.set(true)); - } - }); - rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE); - - assertThat("Unexpected error.", rule.errors, is(empty())); - assertThat("Unexpected frame sent.", rule.connection.getSent(), is(empty())); - - rule.connection.addToReceivedBuffer(Frame.Cancel.from(streamId)); - assertThat("Unexpected frame sent.", rule.connection.getSent(), is(empty())); - assertThat("Subscription not cancelled.", cancelled.get(), is(true)); - } - - public static class ServerSocketRule extends AbstractSocketRule { - - private RSocket acceptingSocket; - - @Override - protected void init() { - acceptingSocket = - new AbstractRSocket() { - @Override - public Mono requestResponse(Payload payload) { - return Mono.just(payload); - } - }; - super.init(); - } - - public void setAcceptingSocket(RSocket acceptingSocket) { - this.acceptingSocket = acceptingSocket; - connection = new TestDuplexConnection(); - connectSub = TestSubscriber.create(); - errors = new ConcurrentLinkedQueue<>(); - super.init(); - } - - @Override - protected RSocketServer newRSocket() { - return new RSocketServer(connection, acceptingSocket, throwable -> errors.add(throwable)); - } - - private void sendRequest(int streamId, FrameType frameType) { - Frame request = Frame.Request.from(streamId, frameType, PayloadImpl.EMPTY, 1); - connection.addToReceivedBuffer(request); - connection.addToReceivedBuffer(Frame.RequestN.from(streamId, 2)); - } - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketTest.java deleted file mode 100644 index 2705986b5..000000000 --- a/rsocket-core/src/test/java/io/rsocket/RSocketTest.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.is; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.verify; - -import io.rsocket.exceptions.InvalidRequestException; -import io.rsocket.test.util.LocalDuplexConnection; -import io.rsocket.test.util.TestSubscriber; -import io.rsocket.util.PayloadImpl; -import java.util.ArrayList; -import java.util.concurrent.CountDownLatch; -import org.hamcrest.MatcherAssert; -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.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import reactor.core.publisher.DirectProcessor; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -public class RSocketTest { - - @Rule public final SocketRule rule = new SocketRule(); - - @Test(timeout = 2_000) - public void testRequestReplyNoError() { - Subscriber subscriber = TestSubscriber.create(); - rule.crs.requestResponse(new PayloadImpl("hello")).subscribe(subscriber); - verify(subscriber).onNext(TestSubscriber.anyPayload()); - verify(subscriber).onComplete(); - rule.assertNoErrors(); - } - - @Test(timeout = 2000) - public void testHandlerEmitsError() { - rule.setRequestAcceptor( - new AbstractRSocket() { - @Override - public Mono requestResponse(Payload payload) { - return Mono.error(new NullPointerException("Deliberate exception.")); - } - }); - Subscriber subscriber = TestSubscriber.create(); - rule.crs.requestResponse(PayloadImpl.EMPTY).subscribe(subscriber); - verify(subscriber).onError(any(InvalidRequestException.class)); - rule.assertNoErrors(); - } - - @Test - public void testChannel() throws Exception { - CountDownLatch latch = new CountDownLatch(10); - Flux requests = Flux.range(0, 10).map(i -> new PayloadImpl("streaming in -> " + i)); - - Flux responses = rule.crs.requestChannel(requests); - - responses.doOnNext(p -> latch.countDown()).subscribe(); - - latch.await(); - } - - public static class SocketRule extends ExternalResource { - - private RSocketClient crs; - private RSocketServer srs; - private RSocket requestAcceptor; - DirectProcessor serverProcessor; - DirectProcessor clientProcessor; - private ArrayList clientErrors = new ArrayList<>(); - private ArrayList serverErrors = new ArrayList<>(); - - @Override - public Statement apply(Statement base, Description description) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - init(); - base.evaluate(); - } - }; - } - - protected void init() { - serverProcessor = DirectProcessor.create(); - clientProcessor = DirectProcessor.create(); - - LocalDuplexConnection serverConnection = - new LocalDuplexConnection("server", clientProcessor, serverProcessor); - LocalDuplexConnection clientConnection = - new LocalDuplexConnection("client", serverProcessor, clientProcessor); - - requestAcceptor = - null != requestAcceptor - ? requestAcceptor - : new AbstractRSocket() { - @Override - public Mono requestResponse(Payload payload) { - return Mono.just(payload); - } - - @Override - public Flux requestChannel(Publisher payloads) { - Flux.from(payloads) - .map(payload -> new PayloadImpl("server got -> [" + payload.toString() + "]")) - .subscribe(); - - return Flux.range(1, 10) - .map( - payload -> new PayloadImpl("server got -> [" + payload.toString() + "]")); - } - }; - - srs = - new RSocketServer( - serverConnection, requestAcceptor, throwable -> serverErrors.add(throwable)); - - crs = - new RSocketClient( - clientConnection, - throwable -> clientErrors.add(throwable), - StreamIdSupplier.clientSupplier()); - } - - public void setRequestAcceptor(RSocket requestAcceptor) { - this.requestAcceptor = requestAcceptor; - init(); - } - - public void assertNoErrors() { - MatcherAssert.assertThat( - "Unexpected error on the client connection.", clientErrors, is(empty())); - MatcherAssert.assertThat( - "Unexpected error on the server connection.", serverErrors, is(empty())); - } - } -} 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/TestScheduler.java b/rsocket-core/src/test/java/io/rsocket/TestScheduler.java new file mode 100644 index 000000000..7bc98d45d --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/TestScheduler.java @@ -0,0 +1,80 @@ +package io.rsocket; + +import java.util.Queue; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.Exceptions; +import reactor.core.scheduler.Scheduler; +import reactor.util.concurrent.Queues; + +/** + * This is an implementation of scheduler which allows task execution on the caller thread or + * scheduling it for thread which are currently working (with "work stealing" behaviour) + */ +public final class TestScheduler implements Scheduler { + + public static final Scheduler INSTANCE = new TestScheduler(); + + volatile int wip; + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(TestScheduler.class, "wip"); + + final Worker sharedWorker = new TestWorker(this); + final Queue tasks = Queues.unboundedMultiproducer().get(); + + private TestScheduler() {} + + @Override + public Disposable schedule(Runnable task) { + tasks.offer(task); + if (WIP.getAndIncrement(this) != 0) { + return Disposables.never(); + } + + int missed = 1; + + for (; ; ) { + for (; ; ) { + Runnable runnable = tasks.poll(); + + if (runnable == null) { + break; + } + + try { + runnable.run(); + } catch (Throwable t) { + Exceptions.throwIfFatal(t); + } + } + + missed = WIP.addAndGet(this, -missed); + if (missed == 0) { + return Disposables.never(); + } + } + } + + @Override + public Worker createWorker() { + return sharedWorker; + } + + static class TestWorker implements Worker { + + final TestScheduler parent; + + TestWorker(TestScheduler parent) { + this.parent = parent; + } + + @Override + public Disposable schedule(Runnable task) { + return parent.schedule(task); + } + + @Override + public void dispose() {} + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/TestUtil.java b/rsocket-core/src/test/java/io/rsocket/TestUtil.java deleted file mode 100644 index 34ea7dedd..000000000 --- a/rsocket-core/src/test/java/io/rsocket/TestUtil.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket; - -import io.rsocket.util.PayloadImpl; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; - -public class TestUtil { - private static final char[] hexArray = "0123456789ABCDEF".toCharArray(); - - private TestUtil() {} - - public static Frame utf8EncodedRequestFrame( - final int streamId, final FrameType type, final String data, final int initialRequestN) { - return Frame.Request.from(streamId, type, new PayloadImpl(data), initialRequestN); - } - - public static Frame utf8EncodedResponseFrame( - final int streamId, final FrameType type, final String data) { - return Frame.PayloadFrame.from(streamId, type, new PayloadImpl(data)); - } - - public static Frame utf8EncodedErrorFrame(final int streamId, final String data) { - return Frame.Error.from(streamId, new Exception(data)); - } - - public static Payload utf8EncodedPayload(final String data, final String metadata) { - return new PayloadImpl(data, metadata); - } - - public static String byteToString(final ByteBuffer byteBuffer) { - return StandardCharsets.UTF_8.decode(byteBuffer).toString(); - } - - public static ByteBuffer byteBufferFromUtf8String(final String data) { - return StandardCharsets.UTF_8.encode(data); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java b/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java new file mode 100644 index 000000000..800e5d678 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java @@ -0,0 +1,153 @@ +package io.rsocket.buffer; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import java.util.concurrent.ConcurrentLinkedQueue; +import org.assertj.core.api.Assertions; + +/** + * Additional Utils which allows to decorate a ByteBufAllocator and track/assertOnLeaks all created + * ByteBuffs + */ +public class LeaksTrackingByteBufAllocator implements ByteBufAllocator { + + /** + * Allows to instrument any given the instance of ByteBufAllocator + * + * @param allocator + * @return + */ + public static LeaksTrackingByteBufAllocator instrument(ByteBufAllocator allocator) { + return new LeaksTrackingByteBufAllocator(allocator); + } + + final ConcurrentLinkedQueue tracker = new ConcurrentLinkedQueue<>(); + + final ByteBufAllocator delegate; + + private LeaksTrackingByteBufAllocator(ByteBufAllocator delegate) { + this.delegate = delegate; + } + + public LeaksTrackingByteBufAllocator assertHasNoLeaks() { + try { + Assertions.assertThat(tracker) + .allSatisfy( + buf -> + Assertions.assertThat(buf) + .matches(bb -> bb.refCnt() == 0, "buffer should be released")); + } finally { + tracker.clear(); + } + return this; + } + + // Delegating logic with tracking of buffers + + @Override + public ByteBuf buffer() { + return track(delegate.buffer()); + } + + @Override + public ByteBuf buffer(int initialCapacity) { + return track(delegate.buffer(initialCapacity)); + } + + @Override + public ByteBuf buffer(int initialCapacity, int maxCapacity) { + return track(delegate.buffer(initialCapacity, maxCapacity)); + } + + @Override + public ByteBuf ioBuffer() { + return track(delegate.ioBuffer()); + } + + @Override + public ByteBuf ioBuffer(int initialCapacity) { + return track(delegate.ioBuffer(initialCapacity)); + } + + @Override + public ByteBuf ioBuffer(int initialCapacity, int maxCapacity) { + return track(delegate.ioBuffer(initialCapacity, maxCapacity)); + } + + @Override + public ByteBuf heapBuffer() { + return track(delegate.heapBuffer()); + } + + @Override + public ByteBuf heapBuffer(int initialCapacity) { + return track(delegate.heapBuffer(initialCapacity)); + } + + @Override + public ByteBuf heapBuffer(int initialCapacity, int maxCapacity) { + return track(delegate.heapBuffer(initialCapacity, maxCapacity)); + } + + @Override + public ByteBuf directBuffer() { + return track(delegate.directBuffer()); + } + + @Override + public ByteBuf directBuffer(int initialCapacity) { + return track(delegate.directBuffer(initialCapacity)); + } + + @Override + public ByteBuf directBuffer(int initialCapacity, int maxCapacity) { + return track(delegate.directBuffer(initialCapacity, maxCapacity)); + } + + @Override + public CompositeByteBuf compositeBuffer() { + return track(delegate.compositeBuffer()); + } + + @Override + public CompositeByteBuf compositeBuffer(int maxNumComponents) { + return track(delegate.compositeBuffer(maxNumComponents)); + } + + @Override + public CompositeByteBuf compositeHeapBuffer() { + return track(delegate.compositeHeapBuffer()); + } + + @Override + public CompositeByteBuf compositeHeapBuffer(int maxNumComponents) { + return track(delegate.compositeHeapBuffer(maxNumComponents)); + } + + @Override + public CompositeByteBuf compositeDirectBuffer() { + return track(delegate.compositeDirectBuffer()); + } + + @Override + public CompositeByteBuf compositeDirectBuffer(int maxNumComponents) { + return track(delegate.compositeDirectBuffer(maxNumComponents)); + } + + @Override + public boolean isDirectBufferPooled() { + return delegate.isDirectBufferPooled(); + } + + @Override + public int calculateNewCapacity(int minNewCapacity, int maxCapacity) { + return delegate.calculateNewCapacity(minNewCapacity, maxCapacity); + } + + T track(T buffer) { + tracker.offer(buffer); + + return buffer; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/AbstractSocketRule.java b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java similarity index 62% rename from rsocket-core/src/test/java/io/rsocket/AbstractSocketRule.java rename to rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java index ecd0f1e64..7398548be 100644 --- a/rsocket-core/src/test/java/io/rsocket/AbstractSocketRule.java +++ b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,11 +14,15 @@ * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.RSocket; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestSubscriber; -import java.util.concurrent.ConcurrentLinkedQueue; import org.junit.rules.ExternalResource; import org.junit.runner.Description; import org.junit.runners.model.Statement; @@ -29,16 +33,17 @@ public abstract class AbstractSocketRule extends ExternalReso protected TestDuplexConnection connection; protected Subscriber connectSub; protected T socket; - protected ConcurrentLinkedQueue errors; + protected LeaksTrackingByteBufAllocator allocator; + protected int maxFrameLength = FRAME_LENGTH_MASK; @Override public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { - connection = new TestDuplexConnection(); + allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + connection = new TestDuplexConnection(allocator); connectSub = TestSubscriber.create(); - errors = new ConcurrentLinkedQueue<>(); init(); base.evaluate(); } @@ -49,5 +54,18 @@ protected void init() { socket = newRSocket(); } + public void setMaxFrameLength(int maxFrameLength) { + this.maxFrameLength = maxFrameLength; + init(); + } + protected abstract T newRSocket(); + + public ByteBufAllocator alloc() { + return allocator; + } + + public void assertHasNoLeaks() { + allocator.assertHasNoLeaks(); + } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/ConnectionSetupPayloadTest.java b/rsocket-core/src/test/java/io/rsocket/core/ConnectionSetupPayloadTest.java new file mode 100644 index 000000000..8eb5dee09 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/ConnectionSetupPayloadTest.java @@ -0,0 +1,90 @@ +package io.rsocket.core; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.Payload; +import io.rsocket.frame.SetupFrameCodec; +import io.rsocket.util.DefaultPayload; +import org.junit.jupiter.api.Test; + +class ConnectionSetupPayloadTest { + private static final int KEEP_ALIVE_INTERVAL = 5; + private static final int KEEP_ALIVE_MAX_LIFETIME = 500; + private static final String METADATA_TYPE = "metadata_type"; + private static final String DATA_TYPE = "data_type"; + + @Test + void testSetupPayloadWithDataMetadata() { + ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); + ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {2, 1, 0}); + Payload payload = DefaultPayload.create(data, metadata); + boolean leaseEnabled = true; + + ByteBuf frame = encodeSetupFrame(leaseEnabled, payload); + ConnectionSetupPayload setupPayload = new DefaultConnectionSetupPayload(frame); + + assertTrue(setupPayload.willClientHonorLease()); + assertEquals(KEEP_ALIVE_INTERVAL, setupPayload.keepAliveInterval()); + assertEquals(KEEP_ALIVE_MAX_LIFETIME, setupPayload.keepAliveMaxLifetime()); + assertEquals(METADATA_TYPE, SetupFrameCodec.metadataMimeType(frame)); + assertEquals(DATA_TYPE, SetupFrameCodec.dataMimeType(frame)); + assertTrue(setupPayload.hasMetadata()); + assertNotNull(setupPayload.metadata()); + assertEquals(payload.metadata(), setupPayload.metadata()); + assertEquals(payload.data(), setupPayload.data()); + frame.release(); + } + + @Test + void testSetupPayloadWithNoMetadata() { + ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); + ByteBuf metadata = null; + Payload payload = DefaultPayload.create(data, metadata); + boolean leaseEnabled = false; + + ByteBuf frame = encodeSetupFrame(leaseEnabled, payload); + ConnectionSetupPayload setupPayload = new DefaultConnectionSetupPayload(frame); + + assertFalse(setupPayload.willClientHonorLease()); + assertFalse(setupPayload.hasMetadata()); + assertNotNull(setupPayload.metadata()); + assertEquals(0, setupPayload.metadata().readableBytes()); + assertEquals(payload.data(), setupPayload.data()); + frame.release(); + } + + @Test + void testSetupPayloadWithEmptyMetadata() { + ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); + ByteBuf metadata = Unpooled.EMPTY_BUFFER; + Payload payload = DefaultPayload.create(data, metadata); + boolean leaseEnabled = false; + + ByteBuf frame = encodeSetupFrame(leaseEnabled, payload); + ConnectionSetupPayload setupPayload = new DefaultConnectionSetupPayload(frame); + + assertFalse(setupPayload.willClientHonorLease()); + assertTrue(setupPayload.hasMetadata()); + assertNotNull(setupPayload.metadata()); + assertEquals(0, setupPayload.metadata().readableBytes()); + assertEquals(payload.data(), setupPayload.data()); + frame.release(); + } + + private static ByteBuf encodeSetupFrame(boolean leaseEnabled, Payload setupPayload) { + return SetupFrameCodec.encode( + ByteBufAllocator.DEFAULT, + leaseEnabled, + KEEP_ALIVE_INTERVAL, + KEEP_ALIVE_MAX_LIFETIME, + Unpooled.EMPTY_BUFFER, + METADATA_TYPE, + DATA_TYPE, + setupPayload); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java new file mode 100644 index 000000000..209bc3810 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java @@ -0,0 +1,372 @@ +/* + * 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.TestScheduler; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.exceptions.ConnectionErrorException; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.KeepAliveFrameCodec; +import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.resume.InMemoryResumableFramesStore; +import io.rsocket.resume.ResumableDuplexConnection; +import io.rsocket.test.util.TestDuplexConnection; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.test.scheduler.VirtualTimeScheduler; + +public class KeepAliveTest { + private static final int KEEP_ALIVE_INTERVAL = 100; + private static final int KEEP_ALIVE_TIMEOUT = 1000; + private static final int RESUMABLE_KEEP_ALIVE_TIMEOUT = 200; + + VirtualTimeScheduler virtualTimeScheduler; + + @BeforeEach + public void setUp() { + virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); + } + + @AfterEach + public void tearDown() { + VirtualTimeScheduler.reset(); + } + + static RSocketState requester(int tickPeriod, int timeout) { + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + TestDuplexConnection connection = new TestDuplexConnection(allocator); + RSocketRequester rSocket = + new RSocketRequester( + connection, + DefaultPayload::create, + StreamIdSupplier.clientSupplier(), + 0, + FRAME_LENGTH_MASK, + tickPeriod, + timeout, + new DefaultKeepAliveHandler(connection), + RequesterLeaseHandler.None, + TestScheduler.INSTANCE); + 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, + tickPeriod, + timeout, + new ResumableKeepAliveHandler(resumableConnection), + RequesterLeaseHandler.None, + TestScheduler.INSTANCE); + return new ResumableRSocketState(rSocket, connection, resumableConnection, allocator); + } + + @Test + void rSocketNotDisposedOnPresentKeepAlives() { + RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + + TestDuplexConnection connection = requesterState.connection(); + + Disposable disposable = + Flux.interval(Duration.ofMillis(KEEP_ALIVE_INTERVAL)) + .subscribe( + n -> + connection.addToReceivedBuffer( + KeepAliveFrameCodec.encode( + requesterState.allocator, true, 0, Unpooled.EMPTY_BUFFER))); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_TIMEOUT * 2)); + + RSocket rSocket = requesterState.rSocket(); + + Assertions.assertThat(rSocket.isDisposed()).isFalse(); + + disposable.dispose(); + + requesterState.connection.dispose(); + requesterState.rSocket.dispose(); + + Assertions.assertThat(requesterState.connection.getSent()).allMatch(ByteBuf::release); + + requesterState.allocator.assertHasNoLeaks(); + } + + @Test + void noKeepAlivesSentAfterRSocketDispose() { + RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + + requesterState.rSocket().dispose(); + + Duration duration = Duration.ofMillis(500); + StepVerifier.create(Flux.from(requesterState.connection().getSentAsPublisher()).take(duration)) + .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) + .expectComplete() + .verify(Duration.ofSeconds(1)); + + requesterState.allocator.assertHasNoLeaks(); + } + + @Test + void rSocketDisposedOnMissingKeepAlives() { + RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + + RSocket rSocket = requesterState.rSocket(); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_TIMEOUT * 2)); + + Assertions.assertThat(rSocket.isDisposed()).isTrue(); + rSocket + .onClose() + .as(StepVerifier::create) + .expectError(ConnectionErrorException.class) + .verify(Duration.ofMillis(100)); + + Assertions.assertThat(requesterState.connection.getSent()).allMatch(ByteBuf::release); + + requesterState.allocator.assertHasNoLeaks(); + } + + @Test + void clientRequesterSendsKeepAlives() { + RSocketState RSocketState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + TestDuplexConnection connection = RSocketState.connection(); + + StepVerifier.create(Flux.from(connection.getSentAsPublisher()).take(3)) + .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) + .expectNextMatches(this::keepAliveFrameWithRespondFlag) + .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) + .expectNextMatches(this::keepAliveFrameWithRespondFlag) + .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) + .expectNextMatches(this::keepAliveFrameWithRespondFlag) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + RSocketState.rSocket.dispose(); + RSocketState.connection.dispose(); + + RSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void requesterRespondsToKeepAlives() { + RSocketState rSocketState = requester(100_000, 100_000); + TestDuplexConnection connection = rSocketState.connection(); + Duration duration = Duration.ofMillis(100); + Mono.delay(duration) + .subscribe( + l -> + connection.addToReceivedBuffer( + KeepAliveFrameCodec.encode( + rSocketState.allocator, true, 0, Unpooled.EMPTY_BUFFER))); + + StepVerifier.create(Flux.from(connection.getSentAsPublisher()).take(1)) + .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) + .expectNextMatches(this::keepAliveFrameWithoutRespondFlag) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + rSocketState.rSocket.dispose(); + rSocketState.connection.dispose(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRequesterNoKeepAlivesAfterDisconnect() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + TestDuplexConnection testConnection = rSocketState.connection(); + ResumableDuplexConnection resumableDuplexConnection = rSocketState.resumableDuplexConnection(); + + resumableDuplexConnection.disconnect(); + + Duration duration = Duration.ofMillis(500); + StepVerifier.create(Flux.from(testConnection.getSentAsPublisher()).take(duration)) + .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + rSocketState.rSocket.dispose(); + rSocketState.connection.dispose(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRequesterKeepAlivesAfterReconnect() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + ResumableDuplexConnection resumableDuplexConnection = rSocketState.resumableDuplexConnection(); + resumableDuplexConnection.disconnect(); + TestDuplexConnection newTestConnection = new TestDuplexConnection(rSocketState.alloc()); + resumableDuplexConnection.reconnect(newTestConnection); + resumableDuplexConnection.resume(0, 0, ignored -> Mono.empty()); + + StepVerifier.create(Flux.from(newTestConnection.getSentAsPublisher()).take(1)) + .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) + .expectNextMatches(frame -> keepAliveFrame(frame) && frame.release()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + rSocketState.rSocket.dispose(); + rSocketState.connection.dispose(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRequesterNoKeepAlivesAfterDispose() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + rSocketState.rSocket().dispose(); + Duration duration = Duration.ofMillis(500); + StepVerifier.create(Flux.from(rSocketState.connection().getSentAsPublisher()).take(duration)) + .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + rSocketState.rSocket.dispose(); + rSocketState.connection.dispose(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRSocketsNotDisposedOnMissingKeepAlives() throws InterruptedException { + ResumableRSocketState resumableRequesterState = + resumableRequester(KEEP_ALIVE_INTERVAL, RESUMABLE_KEEP_ALIVE_TIMEOUT); + RSocket rSocket = resumableRequesterState.rSocket(); + TestDuplexConnection connection = resumableRequesterState.connection(); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(500)); + + Assertions.assertThat(rSocket.isDisposed()).isFalse(); + Assertions.assertThat(connection.isDisposed()).isTrue(); + + Assertions.assertThat(resumableRequesterState.connection.getSent()).allMatch(ByteBuf::release); + + resumableRequesterState.connection.dispose(); + resumableRequesterState.rSocket.dispose(); + + resumableRequesterState.allocator.assertHasNoLeaks(); + } + + private boolean keepAliveFrame(ByteBuf frame) { + return FrameHeaderCodec.frameType(frame) == FrameType.KEEPALIVE; + } + + private boolean keepAliveFrameWithRespondFlag(ByteBuf frame) { + return keepAliveFrame(frame) && KeepAliveFrameCodec.respondFlag(frame) && frame.release(); + } + + private boolean keepAliveFrameWithoutRespondFlag(ByteBuf frame) { + return keepAliveFrame(frame) && !KeepAliveFrameCodec.respondFlag(frame) && frame.release(); + } + + static class RSocketState { + private final RSocket rSocket; + private final TestDuplexConnection connection; + private final LeaksTrackingByteBufAllocator allocator; + + public RSocketState( + RSocket rSocket, LeaksTrackingByteBufAllocator allocator, TestDuplexConnection connection) { + this.rSocket = rSocket; + this.connection = connection; + this.allocator = allocator; + } + + public TestDuplexConnection connection() { + return connection; + } + + public RSocket rSocket() { + return rSocket; + } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } + } + + static class ResumableRSocketState { + private final RSocket rSocket; + private final TestDuplexConnection connection; + private final ResumableDuplexConnection resumableDuplexConnection; + private final LeaksTrackingByteBufAllocator allocator; + + public ResumableRSocketState( + RSocket rSocket, + TestDuplexConnection connection, + ResumableDuplexConnection resumableDuplexConnection, + LeaksTrackingByteBufAllocator allocator) { + this.rSocket = rSocket; + this.connection = connection; + this.resumableDuplexConnection = resumableDuplexConnection; + this.allocator = allocator; + } + + public TestDuplexConnection connection() { + return connection; + } + + public ResumableDuplexConnection resumableDuplexConnection() { + return resumableDuplexConnection; + } + + public RSocket rSocket() { + return rSocket; + } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/PayloadValidationUtilsTest.java b/rsocket-core/src/test/java/io/rsocket/core/PayloadValidationUtilsTest.java new file mode 100644 index 000000000..1d93d9388 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/PayloadValidationUtilsTest.java @@ -0,0 +1,106 @@ +package io.rsocket.core; + +import io.rsocket.Payload; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameLengthCodec; +import io.rsocket.util.DefaultPayload; +import java.util.concurrent.ThreadLocalRandom; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class PayloadValidationUtilsTest { + + @Test + void shouldBeValidFrameWithNoFragmentation() { + int maxFrameLength = + ThreadLocalRandom.current().nextInt(64, FrameLengthCodec.FRAME_LENGTH_MASK); + byte[] data = + new byte[maxFrameLength - FrameLengthCodec.FRAME_LENGTH_SIZE - FrameHeaderCodec.size()]; + ThreadLocalRandom.current().nextBytes(data); + final Payload payload = DefaultPayload.create(data); + + Assertions.assertThat(PayloadValidationUtils.isValid(0, payload, maxFrameLength)).isTrue(); + } + + @Test + void shouldBeInValidFrameWithNoFragmentation() { + int maxFrameLength = + ThreadLocalRandom.current().nextInt(64, FrameLengthCodec.FRAME_LENGTH_MASK); + byte[] data = + new byte[maxFrameLength - FrameLengthCodec.FRAME_LENGTH_SIZE - FrameHeaderCodec.size() + 1]; + ThreadLocalRandom.current().nextBytes(data); + final Payload payload = DefaultPayload.create(data); + + Assertions.assertThat(PayloadValidationUtils.isValid(0, payload, maxFrameLength)).isFalse(); + } + + @Test + void shouldBeValidFrameWithNoFragmentation0() { + int maxFrameLength = + ThreadLocalRandom.current().nextInt(64, FrameLengthCodec.FRAME_LENGTH_MASK); + byte[] metadata = new byte[maxFrameLength / 2]; + byte[] data = + new byte + [maxFrameLength / 2 + - FrameLengthCodec.FRAME_LENGTH_SIZE + - FrameHeaderCodec.size() + - FrameHeaderCodec.size()]; + ThreadLocalRandom.current().nextBytes(data); + ThreadLocalRandom.current().nextBytes(metadata); + final Payload payload = DefaultPayload.create(data, metadata); + + Assertions.assertThat(PayloadValidationUtils.isValid(0, payload, maxFrameLength)).isTrue(); + } + + @Test + void shouldBeInValidFrameWithNoFragmentation1() { + int maxFrameLength = + ThreadLocalRandom.current().nextInt(64, FrameLengthCodec.FRAME_LENGTH_MASK); + byte[] metadata = new byte[maxFrameLength]; + byte[] data = new byte[maxFrameLength]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + final Payload payload = DefaultPayload.create(data, metadata); + + Assertions.assertThat(PayloadValidationUtils.isValid(0, payload, maxFrameLength)).isFalse(); + } + + @Test + void shouldBeValidFrameWithNoFragmentation2() { + int maxFrameLength = + ThreadLocalRandom.current().nextInt(64, FrameLengthCodec.FRAME_LENGTH_MASK); + byte[] metadata = new byte[1]; + byte[] data = new byte[1]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + final Payload payload = DefaultPayload.create(data, metadata); + + Assertions.assertThat(PayloadValidationUtils.isValid(0, payload, maxFrameLength)).isTrue(); + } + + @Test + void shouldBeValidFrameWithNoFragmentation3() { + int maxFrameLength = + ThreadLocalRandom.current().nextInt(64, FrameLengthCodec.FRAME_LENGTH_MASK); + byte[] metadata = new byte[maxFrameLength]; + byte[] data = new byte[maxFrameLength]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + final Payload payload = DefaultPayload.create(data, metadata); + + Assertions.assertThat(PayloadValidationUtils.isValid(64, payload, maxFrameLength)).isTrue(); + } + + @Test + void shouldBeValidFrameWithNoFragmentation4() { + int maxFrameLength = + ThreadLocalRandom.current().nextInt(64, FrameLengthCodec.FRAME_LENGTH_MASK); + byte[] metadata = new byte[1]; + byte[] data = new byte[1]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + final Payload payload = DefaultPayload.create(data, metadata); + + Assertions.assertThat(PayloadValidationUtils.isValid(64, payload, maxFrameLength)).isTrue(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java new file mode 100644 index 000000000..ec4adb8ad --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java @@ -0,0 +1,205 @@ +package io.rsocket.core; + +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + +import io.netty.buffer.ByteBuf; +import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCounted; +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.test.util.TestClientTransport; +import io.rsocket.util.ByteBufPayload; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.test.StepVerifier; + +public class RSocketConnectorTest { + + @Test + public void ensuresThatSetupPayloadCanBeRetained() { + MonoProcessor retainedSetupPayload = MonoProcessor.create(); + TestClientTransport transport = new TestClientTransport(); + + ByteBuf data = transport.alloc().buffer(); + + data.writeCharSequence("data", CharsetUtil.UTF_8); + + RSocketConnector.create() + .setupPayload(ByteBufPayload.create(data)) + .acceptor( + (setup, sendingSocket) -> { + retainedSetupPayload.onNext(setup.retain()); + return Mono.just(new RSocket() {}); + }) + .connect(transport) + .block(); + + Assertions.assertThat(transport.testConnection().getSent()) + .hasSize(1) + .first() + .matches( + bb -> { + DefaultConnectionSetupPayload payload = new DefaultConnectionSetupPayload(bb); + return !payload.hasMetadata() && payload.getDataUtf8().equals("data"); + }) + .matches(buf -> buf.refCnt() == 2) + .matches( + buf -> { + buf.release(); + 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(); + + transport.alloc().assertHasNoLeaks(); + } + + @Test + public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions() { + Payload setupPayload = ByteBufPayload.create("TestData", "TestMetadata"); + Assertions.assertThat(setupPayload.refCnt()).isOne(); + + // Keep the data and metadata around so we can try changing them independently + ByteBuf dataBuf = setupPayload.data(); + ByteBuf metadataBuf = setupPayload.metadata(); + dataBuf.retain(); + metadataBuf.retain(); + + TestClientTransport testClientTransport = new TestClientTransport(); + Mono connectionMono = + RSocketConnector.create().setupPayload(setupPayload).connect(testClientTransport); + + connectionMono + .as(StepVerifier::create) + .expectNextCount(1) + .expectComplete() + .verify(Duration.ofMillis(100)); + + connectionMono + .as(StepVerifier::create) + .expectNextCount(1) + .expectComplete() + .verify(Duration.ofMillis(100)); + + // Changing the original data and metadata should not impact the SetupPayload + dataBuf.writerIndex(dataBuf.readerIndex()); + dataBuf.writeChar('d'); + dataBuf.release(); + + metadataBuf.writerIndex(metadataBuf.readerIndex()); + metadataBuf.writeChar('m'); + metadataBuf.release(); + + Assertions.assertThat(testClientTransport.testConnection().getSent()) + .hasSize(2) + .allMatch( + bb -> { + DefaultConnectionSetupPayload payload = new DefaultConnectionSetupPayload(bb); + return payload.getDataUtf8().equals("TestData") + && payload.getMetadataUtf8().equals("TestMetadata"); + }) + .allMatch( + byteBuf -> { + System.out.println("calling release " + byteBuf.refCnt()); + return byteBuf.release(); + }); + Assertions.assertThat(setupPayload.refCnt()).isZero(); + } + + @Test + public void ensuresThatSetupPayloadProvidedAsMonoIsReleased() { + List saved = new ArrayList<>(); + Mono setupPayloadMono = + Mono.create( + sink -> { + Payload payload = ByteBufPayload.create("TestData", "TestMetadata"); + saved.add(payload); + sink.success(payload); + }); + + TestClientTransport testClientTransport = new TestClientTransport(); + Mono connectionMono = + RSocketConnector.create().setupPayload(setupPayloadMono).connect(testClientTransport); + + connectionMono + .as(StepVerifier::create) + .expectNextCount(1) + .expectComplete() + .verify(Duration.ofMillis(100)); + + connectionMono + .as(StepVerifier::create) + .expectNextCount(1) + .expectComplete() + .verify(Duration.ofMillis(100)); + + Assertions.assertThat(testClientTransport.testConnection().getSent()) + .hasSize(2) + .allMatch( + bb -> { + DefaultConnectionSetupPayload payload = new DefaultConnectionSetupPayload(bb); + return payload.getDataUtf8().equals("TestData") + && payload.getMetadataUtf8().equals("TestMetadata"); + }) + .allMatch(ReferenceCounted::release); + + Assertions.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); + } + + @Test + public void ensuresMaxFrameLengthCanNotBeLessThenMtu() { + RSocketConnector.create() + .fragment(128) + .connect(new TestClientTransport().withMaxFrameLength(64)) + .as(StepVerifier::create) + .expectErrorMessage( + "Configured maximumTransmissionUnit[128] exceeds configured maxFrameLength[64]") + .verify(); + } + + @Test + public void ensuresMaxFrameLengthCanNotBeGreaterThenMaxPayloadSize() { + RSocketConnector.create() + .maxInboundPayloadSize(128) + .connect(new TestClientTransport().withMaxFrameLength(256)) + .as(StepVerifier::create) + .expectErrorMessage("Configured maxFrameLength[256] exceeds maxPayloadSize[128]") + .verify(); + } + + @Test + public void ensuresMaxFrameLengthCanNotBeGreaterThenMaxPossibleFrameLength() { + RSocketConnector.create() + .connect(new TestClientTransport().withMaxFrameLength(Integer.MAX_VALUE)) + .as(StepVerifier::create) + .expectErrorMessage( + "Configured maxFrameLength[" + + Integer.MAX_VALUE + + "] " + + "exceeds maxFrameLength limit " + + FRAME_LENGTH_MASK) + .verify(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java new file mode 100644 index 000000000..7faef600a --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -0,0 +1,503 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; +import static io.rsocket.frame.FrameType.*; +import static org.assertj.core.data.Offset.offset; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCounted; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.TestScheduler; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.exceptions.Exceptions; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.LeaseFrameCodec; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.SetupFrameCodec; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.ClientServerInputMultiplexer; +import io.rsocket.internal.subscriber.AssertSubscriber; +import io.rsocket.lease.*; +import io.rsocket.lease.MissingLeaseException; +import io.rsocket.plugins.InitializingInterceptorRegistry; +import io.rsocket.test.util.TestClientTransport; +import io.rsocket.test.util.TestDuplexConnection; +import io.rsocket.test.util.TestServerTransport; +import io.rsocket.util.ByteBufPayload; +import io.rsocket.util.DefaultPayload; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; +import org.reactivestreams.Publisher; +import reactor.core.publisher.EmitterProcessor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class RSocketLeaseTest { + private static final String TAG = "test"; + + private RSocket rSocketRequester; + private ResponderLeaseHandler responderLeaseHandler; + private LeaksTrackingByteBufAllocator byteBufAllocator; + private TestDuplexConnection connection; + private RSocketResponder rSocketResponder; + private RSocket mockRSocketHandler; + + private EmitterProcessor leaseSender = EmitterProcessor.create(); + private Flux leaseReceiver; + private RequesterLeaseHandler requesterLeaseHandler; + + @BeforeEach + void setUp() { + PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; + byteBufAllocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + + connection = new TestDuplexConnection(byteBufAllocator); + requesterLeaseHandler = new RequesterLeaseHandler.Impl(TAG, leases -> leaseReceiver = leases); + responderLeaseHandler = + new ResponderLeaseHandler.Impl<>( + TAG, byteBufAllocator, stats -> leaseSender, Optional.empty()); + + ClientServerInputMultiplexer multiplexer = + new ClientServerInputMultiplexer(connection, new InitializingInterceptorRegistry(), true); + rSocketRequester = + new RSocketRequester( + multiplexer.asClientConnection(), + payloadDecoder, + StreamIdSupplier.clientSupplier(), + 0, + FRAME_LENGTH_MASK, + 0, + 0, + null, + requesterLeaseHandler, + TestScheduler.INSTANCE); + + mockRSocketHandler = mock(RSocket.class); + when(mockRSocketHandler.metadataPush(any())) + .then( + a -> { + Payload payload = a.getArgument(0); + payload.release(); + return Mono.empty(); + }); + when(mockRSocketHandler.fireAndForget(any())) + .then( + a -> { + Payload payload = a.getArgument(0); + payload.release(); + return Mono.empty(); + }); + when(mockRSocketHandler.requestResponse(any())) + .then( + a -> { + Payload payload = a.getArgument(0); + payload.release(); + return Mono.empty(); + }); + when(mockRSocketHandler.requestStream(any())) + .then( + a -> { + Payload payload = a.getArgument(0); + payload.release(); + return Flux.empty(); + }); + when(mockRSocketHandler.requestChannel(any())) + .then( + a -> { + Publisher payloadPublisher = a.getArgument(0); + return Flux.from(payloadPublisher) + .doOnNext(ReferenceCounted::release) + .thenMany(Flux.empty()); + }); + + rSocketResponder = + new RSocketResponder( + multiplexer.asServerConnection(), + mockRSocketHandler, + payloadDecoder, + responderLeaseHandler, + 0, + FRAME_LENGTH_MASK); + } + + @Test + public void serverRSocketFactoryRejectsUnsupportedLease() { + Payload payload = DefaultPayload.create(DefaultPayload.EMPTY_BUFFER); + ByteBuf setupFrame = + SetupFrameCodec.encode( + ByteBufAllocator.DEFAULT, + true, + 1000, + 30_000, + "application/octet-stream", + "application/octet-stream", + payload); + + TestServerTransport transport = new TestServerTransport(); + RSocketServer.create().bind(transport).block(); + + TestDuplexConnection connection = transport.connect(); + connection.addToReceivedBuffer(setupFrame); + + Collection sent = connection.getSent(); + Assertions.assertThat(sent).hasSize(1); + ByteBuf error = sent.iterator().next(); + Assertions.assertThat(FrameHeaderCodec.frameType(error)).isEqualTo(ERROR); + Assertions.assertThat(Exceptions.from(0, error).getMessage()) + .isEqualTo("lease is not supported"); + } + + @Test + public void clientRSocketFactorySetsLeaseFlag() { + TestClientTransport clientTransport = new TestClientTransport(); + RSocketConnector.create().lease(Leases::new).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(); + } + + @ParameterizedTest + @MethodSource("interactions") + void requesterMissingLeaseRequestsAreRejected( + BiFunction> interaction) { + Assertions.assertThat(rSocketRequester.availability()).isCloseTo(0.0, offset(1e-2)); + ByteBuf buffer = byteBufAllocator.buffer(); + buffer.writeCharSequence("test", CharsetUtil.UTF_8); + Payload payload1 = ByteBufPayload.create(buffer); + StepVerifier.create(interaction.apply(rSocketRequester, payload1)) + .expectError(MissingLeaseException.class) + .verify(Duration.ofSeconds(5)); + + byteBufAllocator.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("interactions") + void requesterPresentLeaseRequestsAreAccepted( + BiFunction> interaction, FrameType frameType) { + ByteBuf frame = leaseFrame(5_000, 2, Unpooled.EMPTY_BUFFER); + requesterLeaseHandler.receive(frame); + + Assertions.assertThat(rSocketRequester.availability()).isCloseTo(1.0, offset(1e-2)); + ByteBuf buffer = byteBufAllocator.buffer(); + buffer.writeCharSequence("test", CharsetUtil.UTF_8); + Payload payload1 = ByteBufPayload.create(buffer); + Flux.from(interaction.apply(rSocketRequester, payload1)) + .as(StepVerifier::create) + .then( + () -> { + if (frameType != REQUEST_FNF) { + connection.addToReceivedBuffer( + PayloadFrameCodec.encodeComplete(byteBufAllocator, 1)); + } + }) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + Assertions.assertThat(connection.getSent()) + .hasSize(1) + .first() + .matches(ReferenceCounted::release); + + Assertions.assertThat(rSocketRequester.availability()).isCloseTo(0.5, offset(1e-2)); + + Assertions.assertThat(frame.release()).isTrue(); + + byteBufAllocator.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("interactions") + @SuppressWarnings({"rawtypes", "unchecked"}) + void requesterDepletedAllowedLeaseRequestsAreRejected( + BiFunction> interaction, FrameType interactionType) { + ByteBuf buffer = byteBufAllocator.buffer(); + buffer.writeCharSequence("test", CharsetUtil.UTF_8); + Payload payload1 = ByteBufPayload.create(buffer); + ByteBuf leaseFrame = leaseFrame(5_000, 1, Unpooled.EMPTY_BUFFER); + requesterLeaseHandler.receive(leaseFrame); + + double initialAvailability = requesterLeaseHandler.availability(); + Publisher request = interaction.apply(rSocketRequester, payload1); + + // ensures that lease is not used until the frame is sent + Assertions.assertThat(initialAvailability).isEqualTo(requesterLeaseHandler.availability()); + Assertions.assertThat(connection.getSent()).hasSize(0); + + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + request.subscribe(assertSubscriber); + + // if request is FNF, then request frame is sent on subscribe + // otherwise we need to make request(1) + if (interactionType != REQUEST_FNF) { + Assertions.assertThat(initialAvailability).isEqualTo(requesterLeaseHandler.availability()); + Assertions.assertThat(connection.getSent()).hasSize(0); + + assertSubscriber.request(1); + } + + // ensures availability is changed and lease is used only up on frame sending + Assertions.assertThat(rSocketRequester.availability()).isCloseTo(0.0, offset(1e-2)); + Assertions.assertThat(connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> FrameHeaderCodec.frameType(bb) == interactionType) + .matches(ReferenceCounted::release); + + ByteBuf buffer2 = byteBufAllocator.buffer(); + buffer2.writeCharSequence("test", CharsetUtil.UTF_8); + Payload payload2 = ByteBufPayload.create(buffer2); + Flux.from(interaction.apply(rSocketRequester, payload2)) + .as(StepVerifier::create) + .expectError(MissingLeaseException.class) + .verify(Duration.ofSeconds(5)); + + Assertions.assertThat(leaseFrame.release()).isTrue(); + + byteBufAllocator.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("interactions") + void requesterExpiredLeaseRequestsAreRejected( + BiFunction> interaction) { + ByteBuf frame = leaseFrame(50, 1, Unpooled.EMPTY_BUFFER); + requesterLeaseHandler.receive(frame); + + ByteBuf buffer = byteBufAllocator.buffer(); + buffer.writeCharSequence("test", CharsetUtil.UTF_8); + Payload payload1 = ByteBufPayload.create(buffer); + + Flux.defer(() -> interaction.apply(rSocketRequester, payload1)) + .delaySubscription(Duration.ofMillis(200)) + .as(StepVerifier::create) + .expectError(MissingLeaseException.class) + .verify(Duration.ofSeconds(5)); + + Assertions.assertThat(frame.release()).isTrue(); + + byteBufAllocator.assertHasNoLeaks(); + } + + @Test + void requesterAvailabilityRespectsTransport() { + requesterLeaseHandler.receive(leaseFrame(5_000, 1, Unpooled.EMPTY_BUFFER)); + double unavailable = 0.0; + connection.setAvailability(unavailable); + Assertions.assertThat(rSocketRequester.availability()).isCloseTo(unavailable, offset(1e-2)); + } + + @ParameterizedTest + @MethodSource("interactions") + void responderMissingLeaseRequestsAreRejected( + BiFunction> interaction) { + ByteBuf buffer = byteBufAllocator.buffer(); + buffer.writeCharSequence("test", CharsetUtil.UTF_8); + Payload payload1 = ByteBufPayload.create(buffer); + + StepVerifier.create(interaction.apply(rSocketResponder, payload1)) + .expectError(MissingLeaseException.class) + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("interactions") + void responderPresentLeaseRequestsAreAccepted( + BiFunction> interaction, FrameType frameType) { + leaseSender.onNext(Lease.create(5_000, 2)); + + ByteBuf buffer = byteBufAllocator.buffer(); + buffer.writeCharSequence("test", CharsetUtil.UTF_8); + Payload payload1 = ByteBufPayload.create(buffer); + + Flux.from(interaction.apply(rSocketResponder, payload1)) + .as(StepVerifier::create) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + switch (frameType) { + case REQUEST_FNF: + Mockito.verify(mockRSocketHandler).fireAndForget(any()); + break; + case REQUEST_RESPONSE: + Mockito.verify(mockRSocketHandler).requestResponse(any()); + break; + case REQUEST_STREAM: + Mockito.verify(mockRSocketHandler).requestStream(any()); + break; + case REQUEST_CHANNEL: + Mockito.verify(mockRSocketHandler).requestChannel(any()); + break; + } + + Assertions.assertThat(connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> FrameHeaderCodec.frameType(bb) == LEASE) + .matches(ReferenceCounted::release); + + byteBufAllocator.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("interactions") + void responderDepletedAllowedLeaseRequestsAreRejected( + BiFunction> interaction) { + leaseSender.onNext(Lease.create(5_000, 1)); + + ByteBuf buffer = byteBufAllocator.buffer(); + buffer.writeCharSequence("test", CharsetUtil.UTF_8); + Payload payload1 = ByteBufPayload.create(buffer); + + Flux responder = Flux.from(interaction.apply(rSocketResponder, payload1)); + responder.subscribe(); + + Assertions.assertThat(connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> FrameHeaderCodec.frameType(bb) == LEASE) + .matches(ReferenceCounted::release); + + ByteBuf buffer2 = byteBufAllocator.buffer(); + buffer2.writeCharSequence("test", CharsetUtil.UTF_8); + Payload payload2 = ByteBufPayload.create(buffer2); + + Flux.from(interaction.apply(rSocketResponder, payload2)) + .as(StepVerifier::create) + .expectError(MissingLeaseException.class) + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("interactions") + void expiredLeaseRequestsAreRejected(BiFunction> interaction) { + leaseSender.onNext(Lease.create(50, 1)); + + ByteBuf buffer = byteBufAllocator.buffer(); + buffer.writeCharSequence("test", CharsetUtil.UTF_8); + Payload payload1 = ByteBufPayload.create(buffer); + + Flux.from(interaction.apply(rSocketRequester, payload1)) + .delaySubscription(Duration.ofMillis(100)) + .as(StepVerifier::create) + .expectError(MissingLeaseException.class) + .verify(Duration.ofSeconds(5)); + + Assertions.assertThat(connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> FrameHeaderCodec.frameType(bb) == LEASE) + .matches(ReferenceCounted::release); + + byteBufAllocator.assertHasNoLeaks(); + } + + @Test + void sendLease() { + ByteBuf metadata = byteBufAllocator.buffer(); + Charset utf8 = StandardCharsets.UTF_8; + String metadataContent = "test"; + metadata.writeCharSequence(metadataContent, utf8); + int ttl = 5_000; + int numberOfRequests = 2; + leaseSender.onNext(Lease.create(5_000, 2, metadata)); + + ByteBuf leaseFrame = + connection + .getSent() + .stream() + .filter(f -> FrameHeaderCodec.frameType(f) == FrameType.LEASE) + .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); + } + + @Test + void receiveLease() { + Collection receivedLeases = new ArrayList<>(); + leaseReceiver.subscribe(lease -> receivedLeases.add(lease)); + + ByteBuf metadata = byteBufAllocator.buffer(); + Charset utf8 = StandardCharsets.UTF_8; + String metadataContent = "test"; + metadata.writeCharSequence(metadataContent, utf8); + int ttl = 5_000; + int numberOfRequests = 2; + + ByteBuf leaseFrame = leaseFrame(ttl, numberOfRequests, metadata).retain(1); + + connection.addToReceivedBuffer(leaseFrame); + + Assertions.assertThat(receivedLeases.isEmpty()).isFalse(); + Lease receivedLease = receivedLeases.iterator().next(); + Assertions.assertThat(receivedLease.getTimeToLiveMillis()).isEqualTo(ttl); + Assertions.assertThat(receivedLease.getStartingAllowedRequests()).isEqualTo(numberOfRequests); + Assertions.assertThat(receivedLease.getMetadata().toString(utf8)).isEqualTo(metadataContent); + } + + ByteBuf leaseFrame(int ttl, int requests, ByteBuf metadata) { + return LeaseFrameCodec.encode(byteBufAllocator, ttl, requests, metadata); + } + + static Stream interactions() { + return Stream.of( + Arguments.of( + (BiFunction>) RSocket::fireAndForget, + FrameType.REQUEST_FNF), + Arguments.of( + (BiFunction>) RSocket::requestResponse, + FrameType.REQUEST_RESPONSE), + Arguments.of( + (BiFunction>) RSocket::requestStream, + FrameType.REQUEST_STREAM), + Arguments.of( + (BiFunction>) + (rSocket, payload) -> rSocket.requestChannel(Mono.just(payload)), + FrameType.REQUEST_CHANNEL)); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java new file mode 100644 index 000000000..9ecdd13ba --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java @@ -0,0 +1,165 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.core; + +import static org.junit.Assert.assertEquals; + +import io.rsocket.RSocket; +import io.rsocket.test.util.TestClientTransport; +import io.rsocket.transport.ClientTransport; +import java.io.UncheckedIOException; +import java.time.Duration; +import java.util.Iterator; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.core.Exceptions; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.util.retry.Retry; + +public class RSocketReconnectTest { + + private Queue retries = new ConcurrentLinkedQueue<>(); + + @Test + public void shouldBeASharedReconnectableInstanceOfRSocketMono() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + Schedulers.onScheduleHook( + "test", + r -> + () -> { + r.run(); + latch.countDown(); + }); + TestClientTransport[] testClientTransport = + new TestClientTransport[] {new TestClientTransport()}; + Mono rSocketMono = + RSocketConnector.create() + .reconnect(Retry.indefinitely()) + .connect(() -> testClientTransport[0]); + + RSocket rSocket1 = rSocketMono.block(); + RSocket rSocket2 = rSocketMono.block(); + + Assertions.assertThat(rSocket1).isEqualTo(rSocket2); + + testClientTransport[0].testConnection().dispose(); + Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + testClientTransport[0] = new TestClientTransport(); + + System.out.println("here"); + RSocket rSocket3 = rSocketMono.block(); + RSocket rSocket4 = rSocketMono.block(); + + Assertions.assertThat(rSocket3).isEqualTo(rSocket4).isNotEqualTo(rSocket2); + } + + @Test + @SuppressWarnings({"rawtype", "unchecked"}) + public void shouldBeRetrieableConnectionSharedReconnectableInstanceOfRSocketMono() { + ClientTransport transport = Mockito.mock(ClientTransport.class); + Mockito.when(transport.connect()) + .thenThrow(UncheckedIOException.class) + .thenThrow(UncheckedIOException.class) + .thenThrow(UncheckedIOException.class) + .thenThrow(UncheckedIOException.class) + .thenReturn(new TestClientTransport().connect()); + Mono rSocketMono = + RSocketConnector.create() + .reconnect( + Retry.backoff(4, Duration.ofMillis(100)) + .maxBackoff(Duration.ofMillis(500)) + .doAfterRetry(onRetry())) + .connect(transport); + + RSocket rSocket1 = rSocketMono.block(); + RSocket rSocket2 = rSocketMono.block(); + + Assertions.assertThat(rSocket1).isEqualTo(rSocket2); + assertRetries( + UncheckedIOException.class, + UncheckedIOException.class, + UncheckedIOException.class, + UncheckedIOException.class); + } + + @Test + @SuppressWarnings({"rawtype", "unchecked"}) + public void shouldBeExaustedRetrieableConnectionSharedReconnectableInstanceOfRSocketMono() { + ClientTransport transport = Mockito.mock(ClientTransport.class); + Mockito.when(transport.connect()) + .thenThrow(UncheckedIOException.class) + .thenThrow(UncheckedIOException.class) + .thenThrow(UncheckedIOException.class) + .thenThrow(UncheckedIOException.class) + .thenThrow(UncheckedIOException.class) + .thenReturn(new TestClientTransport().connect()); + Mono rSocketMono = + RSocketConnector.create() + .reconnect( + Retry.backoff(4, Duration.ofMillis(100)) + .maxBackoff(Duration.ofMillis(500)) + .doAfterRetry(onRetry())) + .connect(transport); + + Assertions.assertThatThrownBy(rSocketMono::block) + .matches(Exceptions::isRetryExhausted) + .hasCauseInstanceOf(UncheckedIOException.class); + + Assertions.assertThatThrownBy(rSocketMono::block) + .matches(Exceptions::isRetryExhausted) + .hasCauseInstanceOf(UncheckedIOException.class); + + assertRetries( + UncheckedIOException.class, + UncheckedIOException.class, + UncheckedIOException.class, + UncheckedIOException.class); + } + + @Test + public void shouldBeNotBeASharedReconnectableInstanceOfRSocketMono() { + + Mono rSocketMono = RSocketConnector.connectWith(new TestClientTransport()); + + RSocket rSocket1 = rSocketMono.block(); + RSocket rSocket2 = rSocketMono.block(); + + Assertions.assertThat(rSocket1).isNotEqualTo(rSocket2); + } + + @SafeVarargs + private final void assertRetries(Class... exceptions) { + assertEquals(exceptions.length, retries.size()); + int index = 0; + for (Iterator it = retries.iterator(); it.hasNext(); ) { + Retry.RetrySignal retryContext = it.next(); + assertEquals(index, retryContext.totalRetries()); + assertEquals(exceptions[index], retryContext.failure().getClass()); + index++; + } + } + + Consumer onRetry() { + return context -> retries.add(context); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java new file mode 100644 index 000000000..5949d9ada --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -0,0 +1,152 @@ +/* + * 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 io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.RSocket; +import io.rsocket.TestScheduler; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.frame.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.lease.RequesterLeaseHandler; +import io.rsocket.test.util.TestDuplexConnection; +import io.rsocket.util.DefaultPayload; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.test.util.RaceTestUtils; + +class RSocketRequesterSubscribersTest { + + private static final Set REQUEST_TYPES = + new HashSet<>( + Arrays.asList( + FrameType.METADATA_PUSH, + FrameType.REQUEST_FNF, + FrameType.REQUEST_RESPONSE, + FrameType.REQUEST_STREAM, + FrameType.REQUEST_CHANNEL)); + + private LeaksTrackingByteBufAllocator allocator; + private RSocket rSocketRequester; + private TestDuplexConnection connection; + + @BeforeEach + void setUp() { + allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + connection = new TestDuplexConnection(allocator); + rSocketRequester = + new RSocketRequester( + connection, + PayloadDecoder.DEFAULT, + StreamIdSupplier.clientSupplier(), + 0, + FRAME_LENGTH_MASK, + 0, + 0, + null, + RequesterLeaseHandler.None, + TestScheduler.INSTANCE); + } + + @ParameterizedTest + @MethodSource("allInteractions") + void singleSubscriber(Function> interaction) { + Flux response = Flux.from(interaction.apply(rSocketRequester)); + + AssertSubscriber assertSubscriberA = AssertSubscriber.create(); + AssertSubscriber assertSubscriberB = AssertSubscriber.create(); + + response.subscribe(assertSubscriberA); + response.subscribe(assertSubscriberB); + + connection.addToReceivedBuffer(PayloadFrameCodec.encodeComplete(connection.alloc(), 1)); + + assertSubscriberA.assertTerminated(); + assertSubscriberB.assertTerminated(); + + Assertions.assertThat(requestFramesCount(connection.getSent())).isEqualTo(1); + } + + @ParameterizedTest + @MethodSource("allInteractions") + void singleSubscriberInCaseOfRacing(Function> interaction) { + for (int i = 1; i < 20000; i += 2) { + Flux response = Flux.from(interaction.apply(rSocketRequester)); + AssertSubscriber assertSubscriberA = AssertSubscriber.create(); + AssertSubscriber assertSubscriberB = AssertSubscriber.create(); + + RaceTestUtils.race( + () -> response.subscribe(assertSubscriberA), () -> response.subscribe(assertSubscriberB)); + + connection.addToReceivedBuffer(PayloadFrameCodec.encodeComplete(connection.alloc(), i)); + + assertSubscriberA.assertTerminated(); + assertSubscriberB.assertTerminated(); + + Assertions.assertThat(new AssertSubscriber[] {assertSubscriberA, assertSubscriberB}) + .anySatisfy(as -> as.assertError(IllegalStateException.class)); + + Assertions.assertThat(connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> REQUEST_TYPES.contains(FrameHeaderCodec.frameType(bb))) + .matches(ByteBuf::release); + + connection.clearSendReceiveBuffers(); + } + } + + @ParameterizedTest + @MethodSource("allInteractions") + void singleSubscriberInteractionsAreLazy(Function> interaction) { + Flux response = Flux.from(interaction.apply(rSocketRequester)); + + Assertions.assertThat(connection.getSent().size()).isEqualTo(0); + } + + static long requestFramesCount(Collection frames) { + return frames + .stream() + .filter(frame -> REQUEST_TYPES.contains(FrameHeaderCodec.frameType(frame))) + .count(); + } + + static Stream>> allInteractions() { + return Stream.of( + rSocket -> rSocket.fireAndForget(DefaultPayload.create("test")), + rSocket -> rSocket.requestResponse(DefaultPayload.create("test")), + rSocket -> rSocket.requestStream(DefaultPayload.create("test")), + // rSocket -> rSocket.requestChannel(Mono.just(DefaultPayload.create("test"))), + rSocket -> rSocket.metadataPush(DefaultPayload.create("", "test"))); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java new file mode 100644 index 000000000..de6f86c57 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java @@ -0,0 +1,62 @@ +package io.rsocket.core; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketRequesterTest.ClientSocketRule; +import io.rsocket.util.EmptyPayload; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +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.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 RSocketRequesterTerminationTest(Function> interaction) { + this.interaction = interaction; + } + + @Test + public void testCurrentStreamIsTerminatedOnConnectionClose() { + RSocketRequester rSocket = rule.socket; + + Mono.delay(Duration.ofSeconds(1)).doOnNext(v -> rule.connection.dispose()).subscribe(); + + StepVerifier.create(interaction.apply(rSocket)) + .expectError(ClosedChannelException.class) + .verify(Duration.ofSeconds(5)); + } + + @Test + public void testSubsequentStreamIsTerminatedAfterConnectionClose() { + RSocketRequester rSocket = rule.socket; + + rule.connection.dispose(); + StepVerifier.create(interaction.apply(rSocket)) + .expectError(ClosedChannelException.class) + .verify(Duration.ofSeconds(5)); + } + + @Parameterized.Parameters + public static Iterable>> rsocketInteractions() { + EmptyPayload payload = EmptyPayload.INSTANCE; + Publisher payloadStream = Flux.just(payload); + + Function> resp = rSocket -> rSocket.requestResponse(payload); + Function> stream = rSocket -> rSocket.requestStream(payload); + Function> channel = rSocket -> rSocket.requestChannel(payloadStream); + + 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 new file mode 100644 index 000000000..f93d55570 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -0,0 +1,1219 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; +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 org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.netty.util.IllegalReferenceCountException; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.RaceTestConstants; +import io.rsocket.TestScheduler; +import io.rsocket.exceptions.ApplicationErrorException; +import io.rsocket.exceptions.CustomRSocketException; +import io.rsocket.exceptions.RejectedSetupException; +import io.rsocket.frame.CancelFrameCodec; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameLengthCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; +import io.rsocket.frame.RequestNFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.subscriber.AssertSubscriber; +import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.test.util.TestSubscriber; +import io.rsocket.util.ByteBufPayload; +import io.rsocket.util.DefaultPayload; +import io.rsocket.util.EmptyPayload; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +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; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.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.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.test.StepVerifier; +import reactor.test.publisher.TestPublisher; +import reactor.test.util.RaceTestUtils; + +public class RSocketRequesterTest { + + ClientSocketRule rule; + + @BeforeEach + 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(); + } + + @AfterEach + public void tearDown() { + Hooks.resetOnErrorDropped(); + Hooks.resetOnNextDropped(); + } + + @Test + @Timeout(2_000) + public void testInvalidFrameOnStream0ShouldNotTerminateRSocket() { + rule.connection.addToReceivedBuffer(RequestNFrameCodec.encode(rule.alloc(), 0, 10)); + Assertions.assertThat(rule.socket.isDisposed()).isFalse(); + rule.assertHasNoLeaks(); + } + + @Test + @Timeout(2_000) + public void testStreamInitialN() { + Flux stream = rule.socket.requestStream(EmptyPayload.INSTANCE); + + BaseSubscriber subscriber = + new BaseSubscriber() { + @Override + protected void hookOnSubscribe(Subscription subscription) { + // don't request here + } + }; + stream.subscribe(subscriber); + + Assertions.assertThat(rule.connection.getSent()).isEmpty(); + + subscriber.request(5); + + List sent = new ArrayList<>(rule.connection.getSent()); + + assertThat("sent frame count", sent.size(), is(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)); + rule.assertHasNoLeaks(); + } + + @Test + @Timeout(2_000) + public void testHandleSetupException() { + rule.connection.addToReceivedBuffer( + ErrorFrameCodec.encode(rule.alloc(), 0, new RejectedSetupException("boom"))); + Assertions.assertThatThrownBy(() -> rule.socket.onClose().block()) + .isInstanceOf(RejectedSetupException.class); + rule.assertHasNoLeaks(); + } + + @Test + @Timeout(2_000) + public void testHandleApplicationException() { + rule.connection.clearSendReceiveBuffers(); + Publisher response = rule.socket.requestResponse(EmptyPayload.INSTANCE); + Subscriber responseSub = TestSubscriber.create(); + response.subscribe(responseSub); + + int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); + rule.connection.addToReceivedBuffer( + ErrorFrameCodec.encode(rule.alloc(), streamId, new ApplicationErrorException("error"))); + + verify(responseSub).onError(any(ApplicationErrorException.class)); + + Assertions.assertThat(rule.connection.getSent()) + // requestResponseFrame + .hasSize(1) + .allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + + @Test + @Timeout(2_000) + public void testHandleValidFrame() { + Publisher response = rule.socket.requestResponse(EmptyPayload.INSTANCE); + Subscriber sub = TestSubscriber.create(); + response.subscribe(sub); + + int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); + rule.connection.addToReceivedBuffer( + PayloadFrameCodec.encodeNextReleasingPayload( + rule.alloc(), streamId, EmptyPayload.INSTANCE)); + + verify(sub).onComplete(); + Assertions.assertThat(rule.connection.getSent()).hasSize(1).allMatch(ReferenceCounted::release); + rule.assertHasNoLeaks(); + } + + @Test + @Timeout(2_000) + public void testRequestReplyWithCancel() { + Mono response = rule.socket.requestResponse(EmptyPayload.INSTANCE); + + try { + response.block(Duration.ofMillis(100)); + } catch (IllegalStateException ise) { + } + + List sent = new ArrayList<>(rule.connection.getSent()); + + assertThat( + "Unexpected frame sent on the connection.", frameType(sent.get(0)), is(REQUEST_RESPONSE)); + assertThat("Unexpected frame sent on the connection.", frameType(sent.get(1)), is(CANCEL)); + Assertions.assertThat(sent).hasSize(2).allMatch(ReferenceCounted::release); + rule.assertHasNoLeaks(); + } + + @Test + @Disabled("invalid") + @Timeout(2_000) + public void testRequestReplyErrorOnSend() { + rule.connection.setAvailability(0); // Fails send + Mono response = rule.socket.requestResponse(EmptyPayload.INSTANCE); + Subscriber responseSub = TestSubscriber.create(10); + response.subscribe(responseSub); + + this.rule + .socket + .onClose() + .as(StepVerifier::create) + .expectComplete() + .verify(Duration.ofMillis(100)); + + verify(responseSub).onSubscribe(any(Subscription.class)); + + rule.assertHasNoLeaks(); + // TODO this should get the error reported through the response subscription + // verify(responseSub).onError(any(RuntimeException.class)); + } + + @Test + @Timeout(2_000) + public void testChannelRequestCancellation() { + MonoProcessor cancelled = MonoProcessor.create(); + Flux request = Flux.never().doOnCancel(cancelled::onComplete); + rule.socket.requestChannel(request).subscribe().dispose(); + Flux.first( + cancelled, + Flux.error(new IllegalStateException("Channel request not cancelled")) + .delaySubscription(Duration.ofSeconds(1))) + .blockFirst(); + rule.assertHasNoLeaks(); + } + + @Test + @Timeout(2_000) + public void testChannelRequestCancellation2() { + MonoProcessor cancelled = MonoProcessor.create(); + Flux request = + Flux.just(EmptyPayload.INSTANCE).repeat(259).doOnCancel(cancelled::onComplete); + rule.socket.requestChannel(request).subscribe().dispose(); + Flux.first( + cancelled, + Flux.error(new IllegalStateException("Channel request not cancelled")) + .delaySubscription(Duration.ofSeconds(1))) + .blockFirst(); + Assertions.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); + 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.error(new IllegalStateException("Channel request not cancelled")) + .delaySubscription(Duration.ofSeconds(1))) + .blockFirst(); + + Assertions.assertThat(request.isDisposed()).isTrue(); + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> frameType(bb) == REQUEST_CHANNEL) + .matches(ReferenceCounted::release); + rule.assertHasNoLeaks(); + } + + @Test + public void testCorrectFrameOrder() { + MonoProcessor delayer = MonoProcessor.create(); + BaseSubscriber subscriber = + new BaseSubscriber() { + @Override + protected void hookOnSubscribe(Subscription subscription) {} + }; + rule.socket + .requestChannel( + Flux.concat(Flux.just(0).delayUntil(i -> delayer), Flux.range(1, 999)) + .map(i -> DefaultPayload.create(i + ""))) + .subscribe(subscriber); + + subscriber.request(1); + subscriber.request(Long.MAX_VALUE); + delayer.onComplete(); + + Iterator iterator = rule.connection.getSent().iterator(); + + ByteBuf initialFrame = iterator.next(); + + Assertions.assertThat(FrameHeaderCodec.frameType(initialFrame)).isEqualTo(REQUEST_CHANNEL); + Assertions.assertThat(RequestChannelFrameCodec.initialRequestN(initialFrame)) + .isEqualTo(Long.MAX_VALUE); + Assertions.assertThat(RequestChannelFrameCodec.data(initialFrame).toString(CharsetUtil.UTF_8)) + .isEqualTo("0"); + Assertions.assertThat(initialFrame.release()).isTrue(); + + Assertions.assertThat(iterator.hasNext()).isFalse(); + rule.assertHasNoLeaks(); + } + + @ParameterizedTest + @ValueSource(ints = {128, 256, FRAME_LENGTH_MASK}) + public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmentation( + 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); + StepVerifier.create( + generator.apply(rule.socket, DefaultPayload.create(data, metadata))) + .expectSubscription() + .expectErrorSatisfies( + t -> + Assertions.assertThat(t) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_PAYLOAD_ERROR_MESSAGE)) + .verify(); + rule.assertHasNoLeaks(); + }); + } + + static Stream>> prepareCalls() { + return Stream.of( + RSocket::fireAndForget, + RSocket::requestResponse, + RSocket::requestStream, + (rSocket, payload) -> rSocket.requestChannel(Flux.just(payload)), + RSocket::metadataPush); + } + + @Test + public void + shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmentationForRequestChannelCase() { + byte[] metadata = new byte[FrameLengthCodec.FRAME_LENGTH_MASK]; + byte[] data = new byte[FrameLengthCodec.FRAME_LENGTH_MASK]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + StepVerifier.create( + rule.socket.requestChannel( + Flux.just(EmptyPayload.INSTANCE, DefaultPayload.create(data, metadata)))) + .expectSubscription() + .then( + () -> + rule.connection.addToReceivedBuffer( + RequestNFrameCodec.encode( + rule.alloc(), rule.getStreamIdForRequestType(REQUEST_CHANNEL), 2))) + .expectErrorSatisfies( + t -> + Assertions.assertThat(t) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_PAYLOAD_ERROR_MESSAGE)) + .verify(); + Assertions.assertThat(rule.connection.getSent()) + // expect to be sent RequestChannelFrame + // expect to be sent CancelFrame + .hasSize(2) + .allMatch(ReferenceCounted::release); + rule.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("racingCases") + public void checkNoLeaksOnRacing( + Function> initiator, + BiConsumer, ClientSocketRule> runner) { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + ClientSocketRule clientSocketRule = new ClientSocketRule(); + try { + clientSocketRule + .apply( + new Statement() { + @Override + public void evaluate() {} + }, + null) + .evaluate(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + + Publisher payloadP = initiator.apply(clientSocketRule); + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + + if (payloadP instanceof Flux) { + ((Flux) payloadP).doOnNext(Payload::release).subscribe(assertSubscriber); + } else { + ((Mono) payloadP).doOnNext(Payload::release).subscribe(assertSubscriber); + } + + runner.accept(assertSubscriber, clientSocketRule); + + Assertions.assertThat(clientSocketRule.connection.getSent()) + .allMatch(ReferenceCounted::release); + + clientSocketRule.assertHasNoLeaks(); + } + } + + private static Stream racingCases() { + return Stream.of( + Arguments.of( + (Function>) + (rule) -> rule.socket.requestStream(EmptyPayload.INSTANCE), + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + ByteBufAllocator allocator = rule.alloc(); + ByteBuf metadata = allocator.buffer(); + metadata.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data = allocator.buffer(); + data.writeCharSequence("def", CharsetUtil.UTF_8); + as.request(1); + int streamId = rule.getStreamIdForRequestType(REQUEST_STREAM); + ByteBuf frame = + PayloadFrameCodec.encode( + allocator, streamId, false, false, true, metadata, data); + + RaceTestUtils.race(as::cancel, () -> rule.connection.addToReceivedBuffer(frame)); + }), + Arguments.of( + (Function>) + (rule) -> rule.socket.requestChannel(Flux.just(EmptyPayload.INSTANCE)), + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + ByteBufAllocator allocator = rule.alloc(); + ByteBuf metadata = allocator.buffer(); + metadata.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data = allocator.buffer(); + data.writeCharSequence("def", CharsetUtil.UTF_8); + as.request(1); + int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); + ByteBuf frame = + PayloadFrameCodec.encode( + allocator, streamId, false, false, true, metadata, data); + + RaceTestUtils.race(as::cancel, () -> rule.connection.addToReceivedBuffer(frame)); + }), + Arguments.of( + (Function>) + (rule) -> { + ByteBufAllocator allocator = rule.alloc(); + ByteBuf metadata = allocator.buffer(); + metadata.writeCharSequence("metadata", CharsetUtil.UTF_8); + ByteBuf data = allocator.buffer(); + data.writeCharSequence("data", CharsetUtil.UTF_8); + final Payload payload = ByteBufPayload.create(data, metadata); + + return rule.socket.requestStream(payload); + }, + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + RaceTestUtils.race(() -> as.request(1), as::cancel); + // ensures proper frames order + if (rule.connection.getSent().size() > 0) { + Assertions.assertThat(rule.connection.getSent()).hasSize(2); + Assertions.assertThat(rule.connection.getSent()) + .element(0) + .matches( + bb -> frameType(bb) == REQUEST_STREAM, + "Expected first frame matches {" + + REQUEST_STREAM + + "} but was {" + + frameType(rule.connection.getSent().stream().findFirst().get()) + + "}"); + Assertions.assertThat(rule.connection.getSent()) + .element(1) + .matches( + bb -> frameType(bb) == CANCEL, + "Expected first frame matches {" + + CANCEL + + "} but was {" + + frameType( + rule.connection.getSent().stream().skip(1).findFirst().get()) + + "}"); + } + }), + Arguments.of( + (Function>) + (rule) -> { + ByteBufAllocator allocator = rule.alloc(); + return rule.socket.requestChannel( + Flux.generate( + () -> 1L, + (index, sink) -> { + ByteBuf metadata = allocator.buffer(); + metadata.writeCharSequence("metadata", CharsetUtil.UTF_8); + ByteBuf data = allocator.buffer(); + data.writeCharSequence("data", CharsetUtil.UTF_8); + final Payload payload = ByteBufPayload.create(data, metadata); + sink.next(payload); + sink.complete(); + return ++index; + })); + }, + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + RaceTestUtils.race(() -> as.request(1), as::cancel); + // ensures proper frames order + if (rule.connection.getSent().size() > 0) { + // + // Assertions.assertThat(rule.connection.getSent()).hasSize(2); + Assertions.assertThat(rule.connection.getSent()) + .element(0) + .matches( + bb -> frameType(bb) == REQUEST_CHANNEL, + "Expected first frame matches {" + + REQUEST_CHANNEL + + "} but was {" + + frameType(rule.connection.getSent().stream().findFirst().get()) + + "}"); + Assertions.assertThat(rule.connection.getSent()) + .element(1) + .matches( + bb -> frameType(bb) == CANCEL, + "Expected first frame matches {" + + CANCEL + + "} but was {" + + frameType( + rule.connection.getSent().stream().skip(1).findFirst().get()) + + "}"); + } + }), + Arguments.of( + (Function>) + (rule) -> + rule.socket.requestChannel( + Flux.generate( + () -> 1L, + (index, sink) -> { + ByteBuf data = rule.alloc().buffer(); + data.writeCharSequence("d" + index, CharsetUtil.UTF_8); + ByteBuf metadata = rule.alloc().buffer(); + metadata.writeCharSequence("m" + index, CharsetUtil.UTF_8); + final Payload payload = ByteBufPayload.create(data, metadata); + sink.next(payload); + return ++index; + })), + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + ByteBufAllocator allocator = rule.alloc(); + as.request(1); + int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); + ByteBuf frame = CancelFrameCodec.encode(allocator, streamId); + + RaceTestUtils.race( + () -> as.request(Long.MAX_VALUE), + () -> rule.connection.addToReceivedBuffer(frame)); + }), + Arguments.of( + (Function>) + (rule) -> + rule.socket.requestChannel( + Flux.generate( + () -> 1L, + (index, sink) -> { + ByteBuf data = rule.alloc().buffer(); + data.writeCharSequence("d" + index, CharsetUtil.UTF_8); + ByteBuf metadata = rule.alloc().buffer(); + metadata.writeCharSequence("m" + index, CharsetUtil.UTF_8); + final Payload payload = ByteBufPayload.create(data, metadata); + sink.next(payload); + return ++index; + })), + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + ByteBufAllocator allocator = rule.alloc(); + as.request(1); + int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); + ByteBuf frame = + ErrorFrameCodec.encode(allocator, streamId, new RuntimeException("test")); + + RaceTestUtils.race( + () -> as.request(Long.MAX_VALUE), + () -> rule.connection.addToReceivedBuffer(frame)); + }), + Arguments.of( + (Function>) + (rule) -> { + ByteBuf data = rule.allocator.buffer(); + data.writeCharSequence("testData", CharsetUtil.UTF_8); + + ByteBuf metadata = rule.allocator.buffer(); + metadata.writeCharSequence("testMetadata", CharsetUtil.UTF_8); + Payload requestPayload = ByteBufPayload.create(data, metadata); + return rule.socket.requestResponse(requestPayload); + }, + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + ByteBufAllocator allocator = rule.alloc(); + ByteBuf metadata = allocator.buffer(); + metadata.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data = allocator.buffer(); + data.writeCharSequence("def", CharsetUtil.UTF_8); + as.request(Long.MAX_VALUE); + int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); + ByteBuf frame = + PayloadFrameCodec.encode( + allocator, streamId, false, false, true, metadata, data); + + RaceTestUtils.race(as::cancel, () -> rule.connection.addToReceivedBuffer(frame)); + }), + Arguments.of( + (Function>) + (rule) -> { + ByteBuf data = rule.allocator.buffer(); + data.writeCharSequence("testData", CharsetUtil.UTF_8); + + ByteBuf metadata = rule.allocator.buffer(); + metadata.writeCharSequence("testMetadata", CharsetUtil.UTF_8); + Payload requestPayload = ByteBufPayload.create(data, metadata); + return rule.socket.requestStream(requestPayload); + }, + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + ByteBufAllocator allocator = rule.alloc(); + ByteBuf metadata = allocator.buffer(); + metadata.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data = allocator.buffer(); + data.writeCharSequence("def", CharsetUtil.UTF_8); + as.request(Long.MAX_VALUE); + int streamId = rule.getStreamIdForRequestType(REQUEST_STREAM); + ByteBuf frame = + PayloadFrameCodec.encode( + allocator, streamId, false, true, true, metadata, data); + + RaceTestUtils.race(as::cancel, () -> rule.connection.addToReceivedBuffer(frame)); + })); + } + + @Test + public void simpleOnDiscardRequestChannelTest() { + AssertSubscriber assertSubscriber = AssertSubscriber.create(1); + TestPublisher testPublisher = TestPublisher.create(); + + Flux payloadFlux = rule.socket.requestChannel(testPublisher); + + payloadFlux.subscribe(assertSubscriber); + + testPublisher.next( + ByteBufPayload.create("d", "m"), + ByteBufPayload.create("d1", "m1"), + ByteBufPayload.create("d2", "m2")); + + assertSubscriber.cancel(); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); + + rule.assertHasNoLeaks(); + } + + @Test + public void simpleOnDiscardRequestChannelTest2() { + ByteBufAllocator allocator = rule.alloc(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(1); + TestPublisher testPublisher = TestPublisher.create(); + + Flux payloadFlux = rule.socket.requestChannel(testPublisher); + + payloadFlux.subscribe(assertSubscriber); + + testPublisher.next(ByteBufPayload.create("d", "m")); + + int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); + testPublisher.next(ByteBufPayload.create("d1", "m1"), ByteBufPayload.create("d2", "m2")); + + rule.connection.addToReceivedBuffer( + ErrorFrameCodec.encode( + allocator, streamId, new CustomRSocketException(0x00000404, "test"))); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); + + rule.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("encodeDecodePayloadCases") + public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( + FrameType frameType, int framesCnt, int responsesCnt) { + ByteBufAllocator allocator = rule.alloc(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(responsesCnt); + TestPublisher testPublisher = TestPublisher.create(); + + Publisher response; + + switch (frameType) { + case REQUEST_FNF: + response = + testPublisher.mono().flatMap(p -> rule.socket.fireAndForget(p).then(Mono.empty())); + break; + case REQUEST_RESPONSE: + response = testPublisher.mono().flatMap(p -> rule.socket.requestResponse(p)); + break; + case REQUEST_STREAM: + response = testPublisher.mono().flatMapMany(p -> rule.socket.requestStream(p)); + break; + case REQUEST_CHANNEL: + response = rule.socket.requestChannel(testPublisher.flux()); + break; + default: + throw new UnsupportedOperationException("illegal case"); + } + + response.subscribe(assertSubscriber); + testPublisher.next(ByteBufPayload.create("d")); + + int streamId = rule.getStreamIdForRequestType(frameType); + + if (responsesCnt > 0) { + for (int i = 0; i < responsesCnt - 1; i++) { + rule.connection.addToReceivedBuffer( + PayloadFrameCodec.encode( + allocator, + streamId, + false, + false, + true, + null, + Unpooled.wrappedBuffer(("rd" + (i + 1)).getBytes()))); + } + + rule.connection.addToReceivedBuffer( + PayloadFrameCodec.encode( + allocator, + streamId, + false, + true, + true, + null, + Unpooled.wrappedBuffer(("rd" + responsesCnt).getBytes()))); + } + + if (framesCnt > 1) { + rule.connection.addToReceivedBuffer( + RequestNFrameCodec.encode(allocator, streamId, framesCnt)); + } + + for (int i = 1; i < framesCnt; i++) { + testPublisher.next(ByteBufPayload.create("d" + i)); + } + + Assertions.assertThat(rule.connection.getSent()) + .describedAs( + "Interaction Type :[%s]. Expected to observe %s frames sent", frameType, framesCnt) + .hasSize(framesCnt) + .allMatch(bb -> !FrameHeaderCodec.hasMetadata(bb)) + .allMatch(ByteBuf::release); + + Assertions.assertThat(assertSubscriber.isTerminated()) + .describedAs("Interaction Type :[%s]. Expected to be terminated", frameType) + .isTrue(); + + Assertions.assertThat(assertSubscriber.values()) + .describedAs( + "Interaction Type :[%s]. Expected to observe %s frames received", + frameType, responsesCnt) + .hasSize(responsesCnt) + .allMatch(p -> !p.hasMetadata()) + .allMatch(p -> p.release()); + + rule.assertHasNoLeaks(); + rule.connection.clearSendReceiveBuffers(); + } + + static Stream encodeDecodePayloadCases() { + return Stream.of( + Arguments.of(REQUEST_FNF, 1, 0), + Arguments.of(REQUEST_RESPONSE, 1, 1), + Arguments.of(REQUEST_STREAM, 1, 5), + Arguments.of(REQUEST_CHANNEL, 5, 5)); + } + + @ParameterizedTest + @MethodSource("refCntCases") + public void ensureSendsErrorOnIllegalRefCntPayload( + BiFunction> sourceProducer) { + Payload invalidPayload = ByteBufPayload.create("test", "test"); + invalidPayload.release(); + + Publisher source = sourceProducer.apply(invalidPayload, rule.socket); + + StepVerifier.create(source, 0) + .expectError(IllegalReferenceCountException.class) + .verify(Duration.ofMillis(100)); + } + + private static Stream>> refCntCases() { + return Stream.of( + (p, r) -> r.fireAndForget(p), + (p, r) -> r.requestResponse(p), + (p, r) -> r.requestStream(p), + (p, r) -> r.requestChannel(Mono.just(p)), + (p, r) -> + r.requestChannel(Flux.just(EmptyPayload.INSTANCE, p).doOnSubscribe(s -> s.request(1)))); + } + + @Test + public void ensuresThatNoOpsMustHappenUntilSubscriptionInCaseOfFnfCall() { + Payload payload1 = ByteBufPayload.create("abc1"); + Mono fnf1 = rule.socket.fireAndForget(payload1); + + Payload payload2 = ByteBufPayload.create("abc2"); + Mono fnf2 = rule.socket.fireAndForget(payload2); + + Assertions.assertThat(rule.connection.getSent()).isEmpty(); + + // checks that fnf2 should have id 1 even though it was generated later than fnf1 + AssertSubscriber voidAssertSubscriber2 = fnf2.subscribeWith(AssertSubscriber.create(0)); + voidAssertSubscriber2.assertTerminated().assertNoError(); + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> frameType(bb) == REQUEST_FNF) + .matches(bb -> FrameHeaderCodec.streamId(bb) == 1) + // ensures that this is fnf1 with abc2 data + .matches( + bb -> + ByteBufUtil.equals( + RequestFireAndForgetFrameCodec.data(bb), + Unpooled.wrappedBuffer("abc2".getBytes()))) + .matches(ReferenceCounted::release); + + rule.connection.clearSendReceiveBuffers(); + + // checks that fnf1 should have id 3 even though it was generated earlier + AssertSubscriber voidAssertSubscriber1 = fnf1.subscribeWith(AssertSubscriber.create(0)); + voidAssertSubscriber1.assertTerminated().assertNoError(); + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> frameType(bb) == REQUEST_FNF) + .matches(bb -> FrameHeaderCodec.streamId(bb) == 3) + // ensures that this is fnf1 with abc1 data + .matches( + bb -> + ByteBufUtil.equals( + RequestFireAndForgetFrameCodec.data(bb), + Unpooled.wrappedBuffer("abc1".getBytes()))) + .matches(ReferenceCounted::release); + } + + @ParameterizedTest + @MethodSource("requestNInteractions") + public void ensuresThatNoOpsMustHappenUntilFirstRequestN( + FrameType frameType, BiFunction> interaction) { + Payload payload1 = ByteBufPayload.create("abc1"); + Publisher interaction1 = interaction.apply(rule, payload1); + + Payload payload2 = ByteBufPayload.create("abc2"); + Publisher interaction2 = interaction.apply(rule, payload2); + + Assertions.assertThat(rule.connection.getSent()).isEmpty(); + + AssertSubscriber assertSubscriber1 = AssertSubscriber.create(0); + interaction1.subscribe(assertSubscriber1); + AssertSubscriber assertSubscriber2 = AssertSubscriber.create(0); + interaction2.subscribe(assertSubscriber2); + assertSubscriber1.assertNotTerminated().assertNoError(); + assertSubscriber2.assertNotTerminated().assertNoError(); + // even though we subscribed, nothing should happen until the first requestN + Assertions.assertThat(rule.connection.getSent()).isEmpty(); + + // first request on the second interaction to ensure that stream id issuing on the first request + assertSubscriber2.request(1); + + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> frameType(bb) == frameType) + .matches( + bb -> FrameHeaderCodec.streamId(bb) == 1, + "Expected to have stream ID {1} but got {" + + FrameHeaderCodec.streamId(rule.connection.getSent().iterator().next()) + + "}") + .matches( + bb -> { + switch (frameType) { + case REQUEST_RESPONSE: + return ByteBufUtil.equals( + RequestResponseFrameCodec.data(bb), + Unpooled.wrappedBuffer("abc2".getBytes())); + case REQUEST_STREAM: + return ByteBufUtil.equals( + RequestStreamFrameCodec.data(bb), Unpooled.wrappedBuffer("abc2".getBytes())); + case REQUEST_CHANNEL: + return ByteBufUtil.equals( + RequestChannelFrameCodec.data(bb), Unpooled.wrappedBuffer("abc2".getBytes())); + } + + return false; + }) + .matches(ReferenceCounted::release); + + rule.connection.clearSendReceiveBuffers(); + + assertSubscriber1.request(1); + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> frameType(bb) == frameType) + .matches( + bb -> FrameHeaderCodec.streamId(bb) == 3, + "Expected to have stream ID {1} but got {" + + FrameHeaderCodec.streamId(rule.connection.getSent().iterator().next()) + + "}") + .matches( + bb -> { + switch (frameType) { + case REQUEST_RESPONSE: + return ByteBufUtil.equals( + RequestResponseFrameCodec.data(bb), + Unpooled.wrappedBuffer("abc1".getBytes())); + case REQUEST_STREAM: + return ByteBufUtil.equals( + RequestStreamFrameCodec.data(bb), Unpooled.wrappedBuffer("abc1".getBytes())); + case REQUEST_CHANNEL: + return ByteBufUtil.equals( + RequestChannelFrameCodec.data(bb), Unpooled.wrappedBuffer("abc1".getBytes())); + } + + return false; + }) + .matches(ReferenceCounted::release); + } + + private static Stream requestNInteractions() { + return Stream.of( + Arguments.of( + REQUEST_RESPONSE, + (BiFunction>) + (rule, payload) -> rule.socket.requestResponse(payload)), + Arguments.of( + REQUEST_STREAM, + (BiFunction>) + (rule, payload) -> rule.socket.requestStream(payload)), + Arguments.of( + REQUEST_CHANNEL, + (BiFunction>) + (rule, payload) -> rule.socket.requestChannel(Flux.just(payload)))); + } + + @ParameterizedTest + @MethodSource("streamRacingCases") + public void ensuresCorrectOrderOfStreamIdIssuingInCaseOfRacing( + BiFunction> interaction1, + BiFunction> interaction2, + FrameType interactionType1, + FrameType interactionType2) { + Assumptions.assumeThat(interactionType1).isNotEqualTo(METADATA_PUSH); + Assumptions.assumeThat(interactionType2).isNotEqualTo(METADATA_PUSH); + 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); + RaceTestUtils.race( + () -> publisher1.subscribe(AssertSubscriber.create()), + () -> publisher2.subscribe(AssertSubscriber.create())); + + Assertions.assertThat(rule.connection.getSent()) + .extracting(FrameHeaderCodec::streamId) + .containsExactly(i, i + 2); + rule.connection.getSent().forEach(bb -> bb.release()); + rule.connection.getSent().clear(); + } + } + + public static Stream streamRacingCases() { + return Stream.of( + Arguments.of( + (BiFunction>) + (r, p) -> r.socket.fireAndForget(p), + (BiFunction>) + (r, p) -> r.socket.requestResponse(p), + REQUEST_FNF, + REQUEST_RESPONSE), + Arguments.of( + (BiFunction>) + (r, p) -> r.socket.requestResponse(p), + (BiFunction>) + (r, p) -> r.socket.requestStream(p), + REQUEST_RESPONSE, + REQUEST_STREAM), + Arguments.of( + (BiFunction>) + (r, p) -> r.socket.requestStream(p), + (BiFunction>) + (r, p) -> { + AtomicBoolean subscribed = new AtomicBoolean(); + Flux just = Flux.just(p).doOnSubscribe((__) -> subscribed.set(true)); + return r.socket + .requestChannel(just) + .doFinally( + __ -> { + if (!subscribed.get()) { + p.release(); + } + }); + }, + REQUEST_STREAM, + REQUEST_CHANNEL), + Arguments.of( + (BiFunction>) + (r, p) -> { + AtomicBoolean subscribed = new AtomicBoolean(); + Flux just = Flux.just(p).doOnSubscribe((__) -> subscribed.set(true)); + return r.socket + .requestChannel(just) + .doFinally( + __ -> { + if (!subscribed.get()) { + p.release(); + } + }); + }, + (BiFunction>) + (r, p) -> r.socket.fireAndForget(p), + REQUEST_CHANNEL, + REQUEST_FNF), + Arguments.of( + (BiFunction>) + (r, p) -> r.socket.metadataPush(p), + (BiFunction>) + (r, p) -> r.socket.fireAndForget(p), + METADATA_PUSH, + REQUEST_FNF)); + } + + @ParameterizedTest + @MethodSource("streamRacingCases") + @SuppressWarnings({"rawtypes", "unchecked"}) + public void shouldTerminateAllStreamsIfThereRacingBetweenDisposeAndRequests( + BiFunction> interaction1, + BiFunction> interaction2, + FrameType interactionType1, + FrameType interactionType2) { + for (int i = 1; i < RaceTestConstants.REPEATS; i++) { + Payload payload1 = ByteBufPayload.create("test", "test"); + Payload payload2 = ByteBufPayload.create("test", "test"); + AssertSubscriber assertSubscriber1 = AssertSubscriber.create(); + AssertSubscriber assertSubscriber2 = AssertSubscriber.create(); + Publisher publisher1 = interaction1.apply(rule, payload1); + Publisher publisher2 = interaction2.apply(rule, payload2); + RaceTestUtils.race( + () -> rule.socket.dispose(), + () -> publisher1.subscribe(assertSubscriber1), + () -> publisher2.subscribe(assertSubscriber2)); + + assertSubscriber1.await().assertTerminated(); + if (interactionType1 != REQUEST_FNF && interactionType1 != METADATA_PUSH) { + assertSubscriber1.assertError(ClosedChannelException.class); + } else { + try { + assertSubscriber1.assertError(ClosedChannelException.class); + } catch (Throwable t) { + // fnf call may be completed + assertSubscriber1.assertComplete(); + } + } + assertSubscriber2.await().assertTerminated(); + if (interactionType2 != REQUEST_FNF && interactionType2 != METADATA_PUSH) { + assertSubscriber2.assertError(ClosedChannelException.class); + } else { + try { + assertSubscriber2.assertError(ClosedChannelException.class); + } catch (Throwable t) { + // fnf call may be completed + assertSubscriber2.assertComplete(); + } + } + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + rule.connection.getSent().clear(); + + Assertions.assertThat(payload1.refCnt()).isZero(); + Assertions.assertThat(payload2.refCnt()).isZero(); + } + } + + @Test + // see https://github.com/rsocket/rsocket-java/issues/858 + public void testWorkaround858() { + ByteBuf buffer = rule.alloc().buffer(); + buffer.writeCharSequence("test", CharsetUtil.UTF_8); + + rule.socket.requestResponse(ByteBufPayload.create(buffer)).subscribe(); + + rule.connection.addToReceivedBuffer( + ErrorFrameCodec.encode(rule.alloc(), 1, new RuntimeException("test"))); + + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> FrameHeaderCodec.frameType(bb) == REQUEST_RESPONSE) + .matches(ByteBuf::release); + + Assertions.assertThat(rule.socket.isDisposed()).isFalse(); + + rule.assertHasNoLeaks(); + } + + @ParameterizedTest + @ValueSource(strings = {"stream", "channel"}) + // see https://github.com/rsocket/rsocket-java/issues/959 + public void testWorkaround959(String type) { + for (int i = 1; i < 20000; i += 2) { + ByteBuf buffer = rule.alloc().buffer(); + buffer.writeCharSequence("test", CharsetUtil.UTF_8); + + final AssertSubscriber assertSubscriber = new AssertSubscriber<>(3); + if (type.equals("stream")) { + rule.socket.requestStream(ByteBufPayload.create(buffer)).subscribe(assertSubscriber); + } else if (type.equals("channel")) { + rule.socket + .requestChannel(Flux.just(ByteBufPayload.create(buffer))) + .subscribe(assertSubscriber); + } + + final ByteBuf payloadFrame = + PayloadFrameCodec.encode( + rule.alloc(), i, false, false, true, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); + + RaceTestUtils.race( + () -> { + rule.connection.addToReceivedBuffer(payloadFrame.copy()); + rule.connection.addToReceivedBuffer(payloadFrame.copy()); + rule.connection.addToReceivedBuffer(payloadFrame); + }, + () -> { + assertSubscriber.request(1); + assertSubscriber.request(1); + assertSubscriber.request(1); + }); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); + + Assertions.assertThat(rule.socket.isDisposed()).isFalse(); + + assertSubscriber.values().forEach(ReferenceCountUtil::safeRelease); + assertSubscriber.assertNoError(); + + rule.connection.clearSendReceiveBuffers(); + rule.assertHasNoLeaks(); + } + } + + public static class ClientSocketRule extends AbstractSocketRule { + @Override + protected RSocketRequester newRSocket() { + return new RSocketRequester( + connection, + PayloadDecoder.ZERO_COPY, + StreamIdSupplier.clientSupplier(), + 0, + maxFrameLength, + Integer.MAX_VALUE, + Integer.MAX_VALUE, + null, + RequesterLeaseHandler.None, + TestScheduler.INSTANCE); + } + + public int getStreamIdForRequestType(FrameType expectedFrameType) { + assertThat("Unexpected frames sent.", connection.getSent(), hasSize(greaterThanOrEqualTo(1))); + List framesFound = new ArrayList<>(); + for (ByteBuf frame : connection.getSent()) { + FrameType frameType = frameType(frame); + if (frameType == expectedFrameType) { + return FrameHeaderCodec.streamId(frame); + } + framesFound.add(frameType); + } + throw new AssertionError( + "No frames sent with frame type: " + + expectedFrameType + + ", frames found: " + + framesFound); + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java new file mode 100644 index 000000000..2c1335fcb --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -0,0 +1,882 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; +import static io.rsocket.frame.FrameHeaderCodec.frameType; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; +import static io.rsocket.frame.FrameType.ERROR; +import static io.rsocket.frame.FrameType.REQUEST_CHANNEL; +import static io.rsocket.frame.FrameType.REQUEST_FNF; +import static io.rsocket.frame.FrameType.REQUEST_N; +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.hasSize; +import static org.hamcrest.Matchers.is; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.RaceTestConstants; +import io.rsocket.frame.CancelFrameCodec; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.KeepAliveFrameCodec; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; +import io.rsocket.frame.RequestNFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.subscriber.AssertSubscriber; +import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.test.util.TestDuplexConnection; +import io.rsocket.test.util.TestSubscriber; +import io.rsocket.util.ByteBufPayload; +import io.rsocket.util.DefaultPayload; +import io.rsocket.util.EmptyPayload; +import java.util.Collection; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.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.CoreSubscriber; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.test.publisher.TestPublisher; +import reactor.test.util.RaceTestUtils; + +public class RSocketResponderTest { + + ServerSocketRule rule; + + @BeforeEach + public void setUp() throws Throwable { + Hooks.onNextDropped(ReferenceCountUtil::safeRelease); + Hooks.onErrorDropped(t -> {}); + rule = new ServerSocketRule(); + rule.apply( + new Statement() { + @Override + public void evaluate() {} + }, + null) + .evaluate(); + } + + @AfterEach + public void tearDown() { + Hooks.resetOnErrorDropped(); + Hooks.resetOnNextDropped(); + } + + @Test + @Timeout(2_000) + @Disabled + public void testHandleKeepAlive() throws Exception { + rule.connection.addToReceivedBuffer( + KeepAliveFrameCodec.encode(rule.alloc(), true, 0, Unpooled.EMPTY_BUFFER)); + ByteBuf sent = rule.connection.awaitSend(); + assertThat("Unexpected frame sent.", frameType(sent), is(FrameType.KEEPALIVE)); + /*Keep alive ack must not have respond flag else, it will result in infinite ping-pong of keep alive frames.*/ + assertThat( + "Unexpected keep-alive frame respond flag.", + KeepAliveFrameCodec.respondFlag(sent), + is(false)); + } + + @Test + @Timeout(2_000) + @Disabled + public void testHandleResponseFrameNoError() throws Exception { + final int streamId = 4; + rule.connection.clearSendReceiveBuffers(); + + rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE); + + Collection> sendSubscribers = rule.connection.getSendSubscribers(); + assertThat("Request not sent.", sendSubscribers, hasSize(1)); + Subscriber sendSub = sendSubscribers.iterator().next(); + assertThat( + "Unexpected frame sent.", + frameType(rule.connection.awaitSend()), + anyOf(is(FrameType.COMPLETE), is(FrameType.NEXT_COMPLETE))); + } + + @Test + @Timeout(2_000) + @Disabled + public void testHandlerEmitsError() throws Exception { + final int streamId = 4; + rule.sendRequest(streamId, FrameType.REQUEST_STREAM); + assertThat( + "Unexpected frame sent.", frameType(rule.connection.awaitSend()), is(FrameType.ERROR)); + } + + @Test + @Timeout(20_000) + public void testCancel() { + ByteBufAllocator allocator = rule.alloc(); + final int streamId = 4; + final AtomicBoolean cancelled = new AtomicBoolean(); + rule.setAcceptingSocket( + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + payload.release(); + return Mono.never().doOnCancel(() -> cancelled.set(true)); + } + }); + rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE); + + assertThat("Unexpected frame sent.", rule.connection.getSent(), is(empty())); + + rule.connection.addToReceivedBuffer(CancelFrameCodec.encode(allocator, streamId)); + + assertThat("Unexpected frame sent.", rule.connection.getSent(), is(empty())); + assertThat("Subscription not cancelled.", cancelled.get(), is(true)); + rule.assertHasNoLeaks(); + } + + @ParameterizedTest + @ValueSource(ints = {128, 256, FRAME_LENGTH_MASK}) + @Timeout(2_000) + public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmentation( + int maxFrameLength) { + rule.setMaxFrameLength(maxFrameLength); + final int streamId = 4; + final AtomicBoolean cancelled = new AtomicBoolean(); + byte[] metadata = new byte[maxFrameLength]; + byte[] data = new byte[maxFrameLength]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + final Payload payload = DefaultPayload.create(data, metadata); + final RSocket acceptingSocket = + new RSocket() { + @Override + public Mono requestResponse(Payload p) { + p.release(); + return Mono.just(payload).doOnCancel(() -> cancelled.set(true)); + } + + @Override + public Flux requestStream(Payload p) { + p.release(); + return Flux.just(payload).doOnCancel(() -> cancelled.set(true)); + } + + @Override + public Flux requestChannel(Publisher payloads) { + Flux.from(payloads) + .doOnNext(Payload::release) + .subscribe( + new BaseSubscriber() { + @Override + protected void hookOnSubscribe(Subscription subscription) { + subscription.request(1); + } + }); + return Flux.just(payload).doOnCancel(() -> cancelled.set(true)); + } + }; + rule.setAcceptingSocket(acceptingSocket); + + final Runnable[] runnables = { + () -> rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE), + () -> rule.sendRequest(streamId, FrameType.REQUEST_STREAM), + () -> rule.sendRequest(streamId, FrameType.REQUEST_CHANNEL) + }; + + for (Runnable runnable : runnables) { + rule.connection.clearSendReceiveBuffers(); + runnable.run(); + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> FrameHeaderCodec.frameType(bb) == FrameType.ERROR) + .matches(bb -> ErrorFrameCodec.dataUtf8(bb).contains(INVALID_PAYLOAD_ERROR_MESSAGE)) + .matches(ReferenceCounted::release); + + assertThat("Subscription not cancelled.", cancelled.get(), is(true)); + } + + rule.assertHasNoLeaks(); + } + + @Test + public void checkNoLeaksOnRacingCancelFromRequestChannelAndNextFromUpstream() { + ByteBufAllocator allocator = rule.alloc(); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + + rule.setAcceptingSocket( + new RSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + payloads.subscribe(assertSubscriber); + return Flux.never(); + } + }, + Integer.MAX_VALUE); + + rule.sendRequest(1, REQUEST_CHANNEL); + + ByteBuf metadata1 = allocator.buffer(); + metadata1.writeCharSequence("abc1", CharsetUtil.UTF_8); + ByteBuf data1 = allocator.buffer(); + data1.writeCharSequence("def1", CharsetUtil.UTF_8); + ByteBuf nextFrame1 = + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata1, data1); + + ByteBuf metadata2 = allocator.buffer(); + metadata2.writeCharSequence("abc2", CharsetUtil.UTF_8); + ByteBuf data2 = allocator.buffer(); + data2.writeCharSequence("def2", CharsetUtil.UTF_8); + ByteBuf nextFrame2 = + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata2, data2); + + ByteBuf metadata3 = allocator.buffer(); + metadata3.writeCharSequence("abc3", CharsetUtil.UTF_8); + ByteBuf data3 = allocator.buffer(); + data3.writeCharSequence("def3", CharsetUtil.UTF_8); + ByteBuf nextFrame3 = + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata3, data3); + + RaceTestUtils.race( + () -> { + rule.connection.addToReceivedBuffer(nextFrame1, nextFrame2, nextFrame3); + }, + assertSubscriber::cancel); + + Assertions.assertThat(assertSubscriber.values()).allMatch(ReferenceCounted::release); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + } + + @Test + public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChannelTest() { + Hooks.onErrorDropped((e) -> {}); + ByteBufAllocator allocator = rule.alloc(); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + + FluxSink[] sinks = new FluxSink[1]; + + rule.setAcceptingSocket( + new RSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + ((Flux) payloads) + .doOnNext(ReferenceCountUtil::safeRelease) + .subscribe(assertSubscriber); + return Flux.create(sink -> sinks[0] = sink, FluxSink.OverflowStrategy.IGNORE); + } + }, + 1); + + rule.sendRequest(1, REQUEST_CHANNEL); + + ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, 1); + FluxSink sink = sinks[0]; + RaceTestUtils.race( + () -> rule.connection.addToReceivedBuffer(cancelFrame), + () -> { + sink.next(ByteBufPayload.create("d1", "m1")); + sink.next(ByteBufPayload.create("d2", "m2")); + sink.next(ByteBufPayload.create("d3", "m3")); + }); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + } + + @Test + public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChannelTest1() { + Hooks.onErrorDropped((e) -> {}); + ByteBufAllocator allocator = rule.alloc(); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + + FluxSink[] sinks = new FluxSink[1]; + + rule.setAcceptingSocket( + new RSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + ((Flux) payloads) + .doOnNext(ReferenceCountUtil::safeRelease) + .subscribe(assertSubscriber); + return Flux.create(sink -> sinks[0] = sink, FluxSink.OverflowStrategy.IGNORE); + } + }, + 1); + + rule.sendRequest(1, REQUEST_CHANNEL); + + ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, 1); + ByteBuf requestNFrame = RequestNFrameCodec.encode(allocator, 1, Integer.MAX_VALUE); + FluxSink sink = sinks[0]; + RaceTestUtils.race( + () -> 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")); + }); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + } + + @Test + public void + checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromUpstreamOnErrorFromRequestChannelTest1() { + Hooks.onErrorDropped((e) -> {}); + ByteBufAllocator allocator = rule.alloc(); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + FluxSink[] sinks = new FluxSink[1]; + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + rule.setAcceptingSocket( + new RSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + payloads.subscribe(assertSubscriber); + + return Flux.create( + sink -> { + sinks[0] = sink; + }, + FluxSink.OverflowStrategy.IGNORE); + } + }, + 1); + + rule.sendRequest(1, REQUEST_CHANNEL); + + ByteBuf metadata1 = allocator.buffer(); + metadata1.writeCharSequence("abc1", CharsetUtil.UTF_8); + ByteBuf data1 = allocator.buffer(); + data1.writeCharSequence("def1", CharsetUtil.UTF_8); + ByteBuf nextFrame1 = + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata1, data1); + + ByteBuf metadata2 = allocator.buffer(); + metadata2.writeCharSequence("abc2", CharsetUtil.UTF_8); + ByteBuf data2 = allocator.buffer(); + data2.writeCharSequence("def2", CharsetUtil.UTF_8); + ByteBuf nextFrame2 = + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata2, data2); + + ByteBuf metadata3 = allocator.buffer(); + metadata3.writeCharSequence("abc3", CharsetUtil.UTF_8); + ByteBuf data3 = allocator.buffer(); + data3.writeCharSequence("def3", CharsetUtil.UTF_8); + ByteBuf nextFrame3 = + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata3, data3); + + ByteBuf requestNFrame = RequestNFrameCodec.encode(allocator, 1, Integer.MAX_VALUE); + + ByteBuf m1 = allocator.buffer(); + m1.writeCharSequence("m1", CharsetUtil.UTF_8); + ByteBuf d1 = allocator.buffer(); + d1.writeCharSequence("d1", CharsetUtil.UTF_8); + Payload np1 = ByteBufPayload.create(d1, m1); + + ByteBuf m2 = allocator.buffer(); + m2.writeCharSequence("m2", CharsetUtil.UTF_8); + ByteBuf d2 = allocator.buffer(); + d2.writeCharSequence("d2", CharsetUtil.UTF_8); + Payload np2 = ByteBufPayload.create(d2, m2); + + ByteBuf m3 = allocator.buffer(); + m3.writeCharSequence("m3", CharsetUtil.UTF_8); + ByteBuf d3 = allocator.buffer(); + d3.writeCharSequence("d3", CharsetUtil.UTF_8); + Payload np3 = ByteBufPayload.create(d3, m3); + + FluxSink sink = sinks[0]; + RaceTestUtils.race( + () -> rule.connection.addToReceivedBuffer(requestNFrame), + () -> rule.connection.addToReceivedBuffer(nextFrame1, nextFrame2, nextFrame3), + () -> { + sink.next(np1); + sink.next(np2); + sink.next(np3); + sink.error(new RuntimeException()); + }); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + assertSubscriber + .assertTerminated() + .assertError(CancellationException.class) + .assertErrorMessage("Disposed"); + Assertions.assertThat(assertSubscriber.values()) + .allMatch( + msg -> { + ReferenceCountUtil.safeRelease(msg); + return msg.refCnt() == 0; + }); + rule.assertHasNoLeaks(); + } + } + + @Test + public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestStreamTest1() { + Hooks.onErrorDropped((e) -> {}); + ByteBufAllocator allocator = rule.alloc(); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + FluxSink[] sinks = new FluxSink[1]; + + rule.setAcceptingSocket( + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + payload.release(); + return Flux.create(sink -> sinks[0] = sink, FluxSink.OverflowStrategy.IGNORE); + } + }, + Integer.MAX_VALUE); + + rule.sendRequest(1, REQUEST_STREAM); + + ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, 1); + FluxSink sink = sinks[0]; + RaceTestUtils.race( + () -> rule.connection.addToReceivedBuffer(cancelFrame), + () -> { + sink.next(ByteBufPayload.create("d1", "m1")); + sink.next(ByteBufPayload.create("d2", "m2")); + sink.next(ByteBufPayload.create("d3", "m3")); + }); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + } + + @Test + public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestResponseTest1() { + Hooks.onErrorDropped((e) -> {}); + ByteBufAllocator allocator = rule.alloc(); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + Operators.MonoSubscriber[] sources = new Operators.MonoSubscriber[1]; + + rule.setAcceptingSocket( + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + payload.release(); + return new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + sources[0] = new Operators.MonoSubscriber<>(actual); + actual.onSubscribe(sources[0]); + } + }; + } + }, + Integer.MAX_VALUE); + + rule.sendRequest(1, REQUEST_RESPONSE); + + ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, 1); + RaceTestUtils.race( + () -> rule.connection.addToReceivedBuffer(cancelFrame), + () -> { + sources[0].complete(ByteBufPayload.create("d1", "m1")); + }); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + } + + @Test + public void simpleDiscardRequestStreamTest() { + ByteBufAllocator allocator = rule.alloc(); + FluxSink[] sinks = new FluxSink[1]; + + rule.setAcceptingSocket( + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + payload.release(); + return Flux.create(sink -> sinks[0] = sink, FluxSink.OverflowStrategy.IGNORE); + } + }, + 1); + + rule.sendRequest(1, REQUEST_STREAM); + + ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, 1); + FluxSink sink = sinks[0]; + + sink.next(ByteBufPayload.create("d1", "m1")); + sink.next(ByteBufPayload.create("d2", "m2")); + sink.next(ByteBufPayload.create("d3", "m3")); + rule.connection.addToReceivedBuffer(cancelFrame); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + + @Test + public void simpleDiscardRequestChannelTest() { + ByteBufAllocator allocator = rule.alloc(); + + rule.setAcceptingSocket( + new RSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + return (Flux) payloads; + } + }, + 1); + + rule.sendRequest(1, REQUEST_STREAM); + + ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, 1); + + ByteBuf metadata1 = allocator.buffer(); + metadata1.writeCharSequence("abc1", CharsetUtil.UTF_8); + ByteBuf data1 = allocator.buffer(); + data1.writeCharSequence("def1", CharsetUtil.UTF_8); + ByteBuf nextFrame1 = + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata1, data1); + + ByteBuf metadata2 = allocator.buffer(); + metadata2.writeCharSequence("abc2", CharsetUtil.UTF_8); + ByteBuf data2 = allocator.buffer(); + data2.writeCharSequence("def2", CharsetUtil.UTF_8); + ByteBuf nextFrame2 = + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata2, data2); + + ByteBuf metadata3 = allocator.buffer(); + metadata3.writeCharSequence("abc3", CharsetUtil.UTF_8); + ByteBuf data3 = allocator.buffer(); + data3.writeCharSequence("de3", CharsetUtil.UTF_8); + ByteBuf nextFrame3 = + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata3, data3); + rule.connection.addToReceivedBuffer(nextFrame1, nextFrame2, nextFrame3); + + rule.connection.addToReceivedBuffer(cancelFrame); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("encodeDecodePayloadCases") + public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( + FrameType frameType, int framesCnt, int responsesCnt) { + ByteBufAllocator allocator = rule.alloc(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(framesCnt); + TestPublisher testPublisher = TestPublisher.create(); + + rule.setAcceptingSocket( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + Mono.just(payload).subscribe(assertSubscriber); + return Mono.empty(); + } + + @Override + public Mono requestResponse(Payload payload) { + Mono.just(payload).subscribe(assertSubscriber); + return testPublisher.mono(); + } + + @Override + public Flux requestStream(Payload payload) { + Mono.just(payload).subscribe(assertSubscriber); + return testPublisher.flux(); + } + + @Override + public Flux requestChannel(Publisher payloads) { + payloads.subscribe(assertSubscriber); + return testPublisher.flux(); + } + }, + 1); + + rule.sendRequest(1, frameType, ByteBufPayload.create("d")); + + // if responses number is bigger than 1 we have to send one extra requestN + if (responsesCnt > 1) { + rule.connection.addToReceivedBuffer( + RequestNFrameCodec.encode(allocator, 1, responsesCnt - 1)); + } + + // respond with specific number of elements + for (int i = 0; i < responsesCnt; i++) { + testPublisher.next(ByteBufPayload.create("rd" + i)); + } + + // Listen to incoming frames. Valid for RequestChannel case only + if (framesCnt > 1) { + for (int i = 1; i < responsesCnt; i++) { + rule.connection.addToReceivedBuffer( + PayloadFrameCodec.encode( + allocator, + 1, + false, + false, + true, + null, + Unpooled.wrappedBuffer(("d" + (i + 1)).getBytes()))); + } + } + + if (responsesCnt > 0) { + Assertions.assertThat( + rule.connection.getSent().stream().filter(bb -> frameType(bb) != REQUEST_N)) + .describedAs( + "Interaction Type :[%s]. Expected to observe %s frames sent", frameType, responsesCnt) + .hasSize(responsesCnt) + .allMatch(bb -> !FrameHeaderCodec.hasMetadata(bb)); + } + + if (framesCnt > 1) { + Assertions.assertThat( + rule.connection.getSent().stream().filter(bb -> frameType(bb) == REQUEST_N)) + .describedAs( + "Interaction Type :[%s]. Expected to observe single RequestN(%s) frame", + frameType, framesCnt - 1) + .hasSize(1) + .first() + .matches(bb -> RequestNFrameCodec.requestN(bb) == (framesCnt - 1)); + } + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + Assertions.assertThat(assertSubscriber.awaitAndAssertNextValueCount(framesCnt).values()) + .hasSize(framesCnt) + .allMatch(p -> !p.hasMetadata()) + .allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + + static Stream encodeDecodePayloadCases() { + return Stream.of( + Arguments.of(REQUEST_FNF, 1, 0), + Arguments.of(REQUEST_RESPONSE, 1, 1), + Arguments.of(REQUEST_STREAM, 1, 5), + Arguments.of(REQUEST_CHANNEL, 5, 5)); + } + + @ParameterizedTest + @MethodSource("refCntCases") + public void ensureSendsErrorOnIllegalRefCntPayload(FrameType frameType) { + rule.setAcceptingSocket( + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + Payload invalidPayload = ByteBufPayload.create("test", "test"); + invalidPayload.release(); + return Mono.just(invalidPayload); + } + + @Override + public Flux requestStream(Payload payload) { + Payload invalidPayload = ByteBufPayload.create("test", "test"); + invalidPayload.release(); + return Flux.just(invalidPayload); + } + + @Override + public Flux requestChannel(Publisher payloads) { + Payload invalidPayload = ByteBufPayload.create("test", "test"); + invalidPayload.release(); + return Flux.just(invalidPayload); + } + }); + + rule.sendRequest(1, frameType); + + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches( + bb -> frameType(bb) == ERROR, + "Expect frame type to be {" + + ERROR + + "} but was {" + + frameType(rule.connection.getSent().iterator().next()) + + "}"); + } + + private static Stream refCntCases() { + return Stream.of(REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL); + } + + @Test + // see https://github.com/rsocket/rsocket-java/issues/858 + public void testWorkaround858() { + ByteBuf buffer = rule.alloc().buffer(); + buffer.writeCharSequence("test", CharsetUtil.UTF_8); + + TestPublisher testPublisher = TestPublisher.create(); + + rule.setAcceptingSocket( + new RSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + Flux.from(payloads).doOnNext(ReferenceCounted::release).subscribe(); + + return testPublisher.flux(); + } + }); + + rule.connection.addToReceivedBuffer( + RequestChannelFrameCodec.encodeReleasingPayload( + rule.alloc(), 1, false, 1, ByteBufPayload.create(buffer))); + rule.connection.addToReceivedBuffer( + ErrorFrameCodec.encode(rule.alloc(), 1, new RuntimeException("test"))); + + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> FrameHeaderCodec.frameType(bb) == REQUEST_N) + .matches(ReferenceCounted::release); + + Assertions.assertThat(rule.socket.isDisposed()).isFalse(); + testPublisher.assertWasCancelled(); + + rule.assertHasNoLeaks(); + } + + public static class ServerSocketRule extends AbstractSocketRule { + + private RSocket acceptingSocket; + private volatile int prefetch; + + @Override + protected void init() { + acceptingSocket = + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + return Mono.just(payload); + } + }; + super.init(); + } + + public void setAcceptingSocket(RSocket acceptingSocket) { + this.acceptingSocket = acceptingSocket; + connection = new TestDuplexConnection(alloc()); + connectSub = TestSubscriber.create(); + this.prefetch = Integer.MAX_VALUE; + super.init(); + } + + public void setAcceptingSocket(RSocket acceptingSocket, int prefetch) { + this.acceptingSocket = acceptingSocket; + connection = new TestDuplexConnection(alloc()); + connectSub = TestSubscriber.create(); + this.prefetch = prefetch; + super.init(); + } + + @Override + protected RSocketResponder newRSocket() { + return new RSocketResponder( + connection, + acceptingSocket, + PayloadDecoder.ZERO_COPY, + ResponderLeaseHandler.None, + 0, + maxFrameLength); + } + + private void sendRequest(int streamId, FrameType frameType) { + sendRequest(streamId, frameType, EmptyPayload.INSTANCE); + } + + private void sendRequest(int streamId, FrameType frameType, Payload payload) { + ByteBuf request; + + switch (frameType) { + case REQUEST_CHANNEL: + request = + RequestChannelFrameCodec.encodeReleasingPayload( + allocator, streamId, false, prefetch, payload); + break; + case REQUEST_STREAM: + request = + RequestStreamFrameCodec.encodeReleasingPayload( + allocator, streamId, prefetch, payload); + break; + case REQUEST_RESPONSE: + request = RequestResponseFrameCodec.encodeReleasingPayload(allocator, streamId, payload); + break; + case REQUEST_FNF: + request = + RequestFireAndForgetFrameCodec.encodeReleasingPayload(allocator, streamId, payload); + break; + default: + throw new IllegalArgumentException("unsupported type: " + frameType); + } + + connection.addToReceivedBuffer(request); + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java new file mode 100644 index 000000000..073ebfd06 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java @@ -0,0 +1,43 @@ +package io.rsocket.core; + +import io.rsocket.test.util.TestClientTransport; +import io.rsocket.test.util.TestServerTransport; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public class RSocketServerFragmentationTest { + + @Test + public void serverErrorsWithEnabledFragmentationOnInsufficientMtu() { + Assertions.assertThatIllegalArgumentException() + .isThrownBy(() -> RSocketServer.create().fragment(2)) + .withMessage("The smallest allowed mtu size is 64 bytes, provided: 2"); + } + + @Test + public void serverSucceedsWithEnabledFragmentationOnSufficientMtu() { + RSocketServer.create().fragment(100).bind(new TestServerTransport()).block(); + } + + @Test + public void serverSucceedsWithDisabledFragmentation() { + RSocketServer.create().bind(new TestServerTransport()).block(); + } + + @Test + public void clientErrorsWithEnabledFragmentationOnInsufficientMtu() { + Assertions.assertThatIllegalArgumentException() + .isThrownBy(() -> RSocketConnector.create().fragment(2)) + .withMessage("The smallest allowed mtu size is 64 bytes, provided: 2"); + } + + @Test + public void clientSucceedsWithEnabledFragmentationOnSufficientMtu() { + RSocketConnector.create().fragment(100).connect(new TestClientTransport()).block(); + } + + @Test + public void clientSucceedsWithDisabledFragmentation() { + RSocketConnector.connectWith(new TestClientTransport()).block(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java new file mode 100644 index 000000000..a6103a2ba --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java @@ -0,0 +1,85 @@ +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.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.RSocket; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.test.util.TestDuplexConnection; +import io.rsocket.test.util.TestServerTransport; +import java.time.Duration; +import java.util.Random; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.test.StepVerifier; + +public class RSocketServerTest { + + @Test + public void ensuresMaxFrameLengthCanNotBeLessThenMtu() { + RSocketServer.create() + .fragment(128) + .bind(new TestServerTransport().withMaxFrameLength(64)) + .as(StepVerifier::create) + .expectErrorMessage( + "Configured maximumTransmissionUnit[128] exceeds configured maxFrameLength[64]") + .verify(); + } + + @Test + public void ensuresMaxFrameLengthCanNotBeGreaterThenMaxPayloadSize() { + RSocketServer.create() + .maxInboundPayloadSize(128) + .bind(new TestServerTransport().withMaxFrameLength(256)) + .as(StepVerifier::create) + .expectErrorMessage("Configured maxFrameLength[256] exceeds maxPayloadSize[128]") + .verify(); + } + + @Test + public void ensuresMaxFrameLengthCanNotBeGreaterThenMaxPossibleFrameLength() { + RSocketServer.create() + .bind(new TestServerTransport().withMaxFrameLength(Integer.MAX_VALUE)) + .as(StepVerifier::create) + .expectErrorMessage( + "Configured maxFrameLength[" + + Integer.MAX_VALUE + + "] " + + "exceeds maxFrameLength limit " + + FRAME_LENGTH_MASK) + .verify(); + } + + @Test + public void unexpectedFramesBeforeSetup() { + MonoProcessor connectedMono = MonoProcessor.create(); + + TestServerTransport transport = new TestServerTransport(); + RSocketServer.create() + .acceptor( + (setup, sendingSocket) -> { + connectedMono.onComplete(); + return Mono.just(new RSocket() {}); + }) + .bind(transport) + .block(); + + byte[] bytes = new byte[16_000_000]; + new Random().nextBytes(bytes); + + TestDuplexConnection connection = transport.connect(); + connection.addToReceivedBuffer( + RequestResponseFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + Unpooled.EMPTY_BUFFER, + 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(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java new file mode 100644 index 000000000..7320d9ade --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -0,0 +1,593 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.TestScheduler; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.exceptions.ApplicationErrorException; +import io.rsocket.exceptions.CustomRSocketException; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.subscriber.AssertSubscriber; +import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.test.util.LocalDuplexConnection; +import io.rsocket.util.DefaultPayload; +import io.rsocket.util.EmptyPayload; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicReference; +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.reactivestreams.Publisher; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.publisher.DirectProcessor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; + +public class RSocketTest { + + @Rule public final SocketRule rule = new SocketRule(); + + @Test + public void rsocketDisposalShouldEndupWithNoErrorsOnClose() { + RSocket requestHandlingRSocket = + new RSocket() { + final Disposable disposable = Disposables.single(); + + @Override + public void dispose() { + disposable.dispose(); + } + + @Override + public boolean isDisposed() { + return disposable.isDisposed(); + } + }; + rule.setRequestAcceptor(requestHandlingRSocket); + rule.crs + .onClose() + .as(StepVerifier::create) + .expectSubscription() + .then(rule.crs::dispose) + .expectComplete() + .verify(Duration.ofMillis(100)); + + Assertions.assertThat(requestHandlingRSocket.isDisposed()).isTrue(); + } + + @Test(timeout = 2_000) + public void testRequestReplyNoError() { + StepVerifier.create(rule.crs.requestResponse(DefaultPayload.create("hello"))) + .expectNextCount(1) + .expectComplete() + .verify(); + } + + @Test(timeout = 2000) + public void testHandlerEmitsError() { + rule.setRequestAcceptor( + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + return Mono.error(new NullPointerException("Deliberate exception.")); + } + }); + rule.crs + .requestResponse(EmptyPayload.INSTANCE) + .as(StepVerifier::create) + .expectErrorSatisfies( + t -> + Assertions.assertThat(t) + .isInstanceOf(ApplicationErrorException.class) + .hasMessage("Deliberate exception.")) + .verify(Duration.ofMillis(100)); + } + + @Test(timeout = 2000) + public void testHandlerEmitsCustomError() { + rule.setRequestAcceptor( + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + return Mono.error( + new CustomRSocketException(0x00000501, "Deliberate Custom exception.")); + } + }); + rule.crs + .requestResponse(EmptyPayload.INSTANCE) + .as(StepVerifier::create) + .expectErrorSatisfies( + t -> + Assertions.assertThat(t) + .isInstanceOf(CustomRSocketException.class) + .hasMessage("Deliberate Custom exception.") + .hasFieldOrPropertyWithValue("errorCode", 0x00000501)) + .verify(); + } + + @Test(timeout = 2000) + public void testRequestPropagatesCorrectlyForRequestChannel() { + rule.setRequestAcceptor( + new RSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + // specifically limits request to 3 in order to prevent 256 request from limitRate + // hidden on the responder side + .limitRequest(3); + } + }); + + Flux.range(0, 3) + .map(i -> DefaultPayload.create("" + i)) + .as(rule.crs::requestChannel) + .as(publisher -> StepVerifier.create(publisher, 3)) + .expectSubscription() + .expectNextCount(3) + .expectComplete() + .verify(Duration.ofMillis(5000)); + } + + @Test(timeout = 2000) + public void testStream() { + Flux responses = rule.crs.requestStream(DefaultPayload.create("Payload In")); + StepVerifier.create(responses).expectNextCount(10).expectComplete().verify(); + } + + @Test(timeout = 2000) + public void testChannel() { + 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) + public void testErrorPropagatesCorrectly() { + AtomicReference error = new AtomicReference<>(); + rule.setRequestAcceptor( + new RSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads).doOnError(error::set); + } + }); + Flux requests = Flux.error(new RuntimeException("test")); + Flux responses = rule.crs.requestChannel(requests); + StepVerifier.create(responses).expectErrorMessage("test").verify(); + Assertions.assertThat(error.get()).isNull(); + } + + @Test + public void requestChannelCase_StreamIsTerminatedAfterBothSidesSentCompletion1() { + TestPublisher requesterPublisher = TestPublisher.create(); + AssertSubscriber requesterSubscriber = new AssertSubscriber<>(0); + + AssertSubscriber responderSubscriber = new AssertSubscriber<>(0); + TestPublisher responderPublisher = TestPublisher.create(); + + initRequestChannelCase( + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); + + nextFromRequesterPublisher(requesterPublisher, responderSubscriber); + + completeFromRequesterPublisher(requesterPublisher, responderSubscriber); + + nextFromResponderPublisher(responderPublisher, requesterSubscriber); + + completeFromResponderPublisher(responderPublisher, requesterSubscriber); + } + + @Test + public void requestChannelCase_StreamIsTerminatedAfterBothSidesSentCompletion2() { + TestPublisher requesterPublisher = TestPublisher.create(); + AssertSubscriber requesterSubscriber = new AssertSubscriber<>(0); + + AssertSubscriber responderSubscriber = new AssertSubscriber<>(0); + TestPublisher responderPublisher = TestPublisher.create(); + + initRequestChannelCase( + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); + + nextFromResponderPublisher(responderPublisher, requesterSubscriber); + + completeFromResponderPublisher(responderPublisher, requesterSubscriber); + + nextFromRequesterPublisher(requesterPublisher, responderSubscriber); + + completeFromRequesterPublisher(requesterPublisher, responderSubscriber); + } + + @Test + public void + requestChannelCase_CancellationFromResponderShouldLeaveStreamInHalfClosedStateWithNextCompletionPossibleFromRequester() { + TestPublisher requesterPublisher = TestPublisher.create(); + AssertSubscriber requesterSubscriber = new AssertSubscriber<>(0); + + AssertSubscriber responderSubscriber = new AssertSubscriber<>(0); + TestPublisher responderPublisher = TestPublisher.create(); + + initRequestChannelCase( + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); + + nextFromRequesterPublisher(requesterPublisher, responderSubscriber); + + cancelFromResponderSubscriber(requesterPublisher, responderSubscriber); + + nextFromResponderPublisher(responderPublisher, requesterSubscriber); + + completeFromResponderPublisher(responderPublisher, requesterSubscriber); + } + + @Test + public void + requestChannelCase_CompletionFromRequesterShouldLeaveStreamInHalfClosedStateWithNextCancellationPossibleFromResponder() { + TestPublisher requesterPublisher = TestPublisher.create(); + AssertSubscriber requesterSubscriber = new AssertSubscriber<>(0); + + AssertSubscriber responderSubscriber = new AssertSubscriber<>(0); + TestPublisher responderPublisher = TestPublisher.create(); + + initRequestChannelCase( + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); + + nextFromResponderPublisher(responderPublisher, requesterSubscriber); + + completeFromResponderPublisher(responderPublisher, requesterSubscriber); + + nextFromRequesterPublisher(requesterPublisher, responderSubscriber); + + cancelFromResponderSubscriber(requesterPublisher, responderSubscriber); + } + + @Test + public void + requestChannelCase_ensureThatRequesterSubscriberCancellationTerminatesStreamsOnBothSides() { + TestPublisher requesterPublisher = TestPublisher.create(); + AssertSubscriber requesterSubscriber = new AssertSubscriber<>(0); + + AssertSubscriber responderSubscriber = new AssertSubscriber<>(0); + TestPublisher responderPublisher = TestPublisher.create(); + + initRequestChannelCase( + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); + + nextFromResponderPublisher(responderPublisher, requesterSubscriber); + + nextFromRequesterPublisher(requesterPublisher, responderSubscriber); + + // ensures both sides are terminated + cancelFromRequesterSubscriber( + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); + } + + @Test + public void requestChannelCase_ErrorFromResponderShouldTerminatesStreamsOnBothSides() { + TestPublisher requesterPublisher = TestPublisher.create(); + AssertSubscriber requesterSubscriber = new AssertSubscriber<>(0); + + AssertSubscriber responderSubscriber = new AssertSubscriber<>(0); + TestPublisher responderPublisher = TestPublisher.create(); + + initRequestChannelCase( + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); + + nextFromResponderPublisher(responderPublisher, requesterSubscriber); + + nextFromRequesterPublisher(requesterPublisher, responderSubscriber); + + // ensures both sides are terminated + errorFromResponderPublisher( + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); + } + + @Test + public void requestChannelCase_ErrorFromRequesterShouldTerminatesStreamsOnBothSides() { + TestPublisher requesterPublisher = TestPublisher.create(); + AssertSubscriber requesterSubscriber = new AssertSubscriber<>(0); + + AssertSubscriber responderSubscriber = new AssertSubscriber<>(0); + TestPublisher responderPublisher = TestPublisher.create(); + + initRequestChannelCase( + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); + + nextFromResponderPublisher(responderPublisher, requesterSubscriber); + + nextFromRequesterPublisher(requesterPublisher, responderSubscriber); + + // ensures both sides are terminated + errorFromRequesterPublisher( + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); + } + + void initRequestChannelCase( + TestPublisher requesterPublisher, + AssertSubscriber requesterSubscriber, + TestPublisher responderPublisher, + AssertSubscriber responderSubscriber) { + rule.setRequestAcceptor( + new RSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + payloads.subscribe(responderSubscriber); + return responderPublisher.flux(); + } + }); + + rule.crs.requestChannel(requesterPublisher).subscribe(requesterSubscriber); + + requesterPublisher.assertWasSubscribed(); + requesterSubscriber.assertSubscribed(); + + responderSubscriber.assertNotSubscribed(); + responderPublisher.assertWasNotSubscribed(); + + // firstRequest + requesterSubscriber.request(1); + requesterPublisher.assertMaxRequested(1); + requesterPublisher.next(DefaultPayload.create("initialData", "initialMetadata")); + + responderSubscriber.assertSubscribed(); + responderPublisher.assertWasSubscribed(); + } + + void nextFromRequesterPublisher( + TestPublisher requesterPublisher, AssertSubscriber responderSubscriber) { + // ensures that outerUpstream and innerSubscriber is not terminated so the requestChannel + requesterPublisher.assertSubscribers(1); + responderSubscriber.assertNotTerminated(); + + responderSubscriber.request(6); + requesterPublisher.next( + DefaultPayload.create("d1", "m1"), + DefaultPayload.create("d2"), + DefaultPayload.create("d3", "m3"), + DefaultPayload.create("d4"), + DefaultPayload.create("d5", "m5")); + + List innerPayloads = responderSubscriber.awaitAndAssertNextValueCount(6).values(); + Assertions.assertThat(innerPayloads.stream().map(Payload::getDataUtf8)) + .containsExactly("initialData", "d1", "d2", "d3", "d4", "d5"); + Assertions.assertThat(innerPayloads.stream().map(Payload::hasMetadata)) + .containsExactly(true, true, false, true, false, true); + Assertions.assertThat(innerPayloads.stream().map(Payload::getMetadataUtf8)) + .containsExactly("initialMetadata", "m1", "", "m3", "", "m5"); + } + + void completeFromRequesterPublisher( + TestPublisher requesterPublisher, AssertSubscriber responderSubscriber) { + // ensures that after sending complete upstream part is closed + requesterPublisher.complete(); + responderSubscriber.assertTerminated(); + requesterPublisher.assertNoSubscribers(); + } + + void cancelFromResponderSubscriber( + TestPublisher requesterPublisher, AssertSubscriber responderSubscriber) { + // ensures that after sending complete upstream part is closed + responderSubscriber.cancel(); + requesterPublisher.assertWasCancelled(); + requesterPublisher.assertNoSubscribers(); + } + + void nextFromResponderPublisher( + TestPublisher responderPublisher, AssertSubscriber requesterSubscriber) { + // ensures that downstream is not terminated so the requestChannel state is half-closed + responderPublisher.assertSubscribers(1); + requesterSubscriber.assertNotTerminated(); + + // ensures responderPublisher can send messages and outerSubscriber can receive them + requesterSubscriber.request(5); + responderPublisher.next( + DefaultPayload.create("rd1", "rm1"), + DefaultPayload.create("rd2"), + DefaultPayload.create("rd3", "rm3"), + DefaultPayload.create("rd4"), + DefaultPayload.create("rd5", "rm5")); + + List outerPayloads = requesterSubscriber.awaitAndAssertNextValueCount(5).values(); + Assertions.assertThat(outerPayloads.stream().map(Payload::getDataUtf8)) + .containsExactly("rd1", "rd2", "rd3", "rd4", "rd5"); + Assertions.assertThat(outerPayloads.stream().map(Payload::hasMetadata)) + .containsExactly(true, false, true, false, true); + Assertions.assertThat(outerPayloads.stream().map(Payload::getMetadataUtf8)) + .containsExactly("rm1", "", "rm3", "", "rm5"); + } + + void completeFromResponderPublisher( + TestPublisher responderPublisher, AssertSubscriber requesterSubscriber) { + // ensures that after sending complete inner upstream is closed + responderPublisher.complete(); + requesterSubscriber.assertTerminated(); + responderPublisher.assertNoSubscribers(); + } + + void cancelFromRequesterSubscriber( + TestPublisher requesterPublisher, + AssertSubscriber requesterSubscriber, + TestPublisher responderPublisher, + AssertSubscriber responderSubscriber) { + // ensures that after sending cancel the whole requestChannel is terminated + requesterSubscriber.cancel(); + // error should be propagated + responderSubscriber.assertTerminated(); + responderPublisher.assertWasCancelled(); + responderPublisher.assertNoSubscribers(); + // ensures that cancellation is propagated to the actual upstream + requesterPublisher.assertWasCancelled(); + requesterPublisher.assertNoSubscribers(); + } + + static final CustomRSocketException EXCEPTION = new CustomRSocketException(123456, "test"); + + void errorFromResponderPublisher( + TestPublisher requesterPublisher, + AssertSubscriber requesterSubscriber, + TestPublisher responderPublisher, + AssertSubscriber responderSubscriber) { + // ensures that after sending cancel the whole requestChannel is terminated + responderPublisher.error(EXCEPTION); + // error should be propagated + responderSubscriber.assertTerminated().assertError(CancellationException.class); + requesterSubscriber + .assertTerminated() + .assertError(CustomRSocketException.class) + .assertErrorMessage("test"); + // ensures that cancellation is propagated to the actual upstream + requesterPublisher.assertWasCancelled(); + requesterPublisher.assertNoSubscribers(); + } + + void errorFromRequesterPublisher( + TestPublisher requesterPublisher, + AssertSubscriber requesterSubscriber, + TestPublisher responderPublisher, + AssertSubscriber responderSubscriber) { + // ensures that after sending cancel the whole requestChannel is terminated + requesterPublisher.error(EXCEPTION); + // error should be propagated + responderSubscriber + .assertTerminated() + .assertError(CustomRSocketException.class) + .assertErrorMessage("test"); + requesterSubscriber + .assertTerminated() + .assertError(CustomRSocketException.class) + .assertErrorMessage("test"); + + // ensures that cancellation is propagated to the actual upstream + responderPublisher.assertWasCancelled(); + responderPublisher.assertNoSubscribers(); + } + + public static class SocketRule extends ExternalResource { + + DirectProcessor serverProcessor; + DirectProcessor clientProcessor; + private RSocketRequester crs; + + @SuppressWarnings("unused") + private RSocketResponder srs; + + 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(); + } + }; + } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } + + protected void init() { + allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + serverProcessor = DirectProcessor.create(); + clientProcessor = DirectProcessor.create(); + + LocalDuplexConnection serverConnection = + new LocalDuplexConnection("server", allocator, clientProcessor, serverProcessor); + LocalDuplexConnection clientConnection = + new LocalDuplexConnection("client", allocator, serverProcessor, clientProcessor); + + clientConnection.onClose().doFinally(__ -> serverConnection.dispose()).subscribe(); + serverConnection.onClose().doFinally(__ -> clientConnection.dispose()).subscribe(); + + requestAcceptor = + null != requestAcceptor + ? requestAcceptor + : new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + return Mono.just(payload); + } + + @Override + public Flux requestStream(Payload payload) { + return Flux.range(1, 10) + .map( + i -> DefaultPayload.create("server got -> [" + payload.toString() + "]")); + } + + @Override + public Flux requestChannel(Publisher payloads) { + Flux.from(payloads) + .map( + payload -> + DefaultPayload.create("server got -> [" + payload.toString() + "]")) + .subscribe(); + + return Flux.range(1, 10) + .map( + payload -> + DefaultPayload.create("server got -> [" + payload.toString() + "]")); + } + }; + + srs = + new RSocketResponder( + serverConnection, + requestAcceptor, + PayloadDecoder.DEFAULT, + ResponderLeaseHandler.None, + 0, + FRAME_LENGTH_MASK); + + crs = + new RSocketRequester( + clientConnection, + PayloadDecoder.DEFAULT, + StreamIdSupplier.clientSupplier(), + 0, + FRAME_LENGTH_MASK, + 0, + 0, + null, + RequesterLeaseHandler.None, + TestScheduler.INSTANCE); + } + + public void setRequestAcceptor(RSocket requestAcceptor) { + this.requestAcceptor = requestAcceptor; + init(); + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java new file mode 100644 index 000000000..85b1d577d --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java @@ -0,0 +1,1126 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import static org.junit.Assert.assertEquals; + +import io.rsocket.RaceTestConstants; +import io.rsocket.internal.subscriber.AssertSubscriber; +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.mockito.Mockito; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Exceptions; +import reactor.core.Scannable; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; +import reactor.test.util.RaceTestUtils; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; +import reactor.util.retry.Retry; + +public class ReconnectMonoTests { + + private Queue retries = new ConcurrentLinkedQueue<>(); + private Queue> received = new ConcurrentLinkedQueue<>(); + private Queue expired = new ConcurrentLinkedQueue<>(); + + @Test + public void shouldExpireValueOnRacingDisposeAndNext() { + Hooks.onErrorDropped(t -> {}); + Hooks.onNextDropped(System.out::println); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final int index = i; + final CoreSubscriber[] monoSubscribers = new CoreSubscriber[1]; + Subscription mockSubscription = Mockito.mock(Subscription.class); + final Mono stringMono = + new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + actual.onSubscribe(mockSubscription); + monoSubscribers[0] = actual; + } + }; + + final ReconnectMono reconnectMono = + stringMono + .doOnDiscard(Object.class, System.out::println) + .as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + RaceTestUtils.race(() -> monoSubscribers[0].onNext("value" + index), reconnectMono::dispose); + + monoSubscribers[0].onComplete(); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + Mockito.verify(mockSubscription).cancel(); + + if (processor.isError()) { + Assertions.assertThat(processor.getError()) + .isInstanceOf(CancellationException.class) + .hasMessage("ReconnectMono has already been disposed"); + + Assertions.assertThat(expired).containsOnly("value" + i); + } else { + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + } + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldNotifyAllTheSubscribersUnderRacingBetweenSubscribeAndComplete() { + Hooks.onErrorDropped(t -> {}); + for (int i = 0; i < 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(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + cold.next("value" + i); + + RaceTestUtils.race(cold::complete, () -> reconnectMono.subscribe(racerProcessor)); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + Assertions.assertThat(racerProcessor.peek()).isEqualTo("value" + i); + + Assertions.assertThat(reconnectMono.resolvingInner.subscribers) + .isEqualTo(ResolvingOperator.READY); + + Assertions.assertThat( + reconnectMono.resolvingInner.add( + new ResolvingOperator.MonoDeferredResolutionOperator<>( + reconnectMono.resolvingInner, processor))) + .isEqualTo(ResolvingOperator.READY_STATE); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value" + i, reconnectMono)); + + received.clear(); + } + } + + @Test + public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() { + Hooks.onErrorDropped(t -> {}); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final int index = i; + final TestPublisher cold = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + final MonoProcessor racerProcessor = MonoProcessor.create(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + reconnectMono.resolvingInner.mainSubscriber.onNext("value_to_expire" + i); + reconnectMono.resolvingInner.mainSubscriber.onComplete(); + + RaceTestUtils.race( + reconnectMono::invalidate, + () -> { + reconnectMono.subscribe(racerProcessor); + if (!racerProcessor.isTerminated()) { + reconnectMono.resolvingInner.mainSubscriber.onNext("value_to_not_expire" + index); + reconnectMono.resolvingInner.mainSubscriber.onComplete(); + } + }); + + 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)); + + Assertions.assertThat(expired).hasSize(1).containsOnly("value_to_expire" + i); + if (reconnectMono.resolvingInner.subscribers == ResolvingOperator.READY) { + Assertions.assertThat(received) + .hasSize(2) + .containsExactly( + Tuples.of("value_to_expire" + i, reconnectMono), + Tuples.of("value_to_not_expire" + i, reconnectMono)); + } else { + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value_to_expire" + i, reconnectMono)); + } + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates() { + Hooks.onErrorDropped(t -> {}); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final int index = i; + final TestPublisher cold = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + final MonoProcessor racerProcessor = MonoProcessor.create(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + reconnectMono.resolvingInner.mainSubscriber.onNext("value_to_expire" + i); + reconnectMono.resolvingInner.mainSubscriber.onComplete(); + + RaceTestUtils.race( + reconnectMono::invalidate, + reconnectMono::invalidate, + () -> { + reconnectMono.subscribe(racerProcessor); + if (!racerProcessor.isTerminated()) { + reconnectMono.resolvingInner.mainSubscriber.onNext( + "value_to_possibly_expire" + index); + reconnectMono.resolvingInner.mainSubscriber.onComplete(); + } + }); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + + 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)); + + if (expired.size() == 2) { + Assertions.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); + } + if (received.size() == 2) { + Assertions.assertThat(received) + .hasSize(2) + .containsExactly( + Tuples.of("value_to_expire" + i, reconnectMono), + Tuples.of("value_to_possibly_expire" + i, reconnectMono)); + } else { + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value_to_expire" + i, reconnectMono)); + } + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldNotExpireNewlyResolvedValueIfBlockIsRacingWithInvalidate() { + Hooks.onErrorDropped(t -> {}); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final int index = i; + 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 = + new ReconnectMono<>( + source.subscribeOn(Schedulers.boundedElastic()), onExpire(), onValue()); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + final AssertSubscriber subscriber = + reconnectMono.subscribeWith(new AssertSubscriber<>()); + + subscriber.await().assertComplete(); + + Assertions.assertThat(expired).isEmpty(); + + RaceTestUtils.race( + () -> + Assertions.assertThat(reconnectMono.block()) + .matches( + (v) -> + v.equals("value_to_not_expire" + index) + || v.equals("value_to_expire" + index)), + reconnectMono::invalidate); + + subscriber.assertTerminated(); + + subscriber.assertValues("value_to_expire" + i); + + Assertions.assertThat(expired).hasSize(1).containsOnly("value_to_expire" + i); + if (reconnectMono.resolvingInner.subscribers == ResolvingOperator.READY) { + Assertions.assertThat(received) + .hasSize(2) + .containsExactly( + Tuples.of("value_to_expire" + i, reconnectMono), + Tuples.of("value_to_not_expire" + i, reconnectMono)); + } else { + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value_to_expire" + i, reconnectMono)); + } + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribers() { + 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(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + Assertions.assertThat(cold.subscribeCount()).isZero(); + + RaceTestUtils.race( + () -> reconnectMono.subscribe(processor), () -> reconnectMono.subscribe(racerProcessor)); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + Assertions.assertThat(racerProcessor.isTerminated()).isTrue(); + + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + Assertions.assertThat(racerProcessor.peek()).isEqualTo("value" + i); + + Assertions.assertThat(reconnectMono.resolvingInner.subscribers) + .isEqualTo(ResolvingOperator.READY); + + Assertions.assertThat(cold.subscribeCount()).isOne(); + + Assertions.assertThat( + reconnectMono.resolvingInner.add( + new ResolvingOperator.MonoDeferredResolutionOperator<>( + reconnectMono.resolvingInner, processor))) + .isEqualTo(ResolvingOperator.READY_STATE); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value" + i, reconnectMono)); + + received.clear(); + } + } + + @Test + public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { + Duration timeout = Duration.ofMillis(100); + for (int i = 0; i < 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(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + Assertions.assertThat(cold.subscribeCount()).isZero(); + + String[] values = new String[1]; + + RaceTestUtils.race( + () -> values[0] = reconnectMono.block(timeout), () -> reconnectMono.subscribe(processor)); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + Assertions.assertThat(values).containsExactly("value" + i); + + Assertions.assertThat(reconnectMono.resolvingInner.subscribers) + .isEqualTo(ResolvingOperator.READY); + + Assertions.assertThat(cold.subscribeCount()).isOne(); + + Assertions.assertThat( + reconnectMono.resolvingInner.add( + new ResolvingOperator.MonoDeferredResolutionOperator<>( + reconnectMono.resolvingInner, processor))) + .isEqualTo(ResolvingOperator.READY_STATE); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value" + i, reconnectMono)); + + received.clear(); + } + } + + @Test + public void shouldEstablishValueOnceInCaseOfRacingBetweenBlocks() { + Duration timeout = Duration.ofMillis(100); + for (int i = 0; i < 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(); + + Assertions.assertThat(cold.subscribeCount()).isZero(); + + String[] values1 = new String[1]; + String[] values2 = new String[1]; + + RaceTestUtils.race( + () -> values1[0] = reconnectMono.block(timeout), + () -> values2[0] = reconnectMono.block(timeout)); + + Assertions.assertThat(values2).containsExactly("value" + i); + Assertions.assertThat(values1).containsExactly("value" + i); + + Assertions.assertThat(reconnectMono.resolvingInner.subscribers) + .isEqualTo(ResolvingOperator.READY); + + Assertions.assertThat(cold.subscribeCount()).isOne(); + + Assertions.assertThat( + reconnectMono.resolvingInner.add( + new ResolvingOperator.MonoDeferredResolutionOperator<>( + reconnectMono.resolvingInner, MonoProcessor.create()))) + .isEqualTo(ResolvingOperator.READY_STATE); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value" + i, reconnectMono)); + + received.clear(); + } + } + + @Test + public void shouldExpireValueOnRacingDisposeAndNoValueComplete() { + Hooks.onErrorDropped(t -> {}); + for (int i = 0; i < 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()); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + RaceTestUtils.race(cold::complete, reconnectMono::dispose); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + + Throwable error = processor.getError(); + + if (error instanceof CancellationException) { + Assertions.assertThat(error) + .isInstanceOf(CancellationException.class) + .hasMessage("ReconnectMono has already been disposed"); + } else { + Assertions.assertThat(error) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Source completed empty"); + } + + Assertions.assertThat(expired).isEmpty(); + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldExpireValueOnRacingDisposeAndComplete() { + Hooks.onErrorDropped(t -> {}); + 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()); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + cold.next("value" + i); + + RaceTestUtils.race(cold::complete, reconnectMono::dispose); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + + if (processor.isError()) { + Assertions.assertThat(processor.getError()) + .isInstanceOf(CancellationException.class) + .hasMessage("ReconnectMono has already been disposed"); + } else { + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value" + i, reconnectMono)); + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + } + + Assertions.assertThat(expired).hasSize(1).containsOnly("value" + i); + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldExpireValueOnRacingDisposeAndError() { + Hooks.onErrorDropped(t -> {}); + RuntimeException runtimeException = new RuntimeException("test"); + for (int i = 0; i < 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()); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + cold.next("value" + i); + + RaceTestUtils.race(() -> cold.error(runtimeException), reconnectMono::dispose); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + + if (processor.isError()) { + if (processor.getError() instanceof CancellationException) { + Assertions.assertThat(processor.getError()) + .isInstanceOf(CancellationException.class) + .hasMessage("ReconnectMono has already been disposed"); + } else { + Assertions.assertThat(processor.getError()) + .isInstanceOf(RuntimeException.class) + .hasMessage("test"); + } + } else { + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value" + i, reconnectMono)); + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + } + + Assertions.assertThat(expired).hasSize(1).containsOnly("value" + i); + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldExpireValueOnRacingDisposeAndErrorWithNoBackoff() { + Hooks.onErrorDropped(t -> {}); + RuntimeException runtimeException = new RuntimeException("test"); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final TestPublisher cold = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + cold.mono() + .retryWhen(Retry.max(1).filter(t -> t instanceof Exception)) + .as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + cold.next("value" + i); + + RaceTestUtils.race(() -> cold.error(runtimeException), reconnectMono::dispose); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + + if (processor.isError()) { + + if (processor.getError() instanceof CancellationException) { + Assertions.assertThat(processor.getError()) + .isInstanceOf(CancellationException.class) + .hasMessage("ReconnectMono has already been disposed"); + } else { + Assertions.assertThat(processor.getError()) + .matches(t -> Exceptions.isRetryExhausted(t)) + .hasCause(runtimeException); + } + + Assertions.assertThat(expired).hasSize(1).containsOnly("value" + i); + } else { + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value" + i, reconnectMono)); + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + } + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldThrowOnBlocking() { + final TestPublisher publisher = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + Assertions.assertThatThrownBy(() -> reconnectMono.block(Duration.ofMillis(100))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Timeout on Mono blocking read"); + } + + @Test + public void shouldThrowOnBlockingIfHasAlreadyTerminated() { + final TestPublisher publisher = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + publisher.error(new RuntimeException("test")); + + Assertions.assertThatThrownBy(() -> reconnectMono.block(Duration.ofMillis(100))) + .isInstanceOf(RuntimeException.class) + .hasMessage("test") + .hasSuppressedException(new Exception("Terminated with an error")); + } + + @Test + public void shouldBeScannable() { + final TestPublisher publisher = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final Mono parent = publisher.mono(); + final ReconnectMono reconnectMono = + parent.as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final Scannable scannableOfReconnect = Scannable.from(reconnectMono); + + Assertions.assertThat( + (List) + scannableOfReconnect.parents().map(s -> s.getClass()).collect(Collectors.toList())) + .hasSize(1) + .containsExactly(publisher.mono().getClass()); + Assertions.assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.TERMINATED)) + .isEqualTo(false); + Assertions.assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.ERROR)).isNull(); + + final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + + final Scannable scannableOfMonoProcessor = Scannable.from(processor); + + Assertions.assertThat( + (List) + scannableOfMonoProcessor + .parents() + .map(s -> s.getClass()) + .collect(Collectors.toList())) + .hasSize(4) + .containsExactly( + ResolvingOperator.MonoDeferredResolutionOperator.class, + ReconnectMono.ResolvingInner.class, + ReconnectMono.class, + publisher.mono().getClass()); + + reconnectMono.dispose(); + + Assertions.assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.TERMINATED)) + .isEqualTo(true); + Assertions.assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.ERROR)) + .isInstanceOf(CancellationException.class); + } + + @Test + public void shouldNotExpiredIfNotCompleted() { + final TestPublisher publisher = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + MonoProcessor processor = MonoProcessor.create(); + + reconnectMono.subscribe(processor); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(processor.isTerminated()).isFalse(); + + publisher.next("test"); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(processor.isTerminated()).isFalse(); + + reconnectMono.invalidate(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(processor.isTerminated()).isFalse(); + publisher.assertSubscribers(1); + Assertions.assertThat(publisher.subscribeCount()).isEqualTo(1); + + publisher.complete(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).hasSize(1); + Assertions.assertThat(processor.isTerminated()).isTrue(); + + publisher.assertSubscribers(0); + Assertions.assertThat(publisher.subscribeCount()).isEqualTo(1); + } + + @Test + public void shouldNotEmitUntilCompletion() { + final TestPublisher publisher = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + MonoProcessor processor = MonoProcessor.create(); + + reconnectMono.subscribe(processor); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(processor.isTerminated()).isFalse(); + + publisher.next("test"); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(processor.isTerminated()).isFalse(); + + publisher.complete(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).hasSize(1); + Assertions.assertThat(processor.isTerminated()).isTrue(); + Assertions.assertThat(processor.peek()).isEqualTo("test"); + } + + @Test + public void shouldBePossibleToRemoveThemSelvesFromTheList_CancellationTest() { + final TestPublisher publisher = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + MonoProcessor processor = MonoProcessor.create(); + + reconnectMono.subscribe(processor); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(processor.isTerminated()).isFalse(); + + publisher.next("test"); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(processor.isTerminated()).isFalse(); + + processor.cancel(); + + Assertions.assertThat(reconnectMono.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(); + } + + @Test + public void shouldExpireValueOnDispose() { + final TestPublisher publisher = TestPublisher.create(); + // given + final int timeout = 10; + + final ReconnectMono reconnectMono = + publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + StepVerifier.create(reconnectMono) + .expectSubscription() + .then(() -> publisher.next("value")) + .expectNext("value") + .expectComplete() + .verify(Duration.ofSeconds(timeout)); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).hasSize(1); + + reconnectMono.dispose(); + + Assertions.assertThat(expired).hasSize(1); + Assertions.assertThat(received).hasSize(1); + Assertions.assertThat(reconnectMono.isDisposed()).isTrue(); + + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + .expectSubscription() + .expectError(CancellationException.class) + .verify(Duration.ofSeconds(timeout)); + } + + @Test + public void shouldNotifyAllTheSubscribers() { + final TestPublisher publisher = TestPublisher.create(); + + final ReconnectMono reconnectMono = + publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final MonoProcessor sub1 = MonoProcessor.create(); + final MonoProcessor sub2 = MonoProcessor.create(); + final MonoProcessor sub3 = MonoProcessor.create(); + final MonoProcessor sub4 = MonoProcessor.create(); + + reconnectMono.subscribe(sub1); + reconnectMono.subscribe(sub2); + reconnectMono.subscribe(sub3); + reconnectMono.subscribe(sub4); + + Assertions.assertThat(reconnectMono.resolvingInner.subscribers).hasSize(4); + + final ArrayList> processors = new ArrayList<>(200); + + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final MonoProcessor subA = MonoProcessor.create(); + final MonoProcessor subB = MonoProcessor.create(); + processors.add(subA); + processors.add(subB); + RaceTestUtils.race(() -> reconnectMono.subscribe(subA), () -> reconnectMono.subscribe(subB)); + } + + Assertions.assertThat(reconnectMono.resolvingInner.subscribers) + .hasSize(RaceTestConstants.REPEATS * 2 + 4); + + sub1.dispose(); + + Assertions.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"); + + for (MonoProcessor sub : processors) { + Assertions.assertThat(sub.peek()).isEqualTo("value"); + Assertions.assertThat(sub.isTerminated()).isTrue(); + } + + Assertions.assertThat(publisher.subscribeCount()).isEqualTo(1); + } + + @Test + public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidates() { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final TestPublisher cold = TestPublisher.createCold(); + cold.next("value"); + final int timeout = 10; + + final ReconnectMono reconnectMono = + cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + .expectSubscription() + .expectNext("value") + .expectComplete() + .verify(Duration.ofSeconds(timeout)); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); + + RaceTestUtils.race(reconnectMono::invalidate, reconnectMono::invalidate); + + Assertions.assertThat(expired).hasSize(1).containsOnly("value"); + Assertions.assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); + + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + .expectSubscription() + .expectNext("value") + .expectComplete() + .verify(Duration.ofSeconds(timeout)); + + Assertions.assertThat(expired).hasSize(1).containsOnly("value"); + Assertions.assertThat(received) + .hasSize(2) + .containsOnly(Tuples.of("value", reconnectMono), Tuples.of("value", reconnectMono)); + + Assertions.assertThat(cold.subscribeCount()).isEqualTo(2); + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidateAndDispose() { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final TestPublisher cold = TestPublisher.createCold(); + cold.next("value"); + final int timeout = 10000; + + final ReconnectMono reconnectMono = + cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + .expectSubscription() + .expectNext("value") + .expectComplete() + .verify(Duration.ofSeconds(timeout)); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); + + RaceTestUtils.race(reconnectMono::invalidate, reconnectMono::dispose); + + Assertions.assertThat(expired).hasSize(1).containsOnly("value"); + Assertions.assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); + + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + .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)); + + Assertions.assertThat(cold.subscribeCount()).isEqualTo(1); + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldTimeoutRetryWithVirtualTime() { + // given + final int minBackoff = 1; + final int maxBackoff = 5; + final int timeout = 10; + + // then + StepVerifier.withVirtualTime( + () -> + Mono.error(new RuntimeException("Something went wrong")) + .retryWhen( + Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(minBackoff)) + .doAfterRetry(onRetry()) + .maxBackoff(Duration.ofSeconds(maxBackoff))) + .timeout(Duration.ofSeconds(timeout)) + .as(m -> new ReconnectMono<>(m, onExpire(), onValue())) + .subscribeOn(Schedulers.elastic())) + .expectSubscription() + .thenAwait(Duration.ofSeconds(timeout)) + .expectError(TimeoutException.class) + .verify(Duration.ofSeconds(timeout)); + + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(expired).isEmpty(); + } + + @Test + public void ensuresThatMainSubscriberAllowsOnlyTerminationWithValue() { + final int timeout = 10; + final ReconnectMono reconnectMono = + new ReconnectMono<>(Mono.empty(), onExpire(), onValue()); + + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + .expectSubscription() + .expectErrorSatisfies( + t -> + Assertions.assertThat(t) + .hasMessage("Source completed empty") + .isInstanceOf(IllegalStateException.class)) + .verify(Duration.ofSeconds(timeout)); + } + + @Test + public void monoRetryNoBackoff() { + Mono mono = + Mono.error(new IOException()) + .retryWhen(Retry.max(2).doAfterRetry(onRetry())) + .as(m -> new ReconnectMono<>(m, onExpire(), onValue())); + + StepVerifier.create(mono).verifyErrorMatches(Exceptions::isRetryExhausted); + assertRetries(IOException.class, IOException.class); + + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(expired).isEmpty(); + } + + @Test + public void monoRetryFixedBackoff() { + Mono mono = + Mono.error(new IOException()) + .retryWhen(Retry.fixedDelay(1, Duration.ofMillis(500)).doAfterRetry(onRetry())) + .as(m -> new ReconnectMono<>(m, onExpire(), onValue())); + + StepVerifier.withVirtualTime(() -> mono) + .expectSubscription() + .expectNoEvent(Duration.ofMillis(300)) + .thenAwait(Duration.ofMillis(300)) + .verifyErrorMatches(Exceptions::isRetryExhausted); + + assertRetries(IOException.class); + + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(expired).isEmpty(); + } + + @Test + public void monoRetryExponentialBackoff() { + Mono mono = + Mono.error(new IOException()) + .retryWhen( + Retry.backoff(4, Duration.ofMillis(100)) + .maxBackoff(Duration.ofMillis(500)) + .jitter(0.0d) + .doAfterRetry(onRetry())) + .as(m -> new ReconnectMono<>(m, onExpire(), onValue())); + + StepVerifier.withVirtualTime(() -> mono) + .expectSubscription() + .thenAwait(Duration.ofMillis(100)) + .thenAwait(Duration.ofMillis(200)) + .thenAwait(Duration.ofMillis(400)) + .thenAwait(Duration.ofMillis(500)) + .verifyErrorMatches(Exceptions::isRetryExhausted); + + assertRetries(IOException.class, IOException.class, IOException.class, IOException.class); + + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(expired).isEmpty(); + } + + Consumer onRetry() { + return context -> retries.add(context); + } + + BiConsumer onValue() { + return (v, __) -> received.add(Tuples.of(v, __)); + } + + Consumer onExpire() { + return (v) -> expired.add(v); + } + + @SafeVarargs + private final void assertRetries(Class... exceptions) { + assertEquals(exceptions.length, retries.size()); + int index = 0; + for (Iterator it = retries.iterator(); it.hasNext(); ) { + Retry.RetrySignal retryContext = it.next(); + assertEquals(index, retryContext.totalRetries()); + assertEquals(exceptions[index], retryContext.failure().getClass()); + index++; + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java b/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java new file mode 100644 index 000000000..15bc0a143 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/ResolvingOperatorTests.java @@ -0,0 +1,964 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import io.rsocket.RaceTestConstants; +import io.rsocket.internal.subscriber.AssertSubscriber; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Queue; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.MonoProcessor; +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 < RaceTestConstants.REPEATS; i++) { + final int index = i; + + MonoProcessor processor = MonoProcessor.create(); + BiConsumer consumer = + (v, t) -> { + if (t != null) { + processor.onError(t); + return; + } + + processor.onNext(v); + }; + + ResolvingTest.create() + .assertNothingExpired() + .assertNothingReceived() + .assertPendingResolution() + .thenAddObserver(consumer) + .assertPendingSubscribers(1) + .assertPendingResolution() + .then(self -> RaceTestUtils.race(() -> self.complete("value" + index), self::dispose)) + .assertDisposeCalled(1) + .assertExpiredExactly("value" + index) + .ifResolvedAssertEqual("value" + index) + .assertIsDisposed(); + + if (processor.isError()) { + Assertions.assertThat(processor.getError()) + .isInstanceOf(CancellationException.class) + .hasMessage("Disposed"); + + } else { + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + } + } + } + + @Test + public void shouldNotifyAllTheSubscribersUnderRacingBetweenSubscribeAndComplete() { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final String valueToSend = "value" + i; + + MonoProcessor processor = MonoProcessor.create(); + BiConsumer consumer = + (v, t) -> { + if (t != null) { + processor.onError(t); + return; + } + + processor.onNext(v); + }; + + MonoProcessor processor2 = MonoProcessor.create(); + BiConsumer consumer2 = + (v, t) -> { + if (t != null) { + processor2.onError(t); + return; + } + + processor2.onNext(v); + }; + + ResolvingTest.create() + .assertNothingExpired() + .assertNothingReceived() + .assertPendingSubscribers(0) + .assertPendingResolution() + .then( + self -> { + RaceTestUtils.race(() -> self.complete(valueToSend), () -> self.observe(consumer)); + + StepVerifier.create(processor) + .expectNext(valueToSend) + .expectComplete() + .verify(Duration.ofMillis(10)); + }) + .assertDisposeCalled(0) + .assertReceivedExactly(valueToSend) + .assertNothingExpired() + .thenAddObserver(consumer2) + .assertPendingSubscribers(0); + + StepVerifier.create(processor2) + .expectNext(valueToSend) + .expectComplete() + .verify(Duration.ofMillis(10)); + } + } + + @Test + public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidate() { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final String valueToSend = "value" + i; + final String valueToSend2 = "value2" + i; + + MonoProcessor processor = MonoProcessor.create(); + BiConsumer consumer = + (v, t) -> { + if (t != null) { + processor.onError(t); + return; + } + + processor.onNext(v); + }; + + MonoProcessor processor2 = MonoProcessor.create(); + BiConsumer consumer2 = + (v, t) -> { + if (t != null) { + processor2.onError(t); + return; + } + + processor2.onNext(v); + }; + + ResolvingTest.create() + .assertNothingExpired() + .assertNothingReceived() + .assertPendingSubscribers(0) + .assertPendingResolution() + .thenAddObserver(consumer) + .then( + self -> { + self.complete(valueToSend); + + StepVerifier.create(processor) + .expectNext(valueToSend) + .expectComplete() + .verify(Duration.ofMillis(10)); + }) + .assertReceivedExactly(valueToSend) + .then( + self -> + RaceTestUtils.race( + self::invalidate, + () -> { + self.observe(consumer2); + if (!processor2.isTerminated()) { + self.complete(valueToSend2); + } + })) + .then( + self -> { + if (self.isPending()) { + self.assertReceivedExactly(valueToSend); + } else { + self.assertReceivedExactly(valueToSend, valueToSend2); + } + }) + .assertExpiredExactly(valueToSend) + .assertPendingSubscribers(0) + .assertDisposeCalled(0) + .then( + self -> + StepVerifier.create(processor2) + .expectNextMatches( + (v) -> { + if (self.subscribers == ResolvingOperator.READY) { + return v.equals(valueToSend2); + } else { + return v.equals(valueToSend); + } + }) + .expectComplete() + .verify(Duration.ofMillis(100))); + } + } + + @Test + public void shouldNotExpireNewlyResolvedValueIfSubscribeIsRacingWithInvalidates() { + 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(); + BiConsumer consumer = + (v, t) -> { + if (t != null) { + processor.onError(t); + return; + } + + processor.onNext(v); + }; + + MonoProcessor processor2 = MonoProcessor.create(); + BiConsumer consumer2 = + (v, t) -> { + if (t != null) { + processor2.onError(t); + return; + } + + processor2.onNext(v); + }; + + ResolvingTest.create() + .assertNothingExpired() + .assertNothingReceived() + .assertPendingSubscribers(0) + .assertPendingResolution() + .thenAddObserver(consumer) + .then( + self -> { + self.complete(valueToSend); + + StepVerifier.create(processor) + .expectNext(valueToSend) + .expectComplete() + .verify(Duration.ofMillis(10)); + }) + .assertReceivedExactly(valueToSend) + .then( + self -> + RaceTestUtils.race( + self::invalidate, + self::invalidate, + () -> { + self.observe(consumer2); + if (!processor2.isTerminated()) { + self.complete(valueToSend2); + } + })) + .then( + self -> { + if (!self.isPending()) { + self.assertReceivedExactly(valueToSend, valueToSend2); + } else { + if (self.received.size() > 1) { + self.assertReceivedExactly(valueToSend, valueToSend2); + } else { + self.assertReceivedExactly(valueToSend); + } + } + + Assertions.assertThat(self.expired) + .haveAtMost( + 2, + new Condition<>( + new Predicate() { + int time = 0; + + @Override + public boolean test(Object s) { + if (time++ == 0) { + return valueToSend.equals(s); + } else { + return valueToSend2.equals(s); + } + } + }, + "should matches one of the given values")); + }) + .assertPendingSubscribers(0) + .assertDisposeCalled(0) + .then( + new Consumer>() { + @Override + public void accept(ResolvingTest self) { + StepVerifier.create(processor2) + .expectNextMatches( + (v) -> { + if (self.subscribers == ResolvingOperator.READY) { + return v.equals(valueToSend2); + } else { + return v.equals(valueToSend) || v.equals(valueToSend2); + } + }) + .expectComplete() + .verify(Duration.ofMillis(100)); + } + }); + } + } + + @Test + public void shouldNotExpireNewlyResolvedValueIfBlockIsRacingWithInvalidate() { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final String valueToSend = "value" + i; + final String valueToSend2 = "value2" + i; + + MonoProcessor processor = MonoProcessor.create(); + BiConsumer consumer = + (v, t) -> { + if (t != null) { + processor.onError(t); + return; + } + + processor.onNext(v); + }; + + ResolvingTest.create() + .assertNothingExpired() + .assertNothingReceived() + .assertPendingSubscribers(0) + .assertPendingResolution() + .thenAddObserver(consumer) + .then( + self -> { + self.complete(valueToSend); + + StepVerifier.create(processor) + .expectNext(valueToSend) + .expectComplete() + .verify(Duration.ofMillis(10)); + }) + .assertReceivedExactly(valueToSend) + .then( + self -> + RaceTestUtils.race( + () -> + Assertions.assertThat(self.block(null)) + .matches((v) -> v.equals(valueToSend) || v.equals(valueToSend2)), + self::invalidate, + () -> { + for (; ; ) { + if (self.subscribers != ResolvingOperator.READY) { + self.complete(valueToSend2); + break; + } + } + })) + .then( + self -> { + if (self.isPending()) { + self.assertReceivedExactly(valueToSend); + } else { + self.assertReceivedExactly(valueToSend, valueToSend2); + } + }) + .assertExpiredExactly(valueToSend) + .assertPendingSubscribers(0) + .assertDisposeCalled(0); + } + } + + @Test + public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribers() { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final String valueToSend = "value" + i; + + MonoProcessor processor = MonoProcessor.create(); + BiConsumer consumer = + (v, t) -> { + if (t != null) { + processor.onError(t); + return; + } + + processor.onNext(v); + }; + + MonoProcessor processor2 = MonoProcessor.create(); + BiConsumer consumer2 = + (v, t) -> { + if (t != null) { + processor2.onError(t); + return; + } + + processor2.onNext(v); + }; + + ResolvingTest.create() + .assertNothingExpired() + .assertNothingReceived() + .assertPendingSubscribers(0) + .assertPendingResolution() + .then( + self -> + RaceTestUtils.race(() -> self.observe(consumer), () -> self.observe(consumer2))) + .assertSubscribeCalled(1) + .assertPendingSubscribers(2) + .then(self -> self.complete(valueToSend)) + .assertPendingSubscribers(0) + .assertReceivedExactly(valueToSend) + .assertNothingExpired() + .assertDisposeCalled(0) + .then( + self -> { + Assertions.assertThat(processor.isTerminated()).isTrue(); + Assertions.assertThat(processor2.isTerminated()).isTrue(); + + Assertions.assertThat(processor.peek()).isEqualTo(valueToSend); + Assertions.assertThat(processor2.peek()).isEqualTo(valueToSend); + + Assertions.assertThat(self.subscribers).isEqualTo(ResolvingOperator.READY); + + Assertions.assertThat(self.add(consumer)).isEqualTo(ResolvingOperator.READY_STATE); + }); + } + } + + @Test + public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final String valueToSend = "value" + i; + + MonoProcessor processor = MonoProcessor.create(); + + MonoProcessor processor2 = MonoProcessor.create(); + BiConsumer consumer2 = + (v, t) -> { + if (t != null) { + processor2.onError(t); + return; + } + + processor2.onNext(v); + }; + + ResolvingTest.create() + .assertNothingExpired() + .assertNothingReceived() + .assertPendingSubscribers(0) + .assertPendingResolution() + .whenSubscribe(self -> self.complete(valueToSend)) + .then( + self -> + RaceTestUtils.race( + () -> processor.onNext(self.block(null)), () -> self.observe(consumer2))) + .assertSubscribeCalled(1) + .assertPendingSubscribers(0) + .assertReceivedExactly(valueToSend) + .assertNothingExpired() + .assertDisposeCalled(0) + .then( + self -> { + Assertions.assertThat(processor.isTerminated()).isTrue(); + Assertions.assertThat(processor2.isTerminated()).isTrue(); + + Assertions.assertThat(processor.peek()).isEqualTo(valueToSend); + Assertions.assertThat(processor2.peek()).isEqualTo(valueToSend); + + Assertions.assertThat(self.subscribers).isEqualTo(ResolvingOperator.READY); + + Assertions.assertThat(self.add(consumer2)).isEqualTo(ResolvingOperator.READY_STATE); + }); + } + } + + @Test + public void shouldEstablishValueOnceInCaseOfRacingBetweenBlocks() { + Duration timeout = Duration.ofMillis(100); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final String valueToSend = "value" + i; + + MonoProcessor processor = MonoProcessor.create(); + MonoProcessor processor2 = MonoProcessor.create(); + + ResolvingTest.create() + .assertNothingExpired() + .assertNothingReceived() + .assertPendingSubscribers(0) + .assertPendingResolution() + .whenSubscribe(self -> self.complete(valueToSend)) + .then( + self -> + RaceTestUtils.race( + () -> processor.onNext(self.block(timeout)), + () -> processor2.onNext(self.block(timeout)))) + .assertSubscribeCalled(1) + .assertPendingSubscribers(0) + .assertReceivedExactly(valueToSend) + .assertNothingExpired() + .assertDisposeCalled(0) + .then( + self -> { + Assertions.assertThat(processor.isTerminated()).isTrue(); + Assertions.assertThat(processor2.isTerminated()).isTrue(); + + Assertions.assertThat(processor.peek()).isEqualTo(valueToSend); + Assertions.assertThat(processor2.peek()).isEqualTo(valueToSend); + + Assertions.assertThat(self.subscribers).isEqualTo(ResolvingOperator.READY); + + Assertions.assertThat(self.add((v, t) -> {})) + .isEqualTo(ResolvingOperator.READY_STATE); + }); + } + } + + @Test + public void shouldExpireValueOnRacingDisposeAndError() { + Hooks.onErrorDropped(t -> {}); + RuntimeException runtimeException = new RuntimeException("test"); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + MonoProcessor processor = MonoProcessor.create(); + BiConsumer consumer = + (v, t) -> { + if (t != null) { + processor.onError(t); + return; + } + + processor.onNext(v); + }; + MonoProcessor processor2 = MonoProcessor.create(); + BiConsumer consumer2 = + (v, t) -> { + if (t != null) { + processor2.onError(t); + return; + } + + processor2.onNext(v); + }; + + ResolvingTest.create() + .assertNothingExpired() + .assertNothingReceived() + .assertPendingSubscribers(0) + .assertPendingResolution() + .thenAddObserver(consumer) + .assertSubscribeCalled(1) + .assertPendingSubscribers(1) + .then(self -> RaceTestUtils.race(() -> self.terminate(runtimeException), self::dispose)) + .assertPendingSubscribers(0) + .assertNothingExpired() + .assertDisposeCalled(1) + .then( + self -> { + Assertions.assertThat(self.subscribers).isEqualTo(ResolvingOperator.TERMINATED); + + Assertions.assertThat(self.add((v, t) -> {})) + .isEqualTo(ResolvingOperator.TERMINATED_STATE); + }) + .thenAddObserver(consumer2); + + StepVerifier.create(processor) + .expectErrorSatisfies( + t -> { + if (t instanceof CancellationException) { + Assertions.assertThat(t) + .isInstanceOf(CancellationException.class) + .hasMessage("Disposed"); + } else { + Assertions.assertThat(t).isInstanceOf(RuntimeException.class).hasMessage("test"); + } + }) + .verify(Duration.ofMillis(10)); + + StepVerifier.create(processor2) + .expectErrorSatisfies( + t -> { + if (t instanceof CancellationException) { + Assertions.assertThat(t) + .isInstanceOf(CancellationException.class) + .hasMessage("Disposed"); + } 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()) + // .isEqualTo(processor2.getError()); + } + } + + @Test + public void shouldThrowOnBlocking() { + ResolvingTest.create() + .assertNothingExpired() + .assertNothingReceived() + .assertPendingSubscribers(0) + .assertPendingResolution() + .then( + self -> + Assertions.assertThatThrownBy(() -> self.block(Duration.ofMillis(100))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Timeout on Mono blocking read")) + .assertPendingSubscribers(0) + .assertNothingExpired() + .assertNothingReceived() + .assertDisposeCalled(0); + } + + @Test + public void shouldThrowOnBlockingIfHasAlreadyTerminated() { + ResolvingTest.create() + .assertNothingExpired() + .assertNothingReceived() + .assertPendingSubscribers(0) + .assertPendingResolution() + .whenSubscribe(self -> self.terminate(new RuntimeException("test"))) + .then( + self -> + Assertions.assertThatThrownBy(() -> self.block(Duration.ofMillis(100))) + .isInstanceOf(RuntimeException.class) + .hasMessage("test") + .hasSuppressedException(new Exception("Terminated with an error"))) + .assertPendingSubscribers(0) + .assertNothingExpired() + .assertNothingReceived() + .assertDisposeCalled(1); + } + + static Stream, Publisher>> innerCases() { + return Stream.of( + (self) -> { + final MonoProcessor processor = MonoProcessor.create(); + final ResolvingOperator.DeferredResolution operator = + new ResolvingOperator.DeferredResolution(self, processor) { + @Override + public void accept(String v, Throwable t) { + if (t != null) { + onError(t); + return; + } + + onNext(v); + } + }; + return processor.doOnSubscribe(s -> self.observe(operator)).doOnCancel(operator::cancel); + }, + (self) -> { + final MonoProcessor processor = MonoProcessor.create(); + final ResolvingOperator.MonoDeferredResolutionOperator operator = + new ResolvingOperator.MonoDeferredResolutionOperator<>(self, processor); + processor.onSubscribe(operator); + return processor.doOnSubscribe(s -> self.observe(operator)).doOnCancel(operator::cancel); + }); + } + + @ParameterizedTest + @MethodSource("innerCases") + public void shouldBePossibleToRemoveThemSelvesFromTheList_CancellationTest( + Function, Publisher> caseProducer) { + ResolvingTest.create() + .then( + self -> { + Publisher resolvingInner = caseProducer.apply(self); + StepVerifier.create(resolvingInner) + .expectSubscription() + .then(() -> self.assertSubscribeCalled(1).assertPendingSubscribers(1)) + .thenCancel() + .verify(Duration.ofMillis(100)); + }) + .assertPendingSubscribers(0) + .assertNothingExpired() + .then(self -> self.complete("test")) + .assertReceivedExactly("test"); + } + + @ParameterizedTest + @MethodSource("innerCases") + public void shouldExpireValueOnDispose( + Function, Publisher> caseProducer) { + ResolvingTest.create() + .then( + self -> { + Publisher resolvingInner = caseProducer.apply(self); + + StepVerifier.create(resolvingInner) + .expectSubscription() + .then(() -> self.complete("test")) + .expectNext("test") + .expectComplete() + .verify(Duration.ofMillis(100)); + }) + .assertPendingSubscribers(0) + .assertNothingExpired() + .assertReceivedExactly("test") + .then(ResolvingOperator::dispose) + .assertExpiredExactly("test") + .assertDisposeCalled(1); + } + + @ParameterizedTest + @MethodSource("innerCases") + 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(); + + final ArrayList> processors = + new ArrayList<>(RaceTestConstants.REPEATS * 2); + + ResolvingTest.create() + .assertDisposeCalled(0) + .assertPendingSubscribers(0) + .assertNothingExpired() + .assertNothingReceived() + .assertPendingResolution() + .then( + self -> { + caseProducer.apply(self).subscribe(sub1); + caseProducer.apply(self).subscribe(sub2); + caseProducer.apply(self).subscribe(sub3); + caseProducer.apply(self).subscribe(sub4); + }) + .assertSubscribeCalled(1) + .assertPendingSubscribers(4) + .then( + self -> { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final MonoProcessor subA = MonoProcessor.create(); + final MonoProcessor subB = MonoProcessor.create(); + processors.add(subA); + processors.add(subB); + RaceTestUtils.race( + () -> caseProducer.apply(self).subscribe(subA), + () -> caseProducer.apply(self).subscribe(subB)); + } + }) + .assertSubscribeCalled(1) + .assertPendingSubscribers(RaceTestConstants.REPEATS * 2 + 4) + .then(self -> sub1.dispose()) + .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); + + for (MonoProcessor sub : processors) { + Assertions.assertThat(sub.peek()).isEqualTo(valueToSend); + Assertions.assertThat(sub.isTerminated()).isTrue(); + } + }) + .assertPendingSubscribers(0) + .assertNothingExpired() + .assertReceivedExactly("value"); + } + + @Test + public void shouldBeSerialIfRacyMonoInner() { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + long[] requested = new long[] {0}; + Subscription mockSubscription = Mockito.mock(Subscription.class); + Mockito.doAnswer( + a -> { + long argument = a.getArgument(0); + return requested[0] += argument; + }) + .when(mockSubscription) + .request(Mockito.anyLong()); + ResolvingOperator.DeferredResolution resolution = + new ResolvingOperator.DeferredResolution( + ResolvingTest.create(), AssertSubscriber.create(0)) { + + @Override + public void accept(Object o, Object o2) {} + }; + + resolution.request(5); + + RaceTestUtils.race( + () -> resolution.onSubscribe(mockSubscription), + () -> { + resolution.request(10); + resolution.request(10); + resolution.request(10); + }); + + resolution.request(15); + + Assertions.assertThat(requested[0]).isEqualTo(50L); + } + } + + @Test + public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidates() { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + ResolvingTest.create() + .assertNothingExpired() + .assertNothingReceived() + .assertPendingResolution() + .then(self -> self.complete("test")) + .assertReceivedExactly("test") + .then(self -> RaceTestUtils.race(self::invalidate, self::invalidate)) + .assertExpiredExactly("test"); + } + } + + @Test + public void shouldExpireValueExactlyOnceOnRacingBetweenInvalidateAndDispose() { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + ResolvingTest.create() + .assertNothingExpired() + .assertNothingReceived() + .assertPendingResolution() + .then(self -> self.complete("test")) + .assertReceivedExactly("test") + .then(self -> RaceTestUtils.race(self::invalidate, self::dispose)) + .assertExpiredExactly("test"); + } + } + + static class ResolvingTest extends ResolvingOperator { + + final AtomicInteger subscribeCalls = new AtomicInteger(); + final AtomicInteger onDisposeCalls = new AtomicInteger(); + + final Queue received = new ConcurrentLinkedQueue<>(); + final Queue expired = new ConcurrentLinkedQueue<>(); + + Consumer> whenSubscribeConsumer = (self) -> {}; + + static ResolvingTest create() { + return new ResolvingTest<>(); + } + + public ResolvingTest assertPendingSubscribers(int cnt) { + Assertions.assertThat(this.subscribers.length).isEqualTo(cnt); + + return this; + } + + public ResolvingTest whenSubscribe(Consumer> consumer) { + this.whenSubscribeConsumer = consumer; + return this; + } + + public ResolvingTest then(Consumer> consumer) { + consumer.accept(this); + + return this; + } + + public ResolvingTest thenAddObserver(BiConsumer consumer) { + this.observe(consumer); + return this; + } + + public ResolvingTest assertPendingResolution() { + Assertions.assertThat(this.isPending()).isTrue(); + + return this; + } + + public ResolvingTest assertIsDisposed() { + Assertions.assertThat(this.isDisposed()).isTrue(); + + return this; + } + + public ResolvingTest assertSubscribeCalled(int times) { + Assertions.assertThat(subscribeCalls).hasValue(times); + + return this; + } + + public ResolvingTest assertDisposeCalled(int times) { + Assertions.assertThat(onDisposeCalls).hasValue(times); + return this; + } + + public ResolvingTest assertNothingExpired() { + return assertExpiredExactly(); + } + + public ResolvingTest assertExpiredExactly(T... values) { + Assertions.assertThat(expired).hasSize(values.length).containsExactly(values); + + return this; + } + + public ResolvingTest assertNothingReceived() { + return assertReceivedExactly(); + } + + public ResolvingTest assertReceivedExactly(T... values) { + Assertions.assertThat(received).hasSize(values.length).containsExactly(values); + + return this; + } + + public ResolvingTest ifResolvedAssertEqual(T value) { + if (received.size() > 0) { + Assertions.assertThat(received).hasSize(1).containsExactly(value); + } + + return this; + } + + @Override + protected void doOnValueResolved(T value) { + received.offer(value); + } + + @Override + protected void doOnValueExpired(T value) { + expired.offer(value); + } + + @Override + protected void doSubscribe() { + whenSubscribeConsumer.accept(this); + subscribeCalls.incrementAndGet(); + } + + @Override + protected void doOnDispose() { + onDisposeCalls.incrementAndGet(); + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java new file mode 100644 index 000000000..fe53b7df4 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -0,0 +1,182 @@ +package io.rsocket.core; + +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; +import static io.rsocket.transport.ServerTransport.ConnectionAcceptor; +import static org.assertj.core.api.Assertions.assertThat; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.*; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.exceptions.Exceptions; +import io.rsocket.exceptions.RejectedSetupException; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.SetupFrameCodec; +import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.test.util.TestDuplexConnection; +import io.rsocket.transport.ServerTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.publisher.UnicastProcessor; +import reactor.test.StepVerifier; + +public class SetupRejectionTest { + + @Test + void responderRejectSetup() { + SingleConnectionTransport transport = new SingleConnectionTransport(); + + String errorMsg = "error"; + RejectingAcceptor acceptor = new RejectingAcceptor(errorMsg); + RSocketServer.create().acceptor(acceptor).bind(transport).block(); + + transport.connect(); + + ByteBuf sentFrame = transport.awaitSent(); + assertThat(FrameHeaderCodec.frameType(sentFrame)).isEqualTo(FrameType.ERROR); + RuntimeException error = Exceptions.from(0, sentFrame); + assertThat(errorMsg).isEqualTo(error.getMessage()); + assertThat(error).isInstanceOf(RejectedSetupException.class); + RSocket acceptorSender = acceptor.senderRSocket().block(); + assertThat(acceptorSender.isDisposed()).isTrue(); + } + + @Test + @Disabled("FIXME: needs to be revised") + void requesterStreamsTerminatedOnZeroErrorFrame() { + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + TestDuplexConnection conn = new TestDuplexConnection(allocator); + RSocketRequester rSocket = + new RSocketRequester( + conn, + DefaultPayload::create, + StreamIdSupplier.clientSupplier(), + 0, + FRAME_LENGTH_MASK, + 0, + 0, + null, + RequesterLeaseHandler.None, + TestScheduler.INSTANCE); + + String errorMsg = "error"; + + StepVerifier.create( + rSocket + .requestResponse(DefaultPayload.create("test")) + .doOnRequest( + ignored -> + conn.addToReceivedBuffer( + ErrorFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 0, + new RejectedSetupException(errorMsg))))) + .expectErrorMatches( + err -> err instanceof RejectedSetupException && errorMsg.equals(err.getMessage())) + .verify(Duration.ofSeconds(5)); + + assertThat(rSocket.isDisposed()).isTrue(); + } + + @Test + void requesterNewStreamsTerminatedAfterZeroErrorFrame() { + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + TestDuplexConnection conn = new TestDuplexConnection(allocator); + RSocketRequester rSocket = + new RSocketRequester( + conn, + DefaultPayload::create, + StreamIdSupplier.clientSupplier(), + 0, + FRAME_LENGTH_MASK, + 0, + 0, + null, + RequesterLeaseHandler.None, + TestScheduler.INSTANCE); + + conn.addToReceivedBuffer( + ErrorFrameCodec.encode(ByteBufAllocator.DEFAULT, 0, new RejectedSetupException("error"))); + + StepVerifier.create( + rSocket + .requestResponse(DefaultPayload.create("test")) + .delaySubscription(Duration.ofMillis(100))) + .expectErrorMatches( + err -> err instanceof RejectedSetupException && "error".equals(err.getMessage())) + .verify(Duration.ofSeconds(5)); + } + + private static class RejectingAcceptor implements SocketAcceptor { + private final String errorMessage; + private final UnicastProcessor senderRSockets = UnicastProcessor.create(); + + public RejectingAcceptor(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket) { + senderRSockets.onNext(sendingSocket); + return Mono.error(new RuntimeException(errorMessage)); + } + + public Mono senderRSocket() { + return senderRSockets.next(); + } + } + + private static class SingleConnectionTransport implements ServerTransport { + + private final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + private final TestDuplexConnection conn = new TestDuplexConnection(allocator); + + @Override + public Mono start(ConnectionAcceptor acceptor) { + return Mono.just(new TestCloseable(acceptor, conn)); + } + + public ByteBuf awaitSent() { + try { + return conn.awaitSend(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public void connect() { + Payload payload = DefaultPayload.create(DefaultPayload.EMPTY_BUFFER); + ByteBuf setup = SetupFrameCodec.encode(allocator, false, 0, 42, "mdMime", "dMime", payload); + + conn.addToReceivedBuffer(setup); + } + } + + private static class TestCloseable implements Closeable { + + private final DuplexConnection conn; + + TestCloseable(ConnectionAcceptor acceptor, DuplexConnection conn) { + this.conn = conn; + Mono.from(acceptor.apply(conn)).subscribe(notUsed -> {}, err -> conn.dispose()); + } + + @Override + public Mono onClose() { + return conn.onClose(); + } + + @Override + public void dispose() { + conn.dispose(); + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/StreamIdSupplierTest.java b/rsocket-core/src/test/java/io/rsocket/core/StreamIdSupplierTest.java similarity index 52% rename from rsocket-core/src/test/java/io/rsocket/StreamIdSupplierTest.java rename to rsocket-core/src/test/java/io/rsocket/core/StreamIdSupplierTest.java index 3025b78eb..00248b6d8 100644 --- a/rsocket-core/src/test/java/io/rsocket/StreamIdSupplierTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/StreamIdSupplierTest.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,43 +14,48 @@ * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import io.netty.util.collection.IntObjectMap; +import io.rsocket.internal.SynchronizedIntObjectHashMap; import org.junit.Test; public class StreamIdSupplierTest { @Test public void testClientSequence() { + IntObjectMap map = new SynchronizedIntObjectHashMap<>(); StreamIdSupplier s = StreamIdSupplier.clientSupplier(); - assertEquals(1, s.nextStreamId()); - assertEquals(3, s.nextStreamId()); - assertEquals(5, s.nextStreamId()); + assertEquals(1, s.nextStreamId(map)); + assertEquals(3, s.nextStreamId(map)); + assertEquals(5, s.nextStreamId(map)); } @Test public void testServerSequence() { + IntObjectMap map = new SynchronizedIntObjectHashMap<>(); StreamIdSupplier s = StreamIdSupplier.serverSupplier(); - assertEquals(2, s.nextStreamId()); - assertEquals(4, s.nextStreamId()); - assertEquals(6, s.nextStreamId()); + assertEquals(2, s.nextStreamId(map)); + assertEquals(4, s.nextStreamId(map)); + assertEquals(6, s.nextStreamId(map)); } @Test public void testClientIsValid() { + IntObjectMap map = new SynchronizedIntObjectHashMap<>(); StreamIdSupplier s = StreamIdSupplier.clientSupplier(); assertFalse(s.isBeforeOrCurrent(1)); assertFalse(s.isBeforeOrCurrent(3)); - s.nextStreamId(); + s.nextStreamId(map); assertTrue(s.isBeforeOrCurrent(1)); assertFalse(s.isBeforeOrCurrent(3)); - s.nextStreamId(); + s.nextStreamId(map); assertTrue(s.isBeforeOrCurrent(3)); // negative @@ -63,16 +68,17 @@ public void testClientIsValid() { @Test public void testServerIsValid() { + IntObjectMap map = new SynchronizedIntObjectHashMap<>(); StreamIdSupplier s = StreamIdSupplier.serverSupplier(); assertFalse(s.isBeforeOrCurrent(2)); assertFalse(s.isBeforeOrCurrent(4)); - s.nextStreamId(); + s.nextStreamId(map); assertTrue(s.isBeforeOrCurrent(2)); assertFalse(s.isBeforeOrCurrent(4)); - s.nextStreamId(); + s.nextStreamId(map); assertTrue(s.isBeforeOrCurrent(4)); // negative @@ -82,4 +88,32 @@ public void testServerIsValid() { // client also accepted (checked externally) assertTrue(s.isBeforeOrCurrent(1)); } + + @Test + public void testWrap() { + IntObjectMap map = new SynchronizedIntObjectHashMap<>(); + StreamIdSupplier s = new StreamIdSupplier(Integer.MAX_VALUE - 3); + + assertEquals(2147483646, s.nextStreamId(map)); + assertEquals(2, s.nextStreamId(map)); + assertEquals(4, s.nextStreamId(map)); + + s = new StreamIdSupplier(Integer.MAX_VALUE - 2); + + assertEquals(2147483647, s.nextStreamId(map)); + assertEquals(1, s.nextStreamId(map)); + assertEquals(3, s.nextStreamId(map)); + } + + @Test + public void testSkipFound() { + IntObjectMap map = new SynchronizedIntObjectHashMap<>(); + map.put(5, new Object()); + map.put(9, new Object()); + StreamIdSupplier s = StreamIdSupplier.clientSupplier(); + assertEquals(1, s.nextStreamId(map)); + assertEquals(3, s.nextStreamId(map)); + assertEquals(7, s.nextStreamId(map)); + assertEquals(11, s.nextStreamId(map)); + } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/TestingStuff.java b/rsocket-core/src/test/java/io/rsocket/core/TestingStuff.java new file mode 100644 index 000000000..e0ebf5064 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/TestingStuff.java @@ -0,0 +1,21 @@ +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/ApplicationErrorExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/ApplicationErrorExceptionTest.java new file mode 100644 index 000000000..35b30b951 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/ApplicationErrorExceptionTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +final class ApplicationErrorExceptionTest + implements RSocketExceptionTest { + + @Override + public ApplicationErrorException getException(String message) { + return new ApplicationErrorException(message); + } + + @Override + public ApplicationErrorException getException(String message, Throwable cause) { + return new ApplicationErrorException(message, cause); + } + + @Override + public int getSpecifiedErrorCode() { + return 0x00000201; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/CanceledExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/CanceledExceptionTest.java new file mode 100644 index 000000000..6df9e6a4d --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/CanceledExceptionTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +final class CanceledExceptionTest implements RSocketExceptionTest { + + @Override + public CanceledException getException(String message) { + return new CanceledException(message); + } + + @Override + public CanceledException getException(String message, Throwable cause) { + return new CanceledException(message, cause); + } + + @Override + public int getSpecifiedErrorCode() { + return 0x00000203; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/ConnectionCloseExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/ConnectionCloseExceptionTest.java new file mode 100644 index 000000000..fe98b55de --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/ConnectionCloseExceptionTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +final class ConnectionCloseExceptionTest implements RSocketExceptionTest { + + @Override + public ConnectionCloseException getException(String message) { + return new ConnectionCloseException(message); + } + + @Override + public ConnectionCloseException getException(String message, Throwable cause) { + return new ConnectionCloseException(message, cause); + } + + @Override + public int getSpecifiedErrorCode() { + return 0x00000102; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/ConnectionErrorExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/ConnectionErrorExceptionTest.java new file mode 100644 index 000000000..a2bd45a38 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/ConnectionErrorExceptionTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +final class ConnectionErrorExceptionTest implements RSocketExceptionTest { + + @Override + public ConnectionErrorException getException(String message) { + return new ConnectionErrorException(message); + } + + @Override + public ConnectionErrorException getException(String message, Throwable cause) { + return new ConnectionErrorException(message, cause); + } + + @Override + public int getSpecifiedErrorCode() { + return 0x00000101; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java new file mode 100644 index 000000000..db5c47179 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java @@ -0,0 +1,238 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +import static io.rsocket.frame.ErrorFrameCodec.APPLICATION_ERROR; +import static io.rsocket.frame.ErrorFrameCodec.CANCELED; +import static io.rsocket.frame.ErrorFrameCodec.CONNECTION_CLOSE; +import static io.rsocket.frame.ErrorFrameCodec.CONNECTION_ERROR; +import static io.rsocket.frame.ErrorFrameCodec.INVALID; +import static io.rsocket.frame.ErrorFrameCodec.INVALID_SETUP; +import static io.rsocket.frame.ErrorFrameCodec.REJECTED; +import static io.rsocket.frame.ErrorFrameCodec.REJECTED_RESUME; +import static io.rsocket.frame.ErrorFrameCodec.REJECTED_SETUP; +import static io.rsocket.frame.ErrorFrameCodec.UNSUPPORTED_SETUP; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.rsocket.RaceTestConstants; +import io.rsocket.frame.ErrorFrameCodec; +import java.util.concurrent.ThreadLocalRandom; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +final class ExceptionsTest { + @DisplayName("from returns ApplicationErrorException") + @Test + void fromApplicationException() { + ByteBuf byteBuf = createErrorFrame(1, APPLICATION_ERROR, "test-message"); + + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(ApplicationErrorException.class) + .hasMessage("test-message"); + + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Invalid Error frame in Stream ID 0: 0x%08X '%s'", APPLICATION_ERROR, "test-message"); + } + + @DisplayName("from returns CanceledException") + @Test + void fromCanceledException() { + ByteBuf byteBuf = createErrorFrame(1, 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"); + } + + @DisplayName("from returns ConnectionCloseException") + @Test + void fromConnectionCloseException() { + ByteBuf byteBuf = createErrorFrame(0, CONNECTION_CLOSE, "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"); + } + + @DisplayName("from returns ConnectionErrorException") + @Test + void fromConnectionErrorException() { + ByteBuf byteBuf = createErrorFrame(0, CONNECTION_ERROR, "test-message"); + + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(ConnectionErrorException.class) + .hasMessage("test-message"); + + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", CONNECTION_ERROR, "test-message"); + } + + @DisplayName("from returns IllegalArgumentException if error frame has illegal error code") + @Test + void fromIllegalErrorFrame() { + ByteBuf byteBuf = createErrorFrame(0, 0x00000000, "test-message"); + + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", 0, "test-message") + .isInstanceOf(IllegalArgumentException.class); + + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 1: 0x%08X '%s'", 0x00000000, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("from returns InvalidException") + @Test + void fromInvalidException() { + ByteBuf byteBuf = createErrorFrame(1, INVALID, "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); + } + + @DisplayName("from returns InvalidSetupException") + @Test + void fromInvalidSetupException() { + ByteBuf byteBuf = createErrorFrame(0, INVALID_SETUP, "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); + } + + @DisplayName("from returns RejectedException") + @Test + void fromRejectedException() { + ByteBuf byteBuf = createErrorFrame(1, REJECTED, "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); + } + + @DisplayName("from returns RejectedResumeException") + @Test + void fromRejectedResumeException() { + ByteBuf byteBuf = createErrorFrame(0, REJECTED_RESUME, "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); + } + + @DisplayName("from returns RejectedSetupException") + @Test + void fromRejectedSetupException() { + ByteBuf byteBuf = createErrorFrame(0, REJECTED_SETUP, "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); + } + + @DisplayName("from returns UnsupportedSetupException") + @Test + void fromUnsupportedSetupException() { + ByteBuf byteBuf = createErrorFrame(0, UNSUPPORTED_SETUP, "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); + } + + @DisplayName("from returns CustomRSocketException") + @Test + void fromCustomRSocketException() { + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + int randomCode = + ThreadLocalRandom.current().nextBoolean() + ? ThreadLocalRandom.current() + .nextInt(Integer.MIN_VALUE, ErrorFrameCodec.MAX_USER_ALLOWED_ERROR_CODE) + : 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(); + } + } + + @DisplayName("from throws NullPointerException with null frame") + @Test + void fromWithNullFrame() { + assertThatNullPointerException() + .isThrownBy(() -> Exceptions.from(0, null)) + .withMessage("frame must not be null"); + } + + private ByteBuf createErrorFrame(int streamId, int errorCode, String message) { + return ErrorFrameCodec.encode( + UnpooledByteBufAllocator.DEFAULT, streamId, new TestRSocketException(errorCode, message)); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/InvalidExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/InvalidExceptionTest.java new file mode 100644 index 000000000..a7dec62b4 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/InvalidExceptionTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +final class InvalidExceptionTest implements RSocketExceptionTest { + + @Override + public InvalidException getException(String message) { + return new InvalidException(message); + } + + @Override + public InvalidException getException(String message, Throwable cause) { + return new InvalidException(message, cause); + } + + @Override + public int getSpecifiedErrorCode() { + return 0x00000204; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/InvalidSetupExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/InvalidSetupExceptionTest.java new file mode 100644 index 000000000..d7fce8cc8 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/InvalidSetupExceptionTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +final class InvalidSetupExceptionTest implements RSocketExceptionTest { + + @Override + public InvalidSetupException getException(String message) { + return new InvalidSetupException(message); + } + + @Override + public InvalidSetupException getException(String message, Throwable cause) { + return new InvalidSetupException(message, cause); + } + + @Override + public int getSpecifiedErrorCode() { + return 0x00000001; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/RSocketExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/RSocketExceptionTest.java new file mode 100644 index 000000000..ccf7649d2 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/RSocketExceptionTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +interface RSocketExceptionTest { + + @DisplayName("constructor does not throw NullPointerException with null message") + @Test + default void constructorWithNullMessage() { + assertThat(getException(null)).hasMessage(null); + } + + @DisplayName("constructor does not throw NullPointerException with null message and cause") + @Test + default void constructorWithNullMessageAndCause() { + assertThat(getException(null)).hasMessage(null); + } + + @DisplayName("errorCode returns specified value") + @Test + default void errorCodeReturnsSpecifiedValue() { + assertThat(getException("test-message").errorCode()).isEqualTo(getSpecifiedErrorCode()); + } + + T getException(String message, Throwable cause); + + T getException(String message); + + int getSpecifiedErrorCode(); +} diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/RejectedExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/RejectedExceptionTest.java new file mode 100644 index 000000000..209595596 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/RejectedExceptionTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +final class RejectedExceptionTest implements RSocketExceptionTest { + + @Override + public RejectedException getException(String message) { + return new RejectedException(message); + } + + @Override + public RejectedException getException(String message, Throwable cause) { + return new RejectedException(message, cause); + } + + @Override + public int getSpecifiedErrorCode() { + return 0x00000202; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/RejectedResumeExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/RejectedResumeExceptionTest.java new file mode 100644 index 000000000..555ff160d --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/RejectedResumeExceptionTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +final class RejectedResumeExceptionTest implements RSocketExceptionTest { + + @Override + public RejectedResumeException getException(String message) { + return new RejectedResumeException(message); + } + + @Override + public RejectedResumeException getException(String message, Throwable cause) { + return new RejectedResumeException(message, cause); + } + + @Override + public int getSpecifiedErrorCode() { + return 0x00000004; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/RejectedSetupExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/RejectedSetupExceptionTest.java new file mode 100644 index 000000000..2fe63c09d --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/RejectedSetupExceptionTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +final class RejectedSetupExceptionTest implements RSocketExceptionTest { + + @Override + public RejectedSetupException getException(String message) { + return new RejectedSetupException(message); + } + + @Override + public RejectedSetupException getException(String message, Throwable cause) { + return new RejectedSetupException(message, cause); + } + + @Override + public int getSpecifiedErrorCode() { + return 0x00000003; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/TestRSocketException.java b/rsocket-core/src/test/java/io/rsocket/exceptions/TestRSocketException.java new file mode 100644 index 000000000..6c2e63730 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/TestRSocketException.java @@ -0,0 +1,39 @@ +package io.rsocket.exceptions; + +public class TestRSocketException extends RSocketException { + private static final long serialVersionUID = 7873267740343446585L; + + private final int errorCode; + + /** + * Constructs a new exception with the specified message. + * + * @param errorCode customizable error code + * @param message the message + * @throws NullPointerException if {@code message} is {@code null} + * @throws IllegalArgumentException if {@code errorCode} is out of allowed range + */ + public TestRSocketException(int errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param errorCode customizable error code + * @param message the message + * @param cause the cause of this exception + * @throws NullPointerException if {@code message} or {@code cause} is {@code null} + * @throws IllegalArgumentException if {@code errorCode} is out of allowed range + */ + public TestRSocketException(int errorCode, String message, Throwable cause) { + super(message, cause); + this.errorCode = errorCode; + } + + @Override + public int errorCode() { + return errorCode; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/UnsupportedSetupExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/UnsupportedSetupExceptionTest.java new file mode 100644 index 000000000..6c73ff564 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/UnsupportedSetupExceptionTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.exceptions; + +final class UnsupportedSetupExceptionTest + implements RSocketExceptionTest { + + @Override + public UnsupportedSetupException getException(String message) { + return new UnsupportedSetupException(message); + } + + @Override + public UnsupportedSetupException getException(String message, Throwable cause) { + return new UnsupportedSetupException(message, cause); + } + + @Override + public int getSpecifiedErrorCode() { + return 0x00000002; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java index dc774a634..246fa1184 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java @@ -1,145 +1,112 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket.fragmentation; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.mockito.Mockito.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; import io.rsocket.DuplexConnection; -import io.rsocket.Frame; -import io.rsocket.FrameType; -import io.rsocket.util.PayloadImpl; -import java.nio.ByteBuffer; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.frame.*; import java.util.concurrent.ThreadLocalRandom; -import org.junit.Test; +import org.junit.Assert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; import org.reactivestreams.Publisher; -import reactor.core.publisher.EmitterProcessor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -/** */ -public class FragmentationDuplexConnectionTest { - @Test - public void testSendOneWithFragmentation() { - DuplexConnection mockConnection = mock(DuplexConnection.class); - when(mockConnection.send(any())) - .then( - invocation -> { - Publisher frames = invocation.getArgumentAt(0, Publisher.class); - - StepVerifier.create(frames).expectNextCount(16).verifyComplete(); - - return Mono.empty(); - }); - when(mockConnection.sendOne(any(Frame.class))).thenReturn(Mono.empty()); - - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); - - Frame frame = - Frame.Request.from(1, FrameType.REQUEST_RESPONSE, new PayloadImpl(data, metadata), 1); +final class FragmentationDuplexConnectionTest { + private static byte[] data = new byte[1024]; + private static byte[] metadata = new byte[1024]; - FragmentationDuplexConnection duplexConnection = - new FragmentationDuplexConnection(mockConnection, 2); - - StepVerifier.create(duplexConnection.sendOne(frame)).verifyComplete(); + static { + ThreadLocalRandom.current().nextBytes(data); + ThreadLocalRandom.current().nextBytes(metadata); } - @Test - public void testShouldNotFragment() { - DuplexConnection mockConnection = mock(DuplexConnection.class); - when(mockConnection.sendOne(any(Frame.class))).thenReturn(Mono.empty()); - - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); + private final DuplexConnection delegate = mock(DuplexConnection.class, RETURNS_SMART_NULLS); - Frame frame = Frame.Cancel.from(1); + { + Mockito.when(delegate.onClose()).thenReturn(Mono.never()); + } - FragmentationDuplexConnection duplexConnection = - new FragmentationDuplexConnection(mockConnection, 2); + @SuppressWarnings("unchecked") + private final ArgumentCaptor> publishers = + ArgumentCaptor.forClass(Publisher.class); - StepVerifier.create(duplexConnection.sendOne(frame)).verifyComplete(); + private LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); - verify(mockConnection, times(1)).sendOne(frame); + @DisplayName("constructor throws IllegalArgumentException with negative maxFragmentLength") + @Test + void constructorInvalidMaxFragmentSize() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> + new FragmentationDuplexConnection( + delegate, Integer.MIN_VALUE, Integer.MAX_VALUE, "")) + .withMessage("The smallest allowed mtu size is 64 bytes, provided: -2147483648"); } + @DisplayName("constructor throws IllegalArgumentException with negative maxFragmentLength") @Test - public void testShouldFragmentMultiple() { - DuplexConnection mockConnection = mock(DuplexConnection.class); - when(mockConnection.send(any())) - .then( - invocation -> { - Publisher frames = invocation.getArgumentAt(0, Publisher.class); - - StepVerifier.create(frames).expectNextCount(16).verifyComplete(); - - return Mono.empty(); - }); - when(mockConnection.sendOne(any(Frame.class))).thenReturn(Mono.empty()); - - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); - - Frame frame1 = - Frame.Request.from(1, FrameType.REQUEST_RESPONSE, new PayloadImpl(data, metadata), 1); - Frame frame2 = - Frame.Request.from(2, FrameType.REQUEST_RESPONSE, new PayloadImpl(data, metadata), 1); - Frame frame3 = - Frame.Request.from(3, FrameType.REQUEST_RESPONSE, new PayloadImpl(data, metadata), 1); - - FragmentationDuplexConnection duplexConnection = - new FragmentationDuplexConnection(mockConnection, 2); - - StepVerifier.create(duplexConnection.send(Flux.just(frame1, frame2, frame3))).verifyComplete(); - - verify(mockConnection, times(3)).send(any()); + void constructorMtuLessThanMin() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new FragmentationDuplexConnection(delegate, 2, Integer.MAX_VALUE, "")) + .withMessage("The smallest allowed mtu size is 64 bytes, provided: 2"); } + @DisplayName("constructor throws NullPointerException with null delegate") @Test - public void testReassembleFragmentFrame() { - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); - Frame frame = - Frame.Request.from(1024, FrameType.REQUEST_RESPONSE, new PayloadImpl(data, metadata), 1); - FrameFragmenter frameFragmenter = new FrameFragmenter(2); - Flux fragmentedFrames = frameFragmenter.fragment(frame); - EmitterProcessor processor = EmitterProcessor.create(128); - DuplexConnection mockConnection = mock(DuplexConnection.class); - when(mockConnection.receive()).then(answer -> processor); - - FragmentationDuplexConnection duplexConnection = - new FragmentationDuplexConnection(mockConnection, 2); - - fragmentedFrames.subscribe(processor); - - duplexConnection - .receive() - .log() - .doOnNext(c -> System.out.println("here - " + c.toString())) - .subscribe(); + void constructorNullDelegate() { + assertThatNullPointerException() + .isThrownBy(() -> new FragmentationDuplexConnection(null, 64, Integer.MAX_VALUE, "")) + .withMessage("delegate must not be null"); } - private ByteBuffer createRandomBytes(int size) { - byte[] bytes = new byte[size]; - ThreadLocalRandom.current().nextBytes(bytes); - return ByteBuffer.wrap(bytes); + @DisplayName("fragments data") + @Test + void sendData() { + ByteBuf encode = + RequestResponseFrameCodec.encode( + allocator, 1, false, Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(data)); + + when(delegate.onClose()).thenReturn(Mono.never()); + when(delegate.alloc()).thenReturn(allocator); + + new FragmentationDuplexConnection(delegate, 64, Integer.MAX_VALUE, "").sendOne(encode.retain()); + + verify(delegate).send(publishers.capture()); + + StepVerifier.create(Flux.from(publishers.getValue())) + .expectNextCount(17) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertFalse(FrameHeaderCodec.hasFollows(byteBuf)); + }) + .verifyComplete(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java new file mode 100644 index 000000000..d27905f90 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java @@ -0,0 +1,56 @@ +package io.rsocket.fragmentation; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameUtil; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.util.DefaultPayload; +import java.util.concurrent.ThreadLocalRandom; +import org.junit.Assert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +public class FragmentationIntegrationTest { + private static byte[] data = new byte[128]; + private static byte[] metadata = new byte[128]; + + static { + ThreadLocalRandom.current().nextBytes(data); + ThreadLocalRandom.current().nextBytes(metadata); + } + + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + + @DisplayName("fragments and reassembles data") + @Test + void fragmentAndReassembleData() { + ByteBuf frame = + PayloadFrameCodec.encodeNextCompleteReleasingPayload( + allocator, 2, DefaultPayload.create(data)); + System.out.println(FrameUtil.toString(frame)); + + frame.retain(); + + Publisher fragments = + FrameFragmenter.fragmentFrame(allocator, 64, frame, FrameHeaderCodec.frameType(frame)); + + FrameReassembler reassembler = new FrameReassembler(allocator, Integer.MAX_VALUE); + + ByteBuf assembled = + Flux.from(fragments) + .doOnNext(byteBuf -> System.out.println(FrameUtil.toString(byteBuf))) + .handle(reassembler::reassembleFrame) + .blockLast(); + + System.out.println("assembled"); + String s = FrameUtil.toString(assembled); + System.out.println(s); + + Assert.assertEquals(FrameHeaderCodec.frameType(frame), FrameHeaderCodec.frameType(assembled)); + Assert.assertEquals(frame.readableBytes(), assembled.readableBytes()); + Assert.assertEquals(PayloadFrameCodec.data(frame), PayloadFrameCodec.data(assembled)); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java index e6d7da05e..4548e4696 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java @@ -1,85 +1,350 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket.fragmentation; -import io.rsocket.Frame; -import io.rsocket.FrameType; -import io.rsocket.util.PayloadImpl; -import java.nio.ByteBuffer; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.frame.*; import java.util.concurrent.ThreadLocalRandom; -import org.junit.Test; +import org.junit.Assert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import reactor.test.StepVerifier; -public class FrameFragmenterTest { +final class FrameFragmenterTest { + private static byte[] data = new byte[4096]; + private static byte[] metadata = new byte[4096]; + + static { + ThreadLocalRandom.current().nextBytes(data); + ThreadLocalRandom.current().nextBytes(metadata); + } + + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + + @Test + void testGettingData() { + ByteBuf rr = + RequestResponseFrameCodec.encode(allocator, 1, true, null, Unpooled.wrappedBuffer(data)); + ByteBuf fnf = + RequestFireAndForgetFrameCodec.encode( + allocator, 1, true, null, Unpooled.wrappedBuffer(data)); + ByteBuf rs = + RequestStreamFrameCodec.encode(allocator, 1, true, 1, null, Unpooled.wrappedBuffer(data)); + ByteBuf rc = + RequestChannelFrameCodec.encode( + allocator, 1, true, false, 1, null, Unpooled.wrappedBuffer(data)); + + ByteBuf data = FrameFragmenter.getData(rr, FrameType.REQUEST_RESPONSE); + Assert.assertEquals(data, Unpooled.wrappedBuffer(data)); + data.release(); + + data = FrameFragmenter.getData(fnf, FrameType.REQUEST_FNF); + Assert.assertEquals(data, Unpooled.wrappedBuffer(data)); + data.release(); + + data = FrameFragmenter.getData(rs, FrameType.REQUEST_STREAM); + Assert.assertEquals(data, Unpooled.wrappedBuffer(data)); + data.release(); + + data = FrameFragmenter.getData(rc, FrameType.REQUEST_CHANNEL); + Assert.assertEquals(data, Unpooled.wrappedBuffer(data)); + data.release(); + } + @Test - public void testFragmentWithMetadataAndData() { - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); + void testGettingMetadata() { + ByteBuf rr = + RequestResponseFrameCodec.encode( + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); + ByteBuf fnf = + RequestFireAndForgetFrameCodec.encode( + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); + ByteBuf rs = + RequestStreamFrameCodec.encode( + allocator, 1, true, 1, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); + ByteBuf rc = + RequestChannelFrameCodec.encode( + allocator, + 1, + true, + false, + 1, + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data)); + + ByteBuf data = FrameFragmenter.getMetadata(rr, FrameType.REQUEST_RESPONSE); + Assert.assertEquals(data, Unpooled.wrappedBuffer(metadata)); + data.release(); - Frame from = - Frame.Request.from(1, FrameType.REQUEST_RESPONSE, new PayloadImpl(data, metadata), 1); + data = FrameFragmenter.getMetadata(fnf, FrameType.REQUEST_FNF); + Assert.assertEquals(data, Unpooled.wrappedBuffer(metadata)); + data.release(); - FrameFragmenter frameFragmenter = new FrameFragmenter(2); + data = FrameFragmenter.getMetadata(rs, FrameType.REQUEST_STREAM); + Assert.assertEquals(data, Unpooled.wrappedBuffer(metadata)); + data.release(); - StepVerifier.create(frameFragmenter.fragment(from)).expectNextCount(16).verifyComplete(); + data = FrameFragmenter.getMetadata(rc, FrameType.REQUEST_CHANNEL); + Assert.assertEquals(data, Unpooled.wrappedBuffer(metadata)); + data.release(); } @Test - public void testFragmentWithMetadataAndDataWithOddData() { - ByteBuffer data = createRandomBytes(17); - ByteBuffer metadata = createRandomBytes(17); + void returnEmptBufferWhenNoMetadataPresent() { + ByteBuf rr = + RequestResponseFrameCodec.encode(allocator, 1, true, null, Unpooled.wrappedBuffer(data)); - Frame from = - Frame.Request.from(1, FrameType.REQUEST_RESPONSE, new PayloadImpl(data, metadata), 1); + ByteBuf data = FrameFragmenter.getMetadata(rr, FrameType.REQUEST_RESPONSE); + Assert.assertEquals(data, Unpooled.EMPTY_BUFFER); + data.release(); + } + + @DisplayName("encode first frame") + @Test + void encodeFirstFrameWithData() { + ByteBuf rr = + RequestResponseFrameCodec.encode(allocator, 1, true, null, Unpooled.wrappedBuffer(data)); - FrameFragmenter frameFragmenter = new FrameFragmenter(2); + ByteBuf fragment = + FrameFragmenter.encodeFirstFragment( + allocator, + 256, + rr, + FrameType.REQUEST_RESPONSE, + 1, + Unpooled.EMPTY_BUFFER, + Unpooled.wrappedBuffer(data)); - StepVerifier.create(frameFragmenter.fragment(from)).expectNextCount(17).verifyComplete(); + Assert.assertEquals(256, fragment.readableBytes()); + Assert.assertEquals(FrameType.REQUEST_RESPONSE, FrameHeaderCodec.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderCodec.streamId(fragment)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(fragment)); + + ByteBuf data = RequestResponseFrameCodec.data(fragment); + ByteBuf byteBuf = Unpooled.wrappedBuffer(this.data).readSlice(data.readableBytes()); + Assert.assertEquals(byteBuf, data); + + Assert.assertFalse(FrameHeaderCodec.hasMetadata(fragment)); } + @DisplayName("encode first channel frame") @Test - public void testFragmentWithMetadataOnly() { - ByteBuffer data = ByteBuffer.allocate(0); - ByteBuffer metadata = createRandomBytes(16); + void encodeFirstWithDataChannel() { + ByteBuf rc = + RequestChannelFrameCodec.encode( + allocator, 1, true, false, 10, null, Unpooled.wrappedBuffer(data)); + + ByteBuf fragment = + FrameFragmenter.encodeFirstFragment( + allocator, + 256, + rc, + FrameType.REQUEST_CHANNEL, + 1, + Unpooled.EMPTY_BUFFER, + Unpooled.wrappedBuffer(data)); - Frame from = - Frame.Request.from(1, FrameType.REQUEST_RESPONSE, new PayloadImpl(data, metadata), 1); + Assert.assertEquals(256, fragment.readableBytes()); + Assert.assertEquals(FrameType.REQUEST_CHANNEL, FrameHeaderCodec.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderCodec.streamId(fragment)); + Assert.assertEquals(10, RequestChannelFrameCodec.initialRequestN(fragment)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(fragment)); - FrameFragmenter frameFragmenter = new FrameFragmenter(2); + ByteBuf data = RequestChannelFrameCodec.data(fragment); + ByteBuf byteBuf = Unpooled.wrappedBuffer(this.data).readSlice(data.readableBytes()); + Assert.assertEquals(byteBuf, data); - StepVerifier.create(frameFragmenter.fragment(from)).expectNextCount(8).verifyComplete(); + Assert.assertFalse(FrameHeaderCodec.hasMetadata(fragment)); } + @DisplayName("encode first stream frame") @Test - public void testFragmentWithDdataOnly() { - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = ByteBuffer.allocate(0); + void encodeFirstWithDataStream() { + ByteBuf rc = + RequestStreamFrameCodec.encode(allocator, 1, true, 50, null, Unpooled.wrappedBuffer(data)); - Frame from = - Frame.Request.from(1, FrameType.REQUEST_RESPONSE, new PayloadImpl(data, metadata), 1); + ByteBuf fragment = + FrameFragmenter.encodeFirstFragment( + allocator, + 256, + rc, + FrameType.REQUEST_STREAM, + 1, + Unpooled.EMPTY_BUFFER, + Unpooled.wrappedBuffer(data)); - FrameFragmenter frameFragmenter = new FrameFragmenter(2); + Assert.assertEquals(256, fragment.readableBytes()); + Assert.assertEquals(FrameType.REQUEST_STREAM, FrameHeaderCodec.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderCodec.streamId(fragment)); + Assert.assertEquals(50, RequestStreamFrameCodec.initialRequestN(fragment)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(fragment)); - StepVerifier.create(frameFragmenter.fragment(from)).expectNextCount(8).verifyComplete(); + ByteBuf data = RequestStreamFrameCodec.data(fragment); + ByteBuf byteBuf = Unpooled.wrappedBuffer(this.data).readSlice(data.readableBytes()); + Assert.assertEquals(byteBuf, data); + + Assert.assertFalse(FrameHeaderCodec.hasMetadata(fragment)); } - private ByteBuffer createRandomBytes(int size) { - byte[] bytes = new byte[size]; - ThreadLocalRandom.current().nextBytes(bytes); - return ByteBuffer.wrap(bytes); + @DisplayName("encode first frame with only metadata") + @Test + void encodeFirstFrameWithMetadata() { + ByteBuf rr = + RequestResponseFrameCodec.encode( + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER); + + ByteBuf fragment = + FrameFragmenter.encodeFirstFragment( + allocator, + 256, + rr, + FrameType.REQUEST_RESPONSE, + 1, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER); + + Assert.assertEquals(256, fragment.readableBytes()); + Assert.assertEquals(FrameType.REQUEST_RESPONSE, FrameHeaderCodec.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderCodec.streamId(fragment)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(fragment)); + + ByteBuf data = RequestResponseFrameCodec.data(fragment); + Assert.assertEquals(data, Unpooled.EMPTY_BUFFER); + + Assert.assertTrue(FrameHeaderCodec.hasMetadata(fragment)); + } + + @DisplayName("encode first stream frame with data and metadata") + @Test + void encodeFirstWithDataAndMetadataStream() { + ByteBuf rc = + RequestStreamFrameCodec.encode( + allocator, 1, true, 50, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); + + ByteBuf fragment = + FrameFragmenter.encodeFirstFragment( + allocator, + 256, + rc, + FrameType.REQUEST_STREAM, + 1, + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data)); + + Assert.assertEquals(256, fragment.readableBytes()); + Assert.assertEquals(FrameType.REQUEST_STREAM, FrameHeaderCodec.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderCodec.streamId(fragment)); + Assert.assertEquals(50, RequestStreamFrameCodec.initialRequestN(fragment)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(fragment)); + + ByteBuf data = RequestStreamFrameCodec.data(fragment); + Assert.assertEquals(0, data.readableBytes()); + + ByteBuf metadata = RequestStreamFrameCodec.metadata(fragment); + ByteBuf byteBuf = Unpooled.wrappedBuffer(this.metadata).readSlice(metadata.readableBytes()); + Assert.assertEquals(byteBuf, metadata); + + Assert.assertTrue(FrameHeaderCodec.hasMetadata(fragment)); + } + + @DisplayName("fragments frame with only data") + @Test + void fragmentData() { + ByteBuf rr = + RequestResponseFrameCodec.encode(allocator, 1, true, null, Unpooled.wrappedBuffer(data)); + + Publisher fragments = + FrameFragmenter.fragmentFrame(allocator, 1024, rr, FrameType.REQUEST_RESPONSE); + + StepVerifier.create(Flux.from(fragments).doOnError(Throwable::printStackTrace)) + .expectNextCount(1) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertEquals(1, FrameHeaderCodec.streamId(byteBuf)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(byteBuf)); + }) + .expectNextCount(2) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertFalse(FrameHeaderCodec.hasFollows(byteBuf)); + }) + .verifyComplete(); + } + + @DisplayName("fragments frame with only metadata") + @Test + void fragmentMetadata() { + ByteBuf rr = + RequestStreamFrameCodec.encode( + allocator, 1, true, 10, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER); + + Publisher fragments = + FrameFragmenter.fragmentFrame(allocator, 1024, rr, FrameType.REQUEST_STREAM); + + StepVerifier.create(Flux.from(fragments).doOnError(Throwable::printStackTrace)) + .expectNextCount(1) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertEquals(1, FrameHeaderCodec.streamId(byteBuf)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(byteBuf)); + }) + .expectNextCount(2) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertFalse(FrameHeaderCodec.hasFollows(byteBuf)); + }) + .verifyComplete(); + } + + @DisplayName("fragments frame with data and metadata") + @Test + void fragmentDataAndMetadata() { + ByteBuf rr = + RequestResponseFrameCodec.encode( + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); + + Publisher fragments = + FrameFragmenter.fragmentFrame(allocator, 1024, rr, FrameType.REQUEST_RESPONSE); + + StepVerifier.create(Flux.from(fragments).doOnError(Throwable::printStackTrace)) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.REQUEST_RESPONSE, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(byteBuf)); + }) + .expectNextCount(6) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(byteBuf)); + }) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertFalse(FrameHeaderCodec.hasFollows(byteBuf)); + }) + .verifyComplete(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java index 278c7fece..deabd5c9c 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java @@ -1,140 +1,544 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket.fragmentation; -import io.rsocket.Frame; -import io.rsocket.FrameType; -import io.rsocket.util.PayloadImpl; -import java.nio.ByteBuffer; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCountUtil; +import io.rsocket.frame.*; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.ThreadLocalRandom; -import org.junit.Test; +import org.assertj.core.api.Assertions; +import org.junit.Assert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +final class FrameReassemblerTest { + private static byte[] data = new byte[1024]; + private static byte[] metadata = new byte[1024]; + + static { + ThreadLocalRandom.current().nextBytes(data); + ThreadLocalRandom.current().nextBytes(metadata); + } + + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; -/** */ -public class FrameReassemblerTest { + @DisplayName("reassembles data") @Test - public void testAppend() { - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); - - Frame from = - Frame.Request.from(1024, FrameType.REQUEST_RESPONSE, new PayloadImpl(data, metadata), 1); - FrameFragmenter frameFragmenter = new FrameFragmenter(2); - FrameReassembler reassembler = new FrameReassembler(from); - frameFragmenter.fragment(from).subscribe(reassembler::append); + void reassembleData() { + List byteBufs = + Arrays.asList( + RequestResponseFrameCodec.encode( + allocator, 1, true, null, Unpooled.wrappedBuffer(data)), + PayloadFrameCodec.encode( + allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), + PayloadFrameCodec.encode( + allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), + PayloadFrameCodec.encode( + allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), + PayloadFrameCodec.encode( + allocator, 1, false, false, true, null, Unpooled.wrappedBuffer(data))); + + FrameReassembler reassembler = new FrameReassembler(allocator, Integer.MAX_VALUE); + + Flux assembled = Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame); + + CompositeByteBuf data = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FrameReassemblerTest.data), + Unpooled.wrappedBuffer(FrameReassemblerTest.data), + Unpooled.wrappedBuffer(FrameReassemblerTest.data), + Unpooled.wrappedBuffer(FrameReassemblerTest.data), + Unpooled.wrappedBuffer(FrameReassemblerTest.data)); + + StepVerifier.create(assembled) + .assertNext( + byteBuf -> { + Assert.assertEquals(data, RequestResponseFrameCodec.data(byteBuf)); + ReferenceCountUtil.safeRelease(byteBuf); + }) + .verifyComplete(); + ReferenceCountUtil.safeRelease(data); } - private ByteBuffer createRandomBytes(int size) { - byte[] bytes = new byte[size]; - ThreadLocalRandom.current().nextBytes(bytes); - return ByteBuffer.wrap(bytes); + @DisplayName("pass through frames without follows") + @Test + void passthrough() { + List byteBufs = + Arrays.asList( + RequestResponseFrameCodec.encode( + allocator, 1, false, null, Unpooled.wrappedBuffer(data))); + + FrameReassembler reassembler = new FrameReassembler(allocator, Integer.MAX_VALUE); + + Flux assembled = Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame); + + CompositeByteBuf data = + allocator + .compositeDirectBuffer() + .addComponents(true, Unpooled.wrappedBuffer(FrameReassemblerTest.data)); + + StepVerifier.create(assembled) + .assertNext( + byteBuf -> { + Assert.assertEquals(data, RequestResponseFrameCodec.data(byteBuf)); + ReferenceCountUtil.safeRelease(byteBuf); + }) + .verifyComplete(); + ReferenceCountUtil.safeRelease(data); } - /* - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); - - Frame from = Frame.Request.from(1024, FrameType.REQUEST_RESPONSE, new PayloadImpl(data, metadata), 1); - - FrameFragmenter frameFragmenter = new FrameFragmenter(2); - - FrameReassembler reassembler = new FrameReassembler(2); - - frameFragmenter - .fragment(from) - .log() - .doOnNext(reassembler::append) - .blockLast(); - - Frame reassemble = reassembler.reassemble(); - Assert.assertEquals(reassemble.getStreamId(), from.getStreamId()); - Assert.assertEquals(reassemble.getType(), from.getType()); - - ByteBuffer reassembleData = reassemble.getData(); - ByteBuffer reassembleMetadata = reassemble.getMetadata(); - - Assert.assertTrue(reassembleData.hasRemaining()); - Assert.assertTrue(reassembleMetadata.hasRemaining()); - - while (reassembleData.hasRemaining()) { - Assert.assertEquals(reassembleData.get(), data.get()); - } - - while (reassembleMetadata.hasRemaining()) { - Assert.assertEquals(reassembleMetadata.get(), metadata.get()); - } + @DisplayName("reassembles metadata") + @Test + void reassembleMetadata() { + List byteBufs = + Arrays.asList( + RequestResponseFrameCodec.encode( + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + false, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER)); + + FrameReassembler reassembler = new FrameReassembler(allocator, Integer.MAX_VALUE); + + Flux assembled = Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame); + + CompositeByteBuf metadata = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata)); + + StepVerifier.create(assembled) + .assertNext( + byteBuf -> { + System.out.println(byteBuf.readableBytes()); + ByteBuf m = RequestResponseFrameCodec.metadata(byteBuf); + Assert.assertEquals(metadata, m); + }) + .verifyComplete(); } + @DisplayName("reassembles metadata request channel") @Test - public void testReassmembleAndClear() { - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); - - Frame request = Frame.Request.from(1024, FrameType.REQUEST_RESPONSE, new PayloadImpl(data, metadata), 1); - - FrameFragmenter frameFragmenter = new FrameFragmenter(2); - - FrameReassembler reassembler = new FrameReassembler(2); - - Iterable fragments = frameFragmenter - .fragment(request) - .log() - .map(frame -> frame.content().copy()) - .toIterable(); - - fragments - .forEach(f -> ByteBufUtil.prettyHexDump(f)); - - - for (int i = 0; i < 5; i++) { - for (ByteBuf frame : fragments) { - reassembler - .append(Frame.from(frame)); - } - - Frame reassemble = reassembler.reassemble(); - - Assert.assertEquals(reassemble.getStreamId(), request.getStreamId()); - Assert.assertEquals(reassemble.getType(), reassemble.getType()); - - ByteBuffer reassembleData = reassemble.getData(); - ByteBuffer reassembleMetadata = reassemble.getMetadata(); + void reassembleMetadataChannel() { + List byteBufs = + Arrays.asList( + RequestChannelFrameCodec.encode( + allocator, + 1, + true, + false, + 100, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + false, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER)); + + FrameReassembler reassembler = new FrameReassembler(allocator, Integer.MAX_VALUE); + + Flux assembled = Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame); + + CompositeByteBuf metadata = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata)); + + StepVerifier.create(assembled) + .assertNext( + byteBuf -> { + System.out.println(byteBuf.readableBytes()); + ByteBuf m = RequestChannelFrameCodec.metadata(byteBuf); + Assert.assertEquals(metadata, m); + Assert.assertEquals(100, RequestChannelFrameCodec.initialRequestN(byteBuf)); + ReferenceCountUtil.safeRelease(byteBuf); + }) + .verifyComplete(); + + ReferenceCountUtil.safeRelease(metadata); + } - Assert.assertTrue(reassembleData.hasRemaining()); - Assert.assertTrue(reassembleMetadata.hasRemaining()); + @DisplayName("reassembles metadata request stream") + @Test + void reassembleMetadataStream() { + List byteBufs = + Arrays.asList( + RequestStreamFrameCodec.encode( + allocator, 1, true, 250, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + false, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER)); + + FrameReassembler reassembler = new FrameReassembler(allocator, Integer.MAX_VALUE); + + Flux assembled = Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame); + + CompositeByteBuf metadata = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata)); + + StepVerifier.create(assembled) + .assertNext( + byteBuf -> { + System.out.println(byteBuf.readableBytes()); + ByteBuf m = RequestStreamFrameCodec.metadata(byteBuf); + Assert.assertEquals(metadata, m); + Assert.assertEquals(250, RequestChannelFrameCodec.initialRequestN(byteBuf)); + ReferenceCountUtil.safeRelease(byteBuf); + }) + .verifyComplete(); + + ReferenceCountUtil.safeRelease(metadata); + } - while (reassembleData.hasRemaining()) { - Assert.assertEquals(reassembleData.get(), data.get()); - } + @DisplayName("reassembles metadata and data") + @Test + void reassembleMetadataAndData() { + + List byteBufs = + Arrays.asList( + RequestResponseFrameCodec.encode( + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data)), + PayloadFrameCodec.encode( + allocator, 1, false, false, true, null, Unpooled.wrappedBuffer(data))); + + FrameReassembler reassembler = new FrameReassembler(allocator, Integer.MAX_VALUE); + + Flux assembled = Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame); + + CompositeByteBuf data = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FrameReassemblerTest.data), + Unpooled.wrappedBuffer(FrameReassemblerTest.data)); + + CompositeByteBuf metadata = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata)); + + StepVerifier.create(assembled) + .assertNext( + byteBuf -> { + Assert.assertEquals(data, RequestResponseFrameCodec.data(byteBuf)); + Assert.assertEquals(metadata, RequestResponseFrameCodec.metadata(byteBuf)); + }) + .verifyComplete(); + ReferenceCountUtil.safeRelease(data); + ReferenceCountUtil.safeRelease(metadata); + } - while (reassembleMetadata.hasRemaining()) { - Assert.assertEquals(reassembleMetadata.get(), metadata.get()); - } + @DisplayName("cancel removes inflight frames") + @Test + public void cancelBeforeAssembling() { + List byteBufs = + Arrays.asList( + RequestResponseFrameCodec.encode( + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data))); + + FrameReassembler reassembler = new FrameReassembler(allocator, Integer.MAX_VALUE); + Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame).blockLast(); + + Assert.assertTrue(reassembler.headers.containsKey(1)); + Assert.assertTrue(reassembler.metadata.containsKey(1)); + Assert.assertTrue(reassembler.data.containsKey(1)); + + Flux.just(CancelFrameCodec.encode(allocator, 1)) + .handle(reassembler::reassembleFrame) + .blockLast(); + + Assert.assertFalse(reassembler.headers.containsKey(1)); + Assert.assertFalse(reassembler.metadata.containsKey(1)); + Assert.assertFalse(reassembler.data.containsKey(1)); + } - } + @ParameterizedTest(name = "throws error if reassembling payload size exceeds {0}") + @ValueSource(ints = {64, 1024, 2048, 4096}) + public void errorTooBigPayload(int maxFrameLength) { + List byteBufs = + Arrays.asList( + RequestResponseFrameCodec.encode( + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data))); + + FrameReassembler reassembler = new FrameReassembler(allocator, maxFrameLength); + + Assertions.assertThatThrownBy( + Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame)::blockLast) + .hasMessage("Reassembled payload went out of allowed size") + .isExactlyInstanceOf(IllegalStateException.class); } + @DisplayName("throws error on empty fragment") @Test - public void substring() { - String s = "1234567890"; - String substring = s.substring(0, 5); - System.out.println(substring); - String substring1 = s.substring(5, 10); - System.out.println(substring1); + public void errorEmptyFrame() { + List byteBufs = + Arrays.asList( + RequestResponseFrameCodec.encode( + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, 1, true, false, true, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER)); + + FrameReassembler reassembler = new FrameReassembler(allocator, Integer.MAX_VALUE); + + Assertions.assertThatThrownBy( + Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame)::blockLast) + .hasMessage("Empty frame.") + .isExactlyInstanceOf(IllegalStateException.class); } - */ + @DisplayName("dispose should clean up maps") + @Test + public void dispose() { + List byteBufs = + Arrays.asList( + RequestResponseFrameCodec.encode( + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data))); + + FrameReassembler reassembler = new FrameReassembler(allocator, Integer.MAX_VALUE); + Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame).blockLast(); + + Assert.assertTrue(reassembler.headers.containsKey(1)); + Assert.assertTrue(reassembler.metadata.containsKey(1)); + Assert.assertTrue(reassembler.data.containsKey(1)); + + reassembler.dispose(); + + Assert.assertFalse(reassembler.headers.containsKey(1)); + Assert.assertFalse(reassembler.metadata.containsKey(1)); + Assert.assertFalse(reassembler.data.containsKey(1)); + } } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java new file mode 100644 index 000000000..da95c3942 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java @@ -0,0 +1,367 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.fragmentation; + +import static org.mockito.Mockito.RETURNS_SMART_NULLS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; +import io.rsocket.DuplexConnection; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.frame.CancelFrameCodec; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import org.assertj.core.api.Assertions; +import org.junit.Assert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.test.StepVerifier; + +final class ReassembleDuplexConnectionTest { + private static byte[] data = new byte[1024]; + private static byte[] metadata = new byte[1024]; + + static { + ThreadLocalRandom.current().nextBytes(data); + ThreadLocalRandom.current().nextBytes(metadata); + } + + private final DuplexConnection delegate = mock(DuplexConnection.class, RETURNS_SMART_NULLS); + + private LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + + @DisplayName("reassembles data") + @Test + void reassembleData() { + List byteBufs = + Arrays.asList( + RequestResponseFrameCodec.encode( + allocator, 1, true, null, Unpooled.wrappedBuffer(data)), + PayloadFrameCodec.encode( + allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), + PayloadFrameCodec.encode( + allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), + PayloadFrameCodec.encode( + allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), + PayloadFrameCodec.encode( + allocator, 1, false, false, true, null, Unpooled.wrappedBuffer(data))); + + CompositeByteBuf data = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.data), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.data), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.data), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.data), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.data)); + + when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); + when(delegate.onClose()).thenReturn(Mono.never()); + when(delegate.alloc()).thenReturn(allocator); + + new ReassemblyDuplexConnection(delegate, Integer.MAX_VALUE) + .receive() + .as(StepVerifier::create) + .assertNext( + byteBuf -> { + Assert.assertEquals(data, RequestResponseFrameCodec.data(byteBuf)); + }) + .verifyComplete(); + } + + @DisplayName("reassembles metadata") + @Test + void reassembleMetadata() { + List byteBufs = + Arrays.asList( + RequestResponseFrameCodec.encode( + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + false, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER)); + + CompositeByteBuf metadata = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata)); + + when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); + when(delegate.onClose()).thenReturn(Mono.never()); + when(delegate.alloc()).thenReturn(allocator); + + new ReassemblyDuplexConnection(delegate, Integer.MAX_VALUE) + .receive() + .as(StepVerifier::create) + .assertNext( + byteBuf -> { + System.out.println(byteBuf.readableBytes()); + ByteBuf m = RequestResponseFrameCodec.metadata(byteBuf); + Assert.assertEquals(metadata, m); + }) + .verifyComplete(); + } + + @DisplayName("reassembles metadata and data") + @Test + void reassembleMetadataAndData() { + List byteBufs = + Arrays.asList( + RequestResponseFrameCodec.encode( + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data)), + PayloadFrameCodec.encode( + allocator, 1, false, false, true, null, Unpooled.wrappedBuffer(data))); + + CompositeByteBuf data = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.data), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.data)); + + CompositeByteBuf metadata = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata)); + + when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); + when(delegate.onClose()).thenReturn(Mono.never()); + when(delegate.alloc()).thenReturn(allocator); + + new ReassemblyDuplexConnection(delegate, Integer.MAX_VALUE) + .receive() + .as(StepVerifier::create) + .assertNext( + byteBuf -> { + Assert.assertEquals(data, RequestResponseFrameCodec.data(byteBuf)); + Assert.assertEquals(metadata, RequestResponseFrameCodec.metadata(byteBuf)); + }) + .verifyComplete(); + } + + @DisplayName("does not reassemble a non-fragment frame") + @Test + void reassembleNonFragment() { + ByteBuf encode = + RequestResponseFrameCodec.encode(allocator, 1, false, null, Unpooled.wrappedBuffer(data)); + + when(delegate.receive()).thenReturn(Flux.just(encode)); + when(delegate.onClose()).thenReturn(Mono.never()); + when(delegate.alloc()).thenReturn(allocator); + + new ReassemblyDuplexConnection(delegate, Integer.MAX_VALUE) + .receive() + .as(StepVerifier::create) + .assertNext( + byteBuf -> { + Assert.assertEquals( + Unpooled.wrappedBuffer(data), RequestResponseFrameCodec.data(byteBuf)); + }) + .verifyComplete(); + } + + @DisplayName("does not reassemble non fragmentable frame") + @Test + void reassembleNonFragmentableFrame() { + ByteBuf encode = CancelFrameCodec.encode(allocator, 2); + + when(delegate.receive()).thenReturn(Flux.just(encode)); + when(delegate.onClose()).thenReturn(Mono.never()); + when(delegate.alloc()).thenReturn(allocator); + + new ReassemblyDuplexConnection(delegate, Integer.MAX_VALUE) + .receive() + .as(StepVerifier::create) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.CANCEL, FrameHeaderCodec.frameType(byteBuf)); + }) + .verifyComplete(); + } + + @ParameterizedTest(name = "throws error if reassembling payload size exceeds {0}") + @ValueSource(ints = {64, 1024, 2048, 4096}) + public void errorTooBigPayload(int maxFrameLength) { + List byteBufs = + Arrays.asList( + RequestResponseFrameCodec.encode( + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, + 1, + true, + false, + true, + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data))); + + MonoProcessor onClose = MonoProcessor.create(); + + when(delegate.receive()) + .thenReturn( + Flux.fromIterable(byteBufs) + .doOnDiscard(ReferenceCounted.class, ReferenceCountUtil::release)); + when(delegate.onClose()).thenReturn(onClose); + when(delegate.alloc()).thenReturn(allocator); + + new ReassemblyDuplexConnection(delegate, maxFrameLength) + .receive() + .doFinally(__ -> onClose.onComplete()) + .as(StepVerifier::create) + .expectErrorSatisfies( + t -> + Assertions.assertThat(t) + .hasMessage("Reassembled payload went out of allowed size") + .isExactlyInstanceOf(IllegalStateException.class)) + .verify(Duration.ofSeconds(1)); + + allocator.assertHasNoLeaks(); + } + + @DisplayName("throws error on empty fragment") + @Test + public void errorEmptyFrame() { + List byteBufs = + Arrays.asList( + RequestResponseFrameCodec.encode( + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), + PayloadFrameCodec.encode( + allocator, 1, true, false, true, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER)); + + MonoProcessor onClose = MonoProcessor.create(); + + when(delegate.receive()) + .thenReturn( + Flux.fromIterable(byteBufs) + .doOnDiscard(ReferenceCounted.class, ReferenceCountUtil::release)); + when(delegate.onClose()).thenReturn(onClose); + when(delegate.alloc()).thenReturn(allocator); + + new ReassemblyDuplexConnection(delegate, Integer.MAX_VALUE) + .receive() + .doFinally(__ -> onClose.onComplete()) + .as(StepVerifier::create) + .expectErrorSatisfies( + t -> + Assertions.assertThat(t) + .hasMessage("Empty frame.") + .isExactlyInstanceOf(IllegalStateException.class)) + .verify(Duration.ofSeconds(1)); + + allocator.assertHasNoLeaks(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java b/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java new file mode 100644 index 000000000..63300c718 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.util.IllegalReferenceCountException; +import org.assertj.core.presentation.StandardRepresentation; + +public final class ByteBufRepresentation extends StandardRepresentation { + + @Override + protected String fallbackToStringOf(Object object) { + if (object instanceof ByteBuf) { + try { + String normalBufferString = object.toString(); + String prettyHexDump = ByteBufUtil.prettyHexDump((ByteBuf) object); + return new StringBuilder() + .append(normalBufferString) + .append("\n") + .append(prettyHexDump) + .toString(); + } catch (IllegalReferenceCountException e) { + // noops + } + } + + return super.fallbackToStringOf(object); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ErrorFrameCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ErrorFrameCodecTest.java new file mode 100644 index 000000000..dc04c1141 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/ErrorFrameCodecTest.java @@ -0,0 +1,21 @@ +package io.rsocket.frame; + +import static org.junit.jupiter.api.Assertions.*; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.rsocket.exceptions.ApplicationErrorException; +import org.junit.jupiter.api.Test; + +class ErrorFrameCodecTest { + @Test + void testEncode() { + ByteBuf frame = + ErrorFrameCodec.encode(ByteBufAllocator.DEFAULT, 1, new ApplicationErrorException("d")); + + frame = FrameLengthCodec.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); + assertEquals("00000b000000012c000000020164", ByteBufUtil.hexDump(frame)); + frame.release(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ErrorFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ErrorFrameFlyweightTest.java deleted file mode 100644 index 94cafe06c..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/ErrorFrameFlyweightTest.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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 static org.junit.Assert.assertEquals; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import java.nio.charset.StandardCharsets; -import org.junit.Test; - -public class ErrorFrameFlyweightTest { - private final ByteBuf byteBuf = Unpooled.buffer(1024); - - @Test - public void testEncoding() { - int encoded = - ErrorFrameFlyweight.encode( - byteBuf, - 1, - ErrorFrameFlyweight.APPLICATION_ERROR, - Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), - Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - assertEquals( - "000010000000012d00000002010000026d6464", ByteBufUtil.hexDump(byteBuf, 0, encoded)); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameCodecTest.java new file mode 100644 index 000000000..28209393e --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameCodecTest.java @@ -0,0 +1,62 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ExtensionFrameCodecTest { + + @Test + void extensionDataMetadata() { + ByteBuf metadata = bytebuf("md"); + ByteBuf data = bytebuf("d"); + int extendedType = 1; + + ByteBuf extension = + ExtensionFrameCodec.encode(ByteBufAllocator.DEFAULT, 1, extendedType, metadata, data); + + Assertions.assertTrue(FrameHeaderCodec.hasMetadata(extension)); + Assertions.assertEquals(extendedType, ExtensionFrameCodec.extendedType(extension)); + Assertions.assertEquals(metadata, ExtensionFrameCodec.metadata(extension)); + Assertions.assertEquals(data, ExtensionFrameCodec.data(extension)); + extension.release(); + } + + @Test + void extensionData() { + ByteBuf data = bytebuf("d"); + int extendedType = 1; + + ByteBuf extension = + ExtensionFrameCodec.encode(ByteBufAllocator.DEFAULT, 1, extendedType, null, data); + + Assertions.assertFalse(FrameHeaderCodec.hasMetadata(extension)); + Assertions.assertEquals(extendedType, ExtensionFrameCodec.extendedType(extension)); + Assertions.assertNull(ExtensionFrameCodec.metadata(extension)); + Assertions.assertEquals(data, ExtensionFrameCodec.data(extension)); + extension.release(); + } + + @Test + void extensionMetadata() { + ByteBuf metadata = bytebuf("md"); + int extendedType = 1; + + ByteBuf extension = + ExtensionFrameCodec.encode( + ByteBufAllocator.DEFAULT, 1, extendedType, metadata, Unpooled.EMPTY_BUFFER); + + Assertions.assertTrue(FrameHeaderCodec.hasMetadata(extension)); + Assertions.assertEquals(extendedType, ExtensionFrameCodec.extendedType(extension)); + Assertions.assertEquals(metadata, ExtensionFrameCodec.metadata(extension)); + Assertions.assertEquals(0, ExtensionFrameCodec.data(extension).readableBytes()); + extension.release(); + } + + private static ByteBuf bytebuf(String str) { + return Unpooled.copiedBuffer(str, StandardCharsets.UTF_8); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderCodecTest.java new file mode 100644 index 000000000..15788e631 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderCodecTest.java @@ -0,0 +1,36 @@ +package io.rsocket.frame; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import org.junit.jupiter.api.Test; + +class FrameHeaderCodecTest { + // Taken from spec + private static final int FRAME_MAX_SIZE = 16_777_215; + + @Test + void typeAndFlag() { + FrameType frameType = FrameType.REQUEST_FNF; + int flags = 0b1110110111; + ByteBuf header = FrameHeaderCodec.encode(ByteBufAllocator.DEFAULT, 0, frameType, flags); + + assertEquals(flags, FrameHeaderCodec.flags(header)); + assertEquals(frameType, FrameHeaderCodec.frameType(header)); + header.release(); + } + + @Test + void typeAndFlagTruncated() { + FrameType frameType = FrameType.SETUP; + int flags = 0b11110110111; // 1 bit too many + ByteBuf header = FrameHeaderCodec.encode(ByteBufAllocator.DEFAULT, 0, frameType, flags); + + assertNotEquals(flags, FrameHeaderCodec.flags(header)); + assertEquals(flags & 0b0000_0011_1111_1111, FrameHeaderCodec.flags(header)); + assertEquals(frameType, FrameHeaderCodec.frameType(header)); + header.release(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderFlyweightTest.java deleted file mode 100644 index 4c5f2bcf5..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderFlyweightTest.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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 static io.rsocket.frame.FrameHeaderFlyweight.FRAME_HEADER_LENGTH; -import static org.junit.Assert.*; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import io.rsocket.FrameType; -import org.junit.Test; - -public class FrameHeaderFlyweightTest { - // Taken from spec - private static final int FRAME_MAX_SIZE = 16_777_215; - - private final ByteBuf byteBuf = Unpooled.buffer(1024); - - @Test - public void headerSize() { - int frameLength = 123456; - FrameHeaderFlyweight.encodeFrameHeader(byteBuf, frameLength, 0, FrameType.SETUP, 0); - assertEquals(frameLength, FrameHeaderFlyweight.frameLength(byteBuf)); - } - - @Test - public void headerSizeMax() { - int frameLength = FRAME_MAX_SIZE; - FrameHeaderFlyweight.encodeFrameHeader(byteBuf, frameLength, 0, FrameType.SETUP, 0); - assertEquals(frameLength, FrameHeaderFlyweight.frameLength(byteBuf)); - } - - @Test(expected = IllegalArgumentException.class) - public void headerSizeTooLarge() { - FrameHeaderFlyweight.encodeFrameHeader(byteBuf, FRAME_MAX_SIZE + 1, 0, FrameType.SETUP, 0); - } - - @Test - public void frameLength() { - int length = - FrameHeaderFlyweight.encode( - byteBuf, 0, 0, FrameType.SETUP, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); - assertEquals(length, 9); // 72 bits - } - - @Test - public void metadataLength() { - ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); - FrameHeaderFlyweight.encode(byteBuf, 0, 0, FrameType.SETUP, metadata, Unpooled.EMPTY_BUFFER); - assertEquals( - 4, - FrameHeaderFlyweight.decodeMetadataLength( - byteBuf, FrameHeaderFlyweight.FRAME_HEADER_LENGTH)); - } - - @Test - public void dataLength() { - ByteBuf data = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4, 5}); - int length = - FrameHeaderFlyweight.encode(byteBuf, 0, 0, FrameType.SETUP, Unpooled.EMPTY_BUFFER, data); - assertEquals( - 5, - FrameHeaderFlyweight.dataLength( - byteBuf, FrameType.SETUP, FrameHeaderFlyweight.FRAME_HEADER_LENGTH)); - } - - @Test - public void metadataSlice() { - ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); - FrameHeaderFlyweight.encode( - byteBuf, 0, 0, FrameType.REQUEST_RESPONSE, metadata, Unpooled.EMPTY_BUFFER); - metadata.resetReaderIndex(); - - assertEquals(metadata, FrameHeaderFlyweight.sliceFrameMetadata(byteBuf)); - } - - @Test - public void dataSlice() { - ByteBuf data = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4, 5}); - FrameHeaderFlyweight.encode( - byteBuf, 0, 0, FrameType.REQUEST_RESPONSE, Unpooled.EMPTY_BUFFER, data); - data.resetReaderIndex(); - - assertEquals(data, FrameHeaderFlyweight.sliceFrameData(byteBuf)); - } - - @Test - public void streamId() { - int streamId = 1234; - FrameHeaderFlyweight.encode( - byteBuf, streamId, 0, FrameType.SETUP, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); - assertEquals(streamId, FrameHeaderFlyweight.streamId(byteBuf)); - } - - @Test - public void typeAndFlag() { - FrameType frameType = FrameType.FIRE_AND_FORGET; - int flags = 0b1110110111; - FrameHeaderFlyweight.encode( - byteBuf, 0, flags, frameType, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); - - assertEquals(flags, FrameHeaderFlyweight.flags(byteBuf)); - assertEquals(frameType, FrameHeaderFlyweight.frameType(byteBuf)); - } - - @Test - public void typeAndFlagTruncated() { - FrameType frameType = FrameType.SETUP; - int flags = 0b11110110111; // 1 bit too many - FrameHeaderFlyweight.encode( - byteBuf, 0, flags, FrameType.SETUP, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); - - assertNotEquals(flags, FrameHeaderFlyweight.flags(byteBuf)); - assertEquals(flags & 0b0000_0011_1111_1111, FrameHeaderFlyweight.flags(byteBuf)); - assertEquals(frameType, FrameHeaderFlyweight.frameType(byteBuf)); - } - - @Test - public void missingMetadataLength() { - for (FrameType frameType : FrameType.values()) { - switch (frameType) { - case UNDEFINED: - break; - case CANCEL: - case METADATA_PUSH: - case LEASE: - assertFalse( - "!hasMetadataLengthField(): " + frameType, - FrameHeaderFlyweight.hasMetadataLengthField(frameType)); - break; - default: - if (frameType.canHaveMetadata()) { - assertTrue( - "hasMetadataLengthField(): " + frameType, - FrameHeaderFlyweight.hasMetadataLengthField(frameType)); - } - } - } - } - - @Test - public void wireFormat() { - ByteBuf expectedBuffer = Unpooled.buffer(1024); - int currentIndex = 0; - // frame length - int frameLength = - FrameHeaderFlyweight.FRAME_HEADER_LENGTH - FrameHeaderFlyweight.FRAME_LENGTH_SIZE; - expectedBuffer.setInt(currentIndex, frameLength << 8); - currentIndex += 3; - // stream id - expectedBuffer.setInt(currentIndex, 5); - currentIndex += Integer.BYTES; - // flags and frame type - expectedBuffer.setShort(currentIndex, (short) 0b001010_0001100000); - currentIndex += Short.BYTES; - - FrameType frameType = FrameType.NEXT_COMPLETE; - FrameHeaderFlyweight.encode( - byteBuf, 5, 0, frameType, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); - - ByteBuf expected = expectedBuffer.slice(0, currentIndex); - ByteBuf actual = byteBuf.slice(0, FRAME_HEADER_LENGTH); - - assertEquals(ByteBufUtil.hexDump(expected), ByteBufUtil.hexDump(actual)); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/GenericFrameCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/GenericFrameCodecTest.java new file mode 100644 index 000000000..ac19dc754 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/GenericFrameCodecTest.java @@ -0,0 +1,264 @@ +package io.rsocket.frame; + +import static org.junit.jupiter.api.Assertions.*; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class GenericFrameCodecTest { + @Test + void testEncoding() { + ByteBuf frame = + RequestStreamFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + 1, + Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), + Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); + + frame = FrameLengthCodec.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); + // Encoded FrameLength⌍ ⌌ Encoded Headers + // | | ⌌ Encoded Request(1) + // | | | ⌌Encoded Metadata Length + // | | | | ⌌Encoded Metadata + // | | | | | ⌌Encoded Data + // __|________|_________|______|____|___| + // ↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓↓ + String expected = "000010000000011900000000010000026d6464"; + assertEquals(expected, ByteBufUtil.hexDump(frame)); + frame.release(); + } + + @Test + void testEncodingWithEmptyMetadata() { + ByteBuf frame = + RequestStreamFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + 1, + Unpooled.EMPTY_BUFFER, + Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); + + frame = FrameLengthCodec.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); + // Encoded FrameLength⌍ ⌌ Encoded Headers + // | | ⌌ Encoded Request(1) + // | | | ⌌Encoded Metadata Length (0) + // | | | | ⌌Encoded Data + // __|________|_________|_______|___| + // ↓ ↓↓ ↓↓ ↓↓ ↓↓↓ + String expected = "00000e0000000119000000000100000064"; + assertEquals(expected, ByteBufUtil.hexDump(frame)); + frame.release(); + } + + @Test + void testEncodingWithNullMetadata() { + ByteBuf frame = + RequestStreamFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + 1, + null, + Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); + + frame = FrameLengthCodec.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); + + // Encoded FrameLength⌍ ⌌ Encoded Headers + // | | ⌌ Encoded Request(1) + // | | | ⌌Encoded Data + // __|________|_________|_____| + // ↓<-> ↓↓ <-> ↓↓ <-> ↓↓↓ + String expected = "00000b0000000118000000000164"; + assertEquals(expected, ByteBufUtil.hexDump(frame)); + frame.release(); + } + + @Test + void requestResponseDataMetadata() { + ByteBuf request = + RequestResponseFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), + Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); + + String data = RequestResponseFrameCodec.data(request).toString(StandardCharsets.UTF_8); + String metadata = RequestResponseFrameCodec.metadata(request).toString(StandardCharsets.UTF_8); + + assertTrue(FrameHeaderCodec.hasMetadata(request)); + assertEquals("d", data); + assertEquals("md", metadata); + request.release(); + } + + @Test + void requestResponseData() { + ByteBuf request = + RequestResponseFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + null, + Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); + + String data = RequestResponseFrameCodec.data(request).toString(StandardCharsets.UTF_8); + ByteBuf metadata = RequestResponseFrameCodec.metadata(request); + + assertFalse(FrameHeaderCodec.hasMetadata(request)); + assertEquals("d", data); + assertNull(metadata); + request.release(); + } + + @Test + void requestResponseMetadata() { + ByteBuf request = + RequestResponseFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), + Unpooled.EMPTY_BUFFER); + + ByteBuf data = RequestResponseFrameCodec.data(request); + String metadata = RequestResponseFrameCodec.metadata(request).toString(StandardCharsets.UTF_8); + + assertTrue(FrameHeaderCodec.hasMetadata(request)); + assertTrue(data.readableBytes() == 0); + assertEquals("md", metadata); + request.release(); + } + + @Test + void requestStreamDataMetadata() { + ByteBuf request = + RequestStreamFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + Integer.MAX_VALUE + 1L, + Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), + Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); + + long actualRequest = RequestStreamFrameCodec.initialRequestN(request); + String data = RequestStreamFrameCodec.data(request).toString(StandardCharsets.UTF_8); + String metadata = RequestStreamFrameCodec.metadata(request).toString(StandardCharsets.UTF_8); + + assertTrue(FrameHeaderCodec.hasMetadata(request)); + assertEquals(Long.MAX_VALUE, actualRequest); + assertEquals("md", metadata); + assertEquals("d", data); + request.release(); + } + + @Test + void requestStreamData() { + ByteBuf request = + RequestStreamFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + 42, + null, + Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); + + long actualRequest = RequestStreamFrameCodec.initialRequestN(request); + String data = RequestStreamFrameCodec.data(request).toString(StandardCharsets.UTF_8); + ByteBuf metadata = RequestStreamFrameCodec.metadata(request); + + assertFalse(FrameHeaderCodec.hasMetadata(request)); + assertEquals(42L, actualRequest); + assertNull(metadata); + assertEquals("d", data); + request.release(); + } + + @Test + void requestStreamMetadata() { + ByteBuf request = + RequestStreamFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + 42, + Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), + Unpooled.EMPTY_BUFFER); + + long actualRequest = RequestStreamFrameCodec.initialRequestN(request); + ByteBuf data = RequestStreamFrameCodec.data(request); + String metadata = RequestStreamFrameCodec.metadata(request).toString(StandardCharsets.UTF_8); + + assertTrue(FrameHeaderCodec.hasMetadata(request)); + assertEquals(42L, actualRequest); + assertTrue(data.readableBytes() == 0); + assertEquals("md", metadata); + request.release(); + } + + @Test + void requestFnfDataAndMetadata() { + ByteBuf request = + RequestFireAndForgetFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), + Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); + + String data = RequestFireAndForgetFrameCodec.data(request).toString(StandardCharsets.UTF_8); + String metadata = + RequestFireAndForgetFrameCodec.metadata(request).toString(StandardCharsets.UTF_8); + + assertTrue(FrameHeaderCodec.hasMetadata(request)); + assertEquals("d", data); + assertEquals("md", metadata); + request.release(); + } + + @Test + void requestFnfData() { + ByteBuf request = + RequestFireAndForgetFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + null, + Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); + + String data = RequestFireAndForgetFrameCodec.data(request).toString(StandardCharsets.UTF_8); + ByteBuf metadata = RequestFireAndForgetFrameCodec.metadata(request); + + assertFalse(FrameHeaderCodec.hasMetadata(request)); + assertEquals("d", data); + assertNull(metadata); + request.release(); + } + + @Test + void requestFnfMetadata() { + ByteBuf request = + RequestFireAndForgetFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), + Unpooled.EMPTY_BUFFER); + + ByteBuf data = RequestFireAndForgetFrameCodec.data(request); + String metadata = + RequestFireAndForgetFrameCodec.metadata(request).toString(StandardCharsets.UTF_8); + + assertTrue(FrameHeaderCodec.hasMetadata(request)); + assertEquals("md", metadata); + assertTrue(data.readableBytes() == 0); + request.release(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/KeepaliveFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/KeepaliveFrameFlyweightTest.java index eeb950148..bc013e024 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/KeepaliveFrameFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/KeepaliveFrameFlyweightTest.java @@ -1,52 +1,32 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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 static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import java.nio.charset.StandardCharsets; -import org.junit.Test; - -public class KeepaliveFrameFlyweightTest { - private final ByteBuf byteBuf = Unpooled.buffer(1024); +import org.junit.jupiter.api.Test; +class KeepaliveFrameFlyweightTest { @Test - public void canReadData() { + void canReadData() { ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); - int length = - KeepaliveFrameFlyweight.encode(byteBuf, KeepaliveFrameFlyweight.FLAGS_KEEPALIVE_R, data); - data.resetReaderIndex(); - - assertEquals( - KeepaliveFrameFlyweight.FLAGS_KEEPALIVE_R, - FrameHeaderFlyweight.flags(byteBuf) & KeepaliveFrameFlyweight.FLAGS_KEEPALIVE_R); - assertEquals(data, FrameHeaderFlyweight.sliceFrameData(byteBuf)); + ByteBuf frame = KeepAliveFrameCodec.encode(ByteBufAllocator.DEFAULT, true, 0, data); + assertTrue(KeepAliveFrameCodec.respondFlag(frame)); + assertEquals(data, KeepAliveFrameCodec.data(frame)); + frame.release(); } @Test - public void testEncoding() { - int encoded = - KeepaliveFrameFlyweight.encode( - byteBuf, - KeepaliveFrameFlyweight.FLAGS_KEEPALIVE_R, - Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - assertEquals("00000f000000000c80000000000000000064", ByteBufUtil.hexDump(byteBuf, 0, encoded)); + void testEncoding() { + ByteBuf frame = + KeepAliveFrameCodec.encode( + ByteBufAllocator.DEFAULT, true, 0, Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); + frame = FrameLengthCodec.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); + assertEquals("00000f000000000c80000000000000000064", ByteBufUtil.hexDump(frame)); + frame.release(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameCodecTest.java new file mode 100644 index 000000000..73c3bde5e --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameCodecTest.java @@ -0,0 +1,42 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class LeaseFrameCodecTest { + + @Test + void leaseMetadata() { + ByteBuf metadata = bytebuf("md"); + int ttl = 1; + int numRequests = 42; + ByteBuf lease = LeaseFrameCodec.encode(ByteBufAllocator.DEFAULT, ttl, numRequests, metadata); + + Assertions.assertTrue(FrameHeaderCodec.hasMetadata(lease)); + Assertions.assertEquals(ttl, LeaseFrameCodec.ttl(lease)); + Assertions.assertEquals(numRequests, LeaseFrameCodec.numRequests(lease)); + Assertions.assertEquals(metadata, LeaseFrameCodec.metadata(lease)); + lease.release(); + } + + @Test + void leaseAbsentMetadata() { + int ttl = 1; + int numRequests = 42; + ByteBuf lease = LeaseFrameCodec.encode(ByteBufAllocator.DEFAULT, ttl, numRequests, null); + + Assertions.assertFalse(FrameHeaderCodec.hasMetadata(lease)); + Assertions.assertEquals(ttl, LeaseFrameCodec.ttl(lease)); + Assertions.assertEquals(numRequests, LeaseFrameCodec.numRequests(lease)); + Assertions.assertNull(LeaseFrameCodec.metadata(lease)); + lease.release(); + } + + private static ByteBuf bytebuf(String str) { + return Unpooled.copiedBuffer(str, StandardCharsets.UTF_8); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameFlyweightTest.java deleted file mode 100644 index b943c6f20..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameFlyweightTest.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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 static org.junit.Assert.*; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import java.nio.charset.StandardCharsets; -import org.junit.Test; - -public class LeaseFrameFlyweightTest { - private final ByteBuf byteBuf = Unpooled.buffer(1024); - - @Test - public void size() { - ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); - int length = LeaseFrameFlyweight.encode(byteBuf, 0, 0, metadata); - assertEquals(length, 9 + 4 * 2 + 4); // Frame header + ttl + #requests + 4 byte metadata - } - - @Test - public void testEncoding() { - int encoded = - LeaseFrameFlyweight.encode( - byteBuf, 0, 0, Unpooled.copiedBuffer("md", StandardCharsets.UTF_8)); - assertEquals( - "00001000000000090000000000000000006d64", ByteBufUtil.hexDump(byteBuf, 0, encoded)); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/PayloadFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/PayloadFlyweightTest.java new file mode 100644 index 000000000..aecbb31ce --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/PayloadFlyweightTest.java @@ -0,0 +1,88 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.Payload; +import io.rsocket.util.DefaultPayload; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class PayloadFlyweightTest { + + @Test + void nextCompleteDataMetadata() { + Payload payload = DefaultPayload.create("d", "md"); + ByteBuf nextComplete = + PayloadFrameCodec.encodeNextCompleteReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); + String data = PayloadFrameCodec.data(nextComplete).toString(StandardCharsets.UTF_8); + String metadata = PayloadFrameCodec.metadata(nextComplete).toString(StandardCharsets.UTF_8); + Assertions.assertEquals("d", data); + Assertions.assertEquals("md", metadata); + nextComplete.release(); + } + + @Test + void nextCompleteData() { + Payload payload = DefaultPayload.create("d"); + ByteBuf nextComplete = + PayloadFrameCodec.encodeNextCompleteReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); + String data = PayloadFrameCodec.data(nextComplete).toString(StandardCharsets.UTF_8); + ByteBuf metadata = PayloadFrameCodec.metadata(nextComplete); + Assertions.assertEquals("d", data); + Assertions.assertNull(metadata); + nextComplete.release(); + } + + @Test + void nextCompleteMetaData() { + Payload payload = + DefaultPayload.create( + Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer("md".getBytes(StandardCharsets.UTF_8))); + + ByteBuf nextComplete = + PayloadFrameCodec.encodeNextCompleteReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); + ByteBuf data = PayloadFrameCodec.data(nextComplete); + String metadata = PayloadFrameCodec.metadata(nextComplete).toString(StandardCharsets.UTF_8); + Assertions.assertTrue(data.readableBytes() == 0); + Assertions.assertEquals("md", metadata); + nextComplete.release(); + } + + @Test + void nextDataMetadata() { + Payload payload = DefaultPayload.create("d", "md"); + ByteBuf next = + PayloadFrameCodec.encodeNextReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); + String data = PayloadFrameCodec.data(next).toString(StandardCharsets.UTF_8); + String metadata = PayloadFrameCodec.metadata(next).toString(StandardCharsets.UTF_8); + Assertions.assertEquals("d", data); + Assertions.assertEquals("md", metadata); + next.release(); + } + + @Test + void nextData() { + Payload payload = DefaultPayload.create("d"); + ByteBuf next = + PayloadFrameCodec.encodeNextReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); + String data = PayloadFrameCodec.data(next).toString(StandardCharsets.UTF_8); + ByteBuf metadata = PayloadFrameCodec.metadata(next); + Assertions.assertEquals("d", data); + Assertions.assertNull(metadata); + next.release(); + } + + @Test + void nextDataEmptyMetadata() { + Payload payload = DefaultPayload.create("d".getBytes(), new byte[0]); + ByteBuf next = + PayloadFrameCodec.encodeNextReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); + String data = PayloadFrameCodec.data(next).toString(StandardCharsets.UTF_8); + ByteBuf metadata = PayloadFrameCodec.metadata(next); + Assertions.assertEquals("d", data); + Assertions.assertEquals(metadata.readableBytes(), 0); + next.release(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/RequestFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/RequestFrameFlyweightTest.java deleted file mode 100644 index 4be29fca4..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/RequestFrameFlyweightTest.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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 static org.junit.Assert.assertEquals; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import io.rsocket.FrameType; -import java.nio.charset.StandardCharsets; -import org.junit.Test; - -public class RequestFrameFlyweightTest { - private final ByteBuf byteBuf = Unpooled.buffer(1024); - - @Test - public void testEncoding() { - int encoded = - RequestFrameFlyweight.encode( - byteBuf, - 1, - 0, - FrameType.REQUEST_STREAM, - 1, - Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), - Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - assertEquals( - "000010000000011900000000010000026d6464", ByteBufUtil.hexDump(byteBuf, 0, encoded)); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/RequestNFrameCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/RequestNFrameCodecTest.java new file mode 100644 index 000000000..e38258040 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/RequestNFrameCodecTest.java @@ -0,0 +1,19 @@ +package io.rsocket.frame; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import org.junit.jupiter.api.Test; + +class RequestNFrameCodecTest { + @Test + void testEncoding() { + ByteBuf frame = RequestNFrameCodec.encode(ByteBufAllocator.DEFAULT, 1, 5); + + frame = FrameLengthCodec.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); + assertEquals("00000a00000001200000000005", ByteBufUtil.hexDump(frame)); + frame.release(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/RequestNFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/RequestNFrameFlyweightTest.java deleted file mode 100644 index 8df28a7be..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/RequestNFrameFlyweightTest.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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 static org.junit.Assert.assertEquals; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import org.junit.Test; - -public class RequestNFrameFlyweightTest { - private final ByteBuf byteBuf = Unpooled.buffer(1024); - - @Test - public void testEncoding() { - int encoded = RequestNFrameFlyweight.encode(byteBuf, 1, 5); - assertEquals("00000a00000001200000000005", ByteBufUtil.hexDump(byteBuf, 0, encoded)); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameCodecTest.java new file mode 100644 index 000000000..fe05335d2 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameCodecTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.frame; + +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 { + + @Test + void testEncoding() { + byte[] tokenBytes = new byte[65000]; + 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)); + 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 new file mode 100644 index 000000000..33dd8eb70 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameCodecTest.java @@ -0,0 +1,16 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import org.junit.Assert; +import org.junit.Test; + +public class ResumeOkFrameCodecTest { + + @Test + public void testEncoding() { + ByteBuf byteBuf = ResumeOkFrameCodec.encode(ByteBufAllocator.DEFAULT, 42); + Assert.assertEquals(42, ResumeOkFrameCodec.lastReceivedClientPos(byteBuf)); + byteBuf.release(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameCodecTest.java new file mode 100644 index 000000000..3317b4618 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameCodecTest.java @@ -0,0 +1,57 @@ +package io.rsocket.frame; + +import static org.junit.jupiter.api.Assertions.*; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.Payload; +import io.rsocket.util.DefaultPayload; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class SetupFrameCodecTest { + @Test + void testEncodingNoResume() { + ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); + ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); + Payload payload = DefaultPayload.create(data, metadata); + ByteBuf frame = + SetupFrameCodec.encode( + ByteBufAllocator.DEFAULT, false, 5, 500, "metadata_type", "data_type", payload); + + assertEquals(FrameType.SETUP, FrameHeaderCodec.frameType(frame)); + assertFalse(SetupFrameCodec.resumeEnabled(frame)); + assertEquals(0, SetupFrameCodec.resumeToken(frame).readableBytes()); + assertEquals("metadata_type", SetupFrameCodec.metadataMimeType(frame)); + assertEquals("data_type", SetupFrameCodec.dataMimeType(frame)); + assertEquals(payload.metadata(), SetupFrameCodec.metadata(frame)); + assertEquals(payload.data(), SetupFrameCodec.data(frame)); + assertEquals(SetupFrameCodec.CURRENT_VERSION, SetupFrameCodec.version(frame)); + frame.release(); + } + + @Test + void testEncodingResume() { + byte[] tokenBytes = new byte[65000]; + Arrays.fill(tokenBytes, (byte) 1); + ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); + ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); + Payload payload = DefaultPayload.create(data, metadata); + ByteBuf token = Unpooled.wrappedBuffer(tokenBytes); + ByteBuf frame = + SetupFrameCodec.encode( + ByteBufAllocator.DEFAULT, true, 5, 500, token, "metadata_type", "data_type", payload); + + assertEquals(FrameType.SETUP, FrameHeaderCodec.frameType(frame)); + assertTrue(SetupFrameCodec.honorLease(frame)); + assertTrue(SetupFrameCodec.resumeEnabled(frame)); + assertEquals(token, SetupFrameCodec.resumeToken(frame)); + assertEquals("metadata_type", SetupFrameCodec.metadataMimeType(frame)); + assertEquals("data_type", SetupFrameCodec.dataMimeType(frame)); + assertEquals(payload.metadata(), SetupFrameCodec.metadata(frame)); + assertEquals(payload.data(), SetupFrameCodec.data(frame)); + assertEquals(SetupFrameCodec.CURRENT_VERSION, SetupFrameCodec.version(frame)); + frame.release(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java deleted file mode 100644 index 8dd5f7a8b..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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 static org.junit.Assert.*; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import io.rsocket.FrameType; -import java.nio.charset.StandardCharsets; -import org.junit.Test; - -public class SetupFrameFlyweightTest { - private final ByteBuf byteBuf = Unpooled.buffer(1024); - - @Test - public void validFrame() { - ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); - ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); - SetupFrameFlyweight.encode(byteBuf, 0, 5, 500, "metadata_type", "data_type", metadata, data); - - metadata.resetReaderIndex(); - data.resetReaderIndex(); - - assertEquals(FrameType.SETUP, FrameHeaderFlyweight.frameType(byteBuf)); - assertEquals("metadata_type", SetupFrameFlyweight.metadataMimeType(byteBuf)); - assertEquals("data_type", SetupFrameFlyweight.dataMimeType(byteBuf)); - assertEquals(metadata, FrameHeaderFlyweight.sliceFrameMetadata(byteBuf)); - assertEquals(data, FrameHeaderFlyweight.sliceFrameData(byteBuf)); - } - - @Test(expected = IllegalArgumentException.class) - public void resumeNotSupported() { - SetupFrameFlyweight.encode( - byteBuf, - SetupFrameFlyweight.FLAGS_RESUME_ENABLE, - 5, - 500, - "", - "", - Unpooled.EMPTY_BUFFER, - Unpooled.EMPTY_BUFFER); - } - - @Test - public void validResumeFrame() { - ByteBuf token = Unpooled.wrappedBuffer(new byte[] {2, 3}); - ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); - ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); - SetupFrameFlyweight.encode( - byteBuf, - SetupFrameFlyweight.FLAGS_RESUME_ENABLE, - 5, - 500, - token, - "metadata_type", - "data_type", - metadata, - data); - - token.resetReaderIndex(); - metadata.resetReaderIndex(); - data.resetReaderIndex(); - - assertEquals(FrameType.SETUP, FrameHeaderFlyweight.frameType(byteBuf)); - assertEquals("metadata_type", SetupFrameFlyweight.metadataMimeType(byteBuf)); - assertEquals("data_type", SetupFrameFlyweight.dataMimeType(byteBuf)); - assertEquals(metadata, FrameHeaderFlyweight.sliceFrameMetadata(byteBuf)); - assertEquals(data, FrameHeaderFlyweight.sliceFrameData(byteBuf)); - assertEquals( - SetupFrameFlyweight.FLAGS_RESUME_ENABLE, - FrameHeaderFlyweight.flags(byteBuf) & SetupFrameFlyweight.FLAGS_RESUME_ENABLE); - } - - @Test - public void testEncoding() { - int encoded = - SetupFrameFlyweight.encode( - byteBuf, - 0, - 5000, - 60000, - "mdmt", - "dmt", - Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), - Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - assertEquals( - "00002100000000050000010000000013880000ea60046d646d7403646d740000026d6464", - ByteBufUtil.hexDump(byteBuf, 0, encoded)); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/VersionCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/VersionCodecTest.java new file mode 100644 index 000000000..be7fb837b --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/VersionCodecTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.frame; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +public class VersionCodecTest { + @Test + public void simple() { + int version = VersionCodec.encode(1, 0); + assertEquals(1, VersionCodec.major(version)); + assertEquals(0, VersionCodec.minor(version)); + assertEquals(0x00010000, version); + assertEquals("1.0", VersionCodec.toString(version)); + } + + @Test + public void complex() { + int version = VersionCodec.encode(0x1234, 0x5678); + assertEquals(0x1234, VersionCodec.major(version)); + assertEquals(0x5678, VersionCodec.minor(version)); + assertEquals(0x12345678, version); + assertEquals("4660.22136", VersionCodec.toString(version)); + } + + @Test + public void noShortOverflow() { + int version = VersionCodec.encode(43210, 43211); + assertEquals(43210, VersionCodec.major(version)); + assertEquals(43211, VersionCodec.minor(version)); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/VersionFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/VersionFlyweightTest.java deleted file mode 100644 index 3e7cf8b55..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/VersionFlyweightTest.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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 static org.junit.Assert.*; - -import org.junit.Test; - -public class VersionFlyweightTest { - @Test - public void simple() { - int version = VersionFlyweight.encode(1, 0); - assertEquals(1, VersionFlyweight.major(version)); - assertEquals(0, VersionFlyweight.minor(version)); - assertEquals(0x00010000, version); - assertEquals("1.0", VersionFlyweight.toString(version)); - } - - @Test - public void complex() { - int version = VersionFlyweight.encode(0x1234, 0x5678); - assertEquals(0x1234, VersionFlyweight.major(version)); - assertEquals(0x5678, VersionFlyweight.minor(version)); - assertEquals(0x12345678, version); - assertEquals("4660.22136", VersionFlyweight.toString(version)); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/old/LeaseFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/old/LeaseFrameFlyweightTest.java new file mode 100644 index 000000000..ef4fcc6b0 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/old/LeaseFrameFlyweightTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.frame.old; + +public class LeaseFrameFlyweightTest { + /*private final ByteBuf byteBuf = Unpooled.buffer(1024); + + @Test + public void size() { + ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); + int length = LeaseFrameFlyweight.encode(byteBuf, 0, 0, metadata); + assertEquals(length, 9 + 4 * 2 + 4); // Frame header + ttl + #requests + 4 byte metadata + } + + @Test + public void testEncoding() { + int encoded = + LeaseFrameFlyweight.encode( + byteBuf, 0, 0, Unpooled.copiedBuffer("md", StandardCharsets.UTF_8)); + assertEquals( + "00001000000000090000000000000000006d64", ByteBufUtil.hexDump(byteBuf, 0, encoded)); + }*/ +} diff --git a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java index 6e402d6e1..341577b4c 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java @@ -1,84 +1,274 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket.internal; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import io.rsocket.Frame; -import io.rsocket.plugins.PluginRegistry; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.KeepAliveFrameCodec; +import io.rsocket.frame.LeaseFrameCodec; +import io.rsocket.frame.MetadataPushFrameCodec; +import io.rsocket.frame.ResumeFrameCodec; +import io.rsocket.frame.ResumeOkFrameCodec; +import io.rsocket.frame.SetupFrameCodec; +import io.rsocket.plugins.InitializingInterceptorRegistry; import io.rsocket.test.util.TestDuplexConnection; +import io.rsocket.util.DefaultPayload; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; public class ClientServerInputMultiplexerTest { private TestDuplexConnection source; - private ClientServerInputMultiplexer multiplexer; + private ClientServerInputMultiplexer clientMultiplexer; + private LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + private ClientServerInputMultiplexer serverMultiplexer; @Before public void setup() { - source = new TestDuplexConnection(); - multiplexer = new ClientServerInputMultiplexer(source, new PluginRegistry()); + source = new TestDuplexConnection(allocator); + clientMultiplexer = + new ClientServerInputMultiplexer(source, new InitializingInterceptorRegistry(), true); + serverMultiplexer = + new ClientServerInputMultiplexer(source, new InitializingInterceptorRegistry(), false); } @Test - public void testSplits() { + public void clientSplits() { AtomicInteger clientFrames = new AtomicInteger(); AtomicInteger serverFrames = new AtomicInteger(); - AtomicInteger connectionFrames = new AtomicInteger(); + AtomicInteger setupFrames = new AtomicInteger(); - multiplexer + clientMultiplexer .asClientConnection() .receive() .doOnNext(f -> clientFrames.incrementAndGet()) .subscribe(); - multiplexer + clientMultiplexer .asServerConnection() .receive() .doOnNext(f -> serverFrames.incrementAndGet()) .subscribe(); - multiplexer - .asStreamZeroConnection() + clientMultiplexer + .asSetupConnection() .receive() - .doOnNext(f -> connectionFrames.incrementAndGet()) + .doOnNext(f -> setupFrames.incrementAndGet()) .subscribe(); - source.addToReceivedBuffer(Frame.Error.from(1, new Exception())); - assertEquals(1, clientFrames.get()); + source.addToReceivedBuffer(setupFrame()); + assertEquals(0, clientFrames.get()); assertEquals(0, serverFrames.get()); - assertEquals(0, connectionFrames.get()); + assertEquals(1, setupFrames.get()); - source.addToReceivedBuffer(Frame.Error.from(2, new Exception())); + source.addToReceivedBuffer(errorFrame(1)); assertEquals(1, clientFrames.get()); + assertEquals(0, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(errorFrame(1)); + assertEquals(2, clientFrames.get()); + assertEquals(0, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(leaseFrame()); + assertEquals(3, clientFrames.get()); + assertEquals(0, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(keepAliveFrame()); + assertEquals(4, clientFrames.get()); + assertEquals(0, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(errorFrame(2)); + assertEquals(4, clientFrames.get()); assertEquals(1, serverFrames.get()); - assertEquals(0, connectionFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(errorFrame(0)); + assertEquals(5, clientFrames.get()); + assertEquals(1, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(metadataPushFrame()); + assertEquals(5, clientFrames.get()); + assertEquals(2, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(resumeFrame()); + assertEquals(5, clientFrames.get()); + assertEquals(2, serverFrames.get()); + assertEquals(2, setupFrames.get()); + + source.addToReceivedBuffer(resumeOkFrame()); + assertEquals(5, clientFrames.get()); + assertEquals(2, serverFrames.get()); + assertEquals(3, setupFrames.get()); + } + + @Test + public void serverSplits() { + AtomicInteger clientFrames = new AtomicInteger(); + AtomicInteger serverFrames = new AtomicInteger(); + AtomicInteger setupFrames = new AtomicInteger(); - source.addToReceivedBuffer(Frame.Error.from(1, new Exception())); + serverMultiplexer + .asClientConnection() + .receive() + .doOnNext(f -> clientFrames.incrementAndGet()) + .subscribe(); + serverMultiplexer + .asServerConnection() + .receive() + .doOnNext(f -> serverFrames.incrementAndGet()) + .subscribe(); + serverMultiplexer + .asSetupConnection() + .receive() + .doOnNext(f -> setupFrames.incrementAndGet()) + .subscribe(); + + source.addToReceivedBuffer(setupFrame()); + assertEquals(0, clientFrames.get()); + assertEquals(0, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(errorFrame(1)); + assertEquals(1, clientFrames.get()); + assertEquals(0, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(errorFrame(1)); + assertEquals(2, clientFrames.get()); + assertEquals(0, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(leaseFrame()); assertEquals(2, clientFrames.get()); assertEquals(1, serverFrames.get()); - assertEquals(0, connectionFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(keepAliveFrame()); + assertEquals(2, clientFrames.get()); + assertEquals(2, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(errorFrame(2)); + assertEquals(2, clientFrames.get()); + assertEquals(3, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(errorFrame(0)); + assertEquals(2, clientFrames.get()); + assertEquals(4, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(metadataPushFrame()); + assertEquals(3, clientFrames.get()); + assertEquals(4, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(resumeFrame()); + assertEquals(3, clientFrames.get()); + assertEquals(4, serverFrames.get()); + assertEquals(2, setupFrames.get()); + + source.addToReceivedBuffer(resumeOkFrame()); + assertEquals(3, clientFrames.get()); + assertEquals(4, serverFrames.get()); + assertEquals(3, setupFrames.get()); } @Test - @Ignore - public void testPluginsApplied() { - // TODO implement test - fail(); + public void unexpectedFramesBeforeSetupFrame() { + AtomicInteger clientFrames = new AtomicInteger(); + AtomicInteger serverFrames = new AtomicInteger(); + AtomicInteger setupFrames = new AtomicInteger(); + + AtomicReference clientError = new AtomicReference<>(); + AtomicReference serverError = new AtomicReference<>(); + AtomicReference setupError = new AtomicReference<>(); + + serverMultiplexer + .asClientConnection() + .receive() + .subscribe(bb -> clientFrames.incrementAndGet(), clientError::set); + serverMultiplexer + .asServerConnection() + .receive() + .subscribe(bb -> serverFrames.incrementAndGet(), serverError::set); + serverMultiplexer + .asSetupConnection() + .receive() + .subscribe(bb -> setupFrames.incrementAndGet(), setupError::set); + + source.addToReceivedBuffer(keepAliveFrame()); + + assertThat(clientError.get().getMessage()) + .isEqualTo("SETUP or LEASE frame must be received before any others."); + assertThat(serverError.get().getMessage()) + .isEqualTo("SETUP or LEASE frame must be received before any others."); + assertThat(setupError.get().getMessage()) + .isEqualTo("SETUP or LEASE frame must be received before any others."); + + assertEquals(0, clientFrames.get()); + assertEquals(0, serverFrames.get()); + assertEquals(0, setupFrames.get()); + } + + private ByteBuf resumeFrame() { + return ResumeFrameCodec.encode(allocator, Unpooled.EMPTY_BUFFER, 0, 0); + } + + private ByteBuf setupFrame() { + return SetupFrameCodec.encode( + ByteBufAllocator.DEFAULT, + false, + 0, + 42, + "application/octet-stream", + "application/octet-stream", + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER)); + } + + private ByteBuf leaseFrame() { + return LeaseFrameCodec.encode(allocator, 1_000, 1, Unpooled.EMPTY_BUFFER); + } + + private ByteBuf errorFrame(int i) { + return ErrorFrameCodec.encode(allocator, i, new Exception()); + } + + private ByteBuf resumeOkFrame() { + return ResumeOkFrameCodec.encode(allocator, 0); + } + + private ByteBuf keepAliveFrame() { + return KeepAliveFrameCodec.encode(allocator, false, 0, Unpooled.EMPTY_BUFFER); + } + + private ByteBuf metadataPushFrame() { + return MetadataPushFrameCodec.encode(allocator, Unpooled.EMPTY_BUFFER); } } diff --git a/rsocket-core/src/test/java/io/rsocket/internal/SchedulerUtils.java b/rsocket-core/src/test/java/io/rsocket/internal/SchedulerUtils.java new file mode 100644 index 000000000..d73f92b85 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/internal/SchedulerUtils.java @@ -0,0 +1,23 @@ +package io.rsocket.internal; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import reactor.core.scheduler.Scheduler; + +public class SchedulerUtils { + + public static void warmup(Scheduler scheduler) throws InterruptedException { + warmup(scheduler, 10000); + } + + public static void warmup(Scheduler scheduler, int times) throws InterruptedException { + scheduler.start(); + + // warm up + CountDownLatch latch = new CountDownLatch(times); + for (int i = 0; i < times; i++) { + scheduler.schedule(latch::countDown); + } + latch.await(5, TimeUnit.SECONDS); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java new file mode 100644 index 000000000..b0c6c6fd8 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java @@ -0,0 +1,368 @@ +/* + * 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.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCountUtil; +import io.rsocket.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.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.Fuseable; +import reactor.core.publisher.Hooks; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; +import reactor.test.util.RaceTestUtils; + +public class UnboundedProcessorTest { + + @BeforeAll + public static void setup() { + Hooks.onErrorDropped(__ -> {}); + } + + @AfterAll + public static void teardown() { + Hooks.resetOnErrorDropped(); + } + + @ParameterizedTest( + name = + "Test that emitting {0} onNext before subscribe and requestN should deliver all the signals once the subscriber is available") + @ValueSource(ints = {10, 100, 10_000, 100_000, 1_000_000, 10_000_000}) + public void testOnNextBeforeSubscribeN(int n) { + UnboundedProcessor processor = new UnboundedProcessor<>(); + + for (int i = 0; i < n; i++) { + processor.onNext(Unpooled.EMPTY_BUFFER); + } + + processor.onComplete(); + + StepVerifier.create(processor.count()).expectNext(Long.valueOf(n)).verifyComplete(); + } + + @ParameterizedTest( + name = + "Test that emitting {0} onNext after subscribe and requestN should deliver all the signals") + @ValueSource(ints = {10, 100, 10_000}) + public void testOnNextAfterSubscribeN(int n) { + UnboundedProcessor processor = new UnboundedProcessor<>(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + + processor.subscribe(assertSubscriber); + + for (int i = 0; i < n; i++) { + processor.onNext(Unpooled.EMPTY_BUFFER); + } + + assertSubscriber.awaitAndAssertNextValueCount(n); + } + + @ParameterizedTest( + name = + "Test that prioritized value sending deliver prioritized signals before the others mode[fusionEnabled={0}]") + @ValueSource(booleans = {true, false}) + public void testPrioritizedSending(boolean fusedCase) { + UnboundedProcessor processor = new UnboundedProcessor<>(); + + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + processor.onNext(Unpooled.EMPTY_BUFFER); + } + + processor.onNextPrioritized(Unpooled.copiedBuffer("test", CharsetUtil.UTF_8)); + + assertThat(fusedCase ? processor.poll() : processor.next().block()) + .isNotNull() + .extracting(bb -> bb.toString(CharsetUtil.UTF_8)) + .isEqualTo("test"); + } + + @ParameterizedTest( + name = + "Ensures that racing between onNext | dispose | cancel | request(n) will not cause any issues and leaks; mode[fusionEnabled={0}]") + @ValueSource(booleans = {true, false}) + public void ensureUnboundedProcessorDisposesQueueProperly(boolean withFusionEnabled) { + final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + for (int i = 0; i < RaceTestConstants.REPEATS; i++) { + final UnboundedProcessor unboundedProcessor = new UnboundedProcessor<>(); + + final ByteBuf buffer1 = allocator.buffer(1); + final ByteBuf buffer2 = allocator.buffer(2); + + final AssertSubscriber assertSubscriber = + new AssertSubscriber(0) + .requestedFusionMode(withFusionEnabled ? Fuseable.ANY : Fuseable.NONE); + + unboundedProcessor.subscribe(assertSubscriber); + + RaceTestUtils.race( + () -> { + 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}) + @Disabled("hard to support in 1.0.x") + public void smokeTest2(boolean withFusionEnabled) { + final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + final RuntimeException runtimeException = new RuntimeException("test"); + for (int i = 0; i < 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.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 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 new file mode 100644 index 000000000..54b99c797 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/internal/subscriber/AssertSubscriber.java @@ -0,0 +1,1256 @@ +/* + * Copyright (c) 2011-2017 Pivotal Software Inc, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.internal.subscriber; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.core.publisher.Operators; +import reactor.util.annotation.NonNull; +import reactor.util.context.Context; + +/** + * A Subscriber implementation that hosts assertion tests for its state and allows asynchronous + * cancellation and requesting. + * + *

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

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

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

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

Usage: + * + *

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

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

Be sure at least a publisher has subscribed to it via {@link + * Publisher#subscribe(Subscriber)} before use assert methods. + * + * @param n Number of elements to request (can be 0 if you want no initial demand). + * @param the observed value type + * @return a fresh AssertSubscriber instance + */ + public static AssertSubscriber create(long n) { + return new AssertSubscriber<>(n); + } + + // + // ============================================================================================================== + // constructors + // + // ============================================================================================================== + + public AssertSubscriber() { + this(Context.empty(), Long.MAX_VALUE); + } + + public AssertSubscriber(long n) { + this(Context.empty(), n); + } + + public AssertSubscriber(Context context) { + this(context, Long.MAX_VALUE); + } + + public AssertSubscriber(Context context, long n) { + if (n < 0) { + throw new IllegalArgumentException("initialRequest >= required but it was " + n); + } + this.context = context; + REQUESTED.lazySet(this, n); + } + + // + // ============================================================================================================== + // Configuration + // + // ============================================================================================================== + + /** + * Enable or disabled the values storage. It is enabled by default, and can be disable in order to + * be able to perform performance benchmarks or tests with a huge amount values. + * + * @param enabled enable value storage? + * @return this + */ + public final AssertSubscriber configureValuesStorage(boolean enabled) { + this.valuesStorage = enabled; + return this; + } + + /** + * Configure the timeout in seconds for waiting next values to be received (3 seconds by default). + * + * @param timeout the new default value timeout duration + * @return this + */ + public final AssertSubscriber configureValuesTimeout(Duration timeout) { + this.valuesTimeout = timeout; + return this; + } + + /** + * Returns the established fusion mode or -1 if it was not enabled + * + * @return the fusion mode, see Fuseable constants + */ + public final int establishedFusionMode() { + return establishedFusionMode; + } + + // + // ============================================================================================================== + // Assertions + // + // ============================================================================================================== + + /** + * Assert a complete successfully signal has been received. + * + * @return this + */ + public final AssertSubscriber assertComplete() { + assertNoError(); + int c = completionCount; + if (c == 0) { + throw new AssertionError("Not completed", null); + } + if (c > 1) { + throw new AssertionError("Multiple completions: " + c, null); + } + return this; + } + + /** + * Assert the specified values have been received. Values storage should be enabled to use this + * method. + * + * @param expectedValues the values to assert + * @see #configureValuesStorage(boolean) + * @return this + */ + public final AssertSubscriber assertContainValues(Set expectedValues) { + if (!valuesStorage) { + throw new IllegalStateException("Using assertNoValues() requires enabling values storage"); + } + if (expectedValues.size() > values.size()) { + throw new AssertionError("Actual contains fewer elements" + values, null); + } + + Iterator expected = expectedValues.iterator(); + + for (; ; ) { + boolean n2 = expected.hasNext(); + if (n2) { + T t2 = expected.next(); + if (!values.contains(t2)) { + throw new AssertionError( + "The element is not contained in the " + + "received results" + + " = " + + valueAndClass(t2), + null); + } + } else { + break; + } + } + return this; + } + + /** + * Assert an error signal has been received. + * + * @return this + */ + public final AssertSubscriber assertError() { + assertNotComplete(); + int s = errors.size(); + if (s == 0) { + throw new AssertionError("No error", null); + } + if (s > 1) { + throw new AssertionError("Multiple errors: " + s, null); + } + return this; + } + + /** + * Assert an error signal has been received. + * + * @param clazz The class of the exception contained in the error signal + * @return this + */ + public final AssertSubscriber assertError(Class clazz) { + assertNotComplete(); + int s = errors.size(); + if (s == 0) { + throw new AssertionError("No error", null); + } + if (s == 1) { + Throwable e = errors.get(0); + if (!clazz.isInstance(e)) { + throw new AssertionError( + "Error class incompatible: expected = " + clazz + ", actual = " + e, null); + } + } + if (s > 1) { + throw new AssertionError("Multiple errors: " + errors, null); + } + return this; + } + + public final AssertSubscriber assertErrorMessage(String message) { + assertNotComplete(); + int s = errors.size(); + if (s == 0) { + assertionError("No error", null); + } + if (s == 1) { + if (!Objects.equals(message, errors.get(0).getMessage())) { + assertionError( + "Error class incompatible: expected = \"" + + message + + "\", actual = \"" + + errors.get(0).getMessage() + + "\"", + null); + } + } + if (s > 1) { + assertionError("Multiple errors: " + s, null); + } + + return this; + } + + /** + * Assert an error signal has been received. + * + * @param expectation A method that can verify the exception contained in the error signal and + * throw an exception (like an {@link AssertionError}) if the exception is not valid. + * @return this + */ + public final AssertSubscriber assertErrorWith(Consumer expectation) { + assertNotComplete(); + int s = errors.size(); + if (s == 0) { + throw new AssertionError("No error", null); + } + if (s == 1) { + expectation.accept(errors.get(0)); + } + if (s > 1) { + throw new AssertionError("Multiple errors: " + s, null); + } + return this; + } + + /** + * Assert that the upstream was a Fuseable source. + * + * @return this + */ + public final AssertSubscriber assertFuseableSource() { + if (qs == null) { + throw new AssertionError("Upstream was not Fuseable"); + } + return this; + } + + /** + * Assert that the fusion mode was granted. + * + * @return this + */ + public final AssertSubscriber assertFusionEnabled() { + if (establishedFusionMode != Fuseable.SYNC && establishedFusionMode != Fuseable.ASYNC) { + throw new AssertionError("Fusion was not enabled"); + } + return this; + } + + public final AssertSubscriber assertFusionMode(int expectedMode) { + if (establishedFusionMode != expectedMode) { + throw new AssertionError( + "Wrong fusion mode: expected: " + + fusionModeName(expectedMode) + + ", actual: " + + fusionModeName(establishedFusionMode)); + } + return this; + } + + /** + * Assert that the fusion mode was granted. + * + * @return this + */ + public final AssertSubscriber assertFusionRejected() { + if (establishedFusionMode != Fuseable.NONE) { + throw new AssertionError("Fusion was granted"); + } + return this; + } + + /** + * Assert no error signal has been received. + * + * @return this + */ + public final AssertSubscriber assertNoError() { + int s = errors.size(); + if (s == 1) { + Throwable e = errors.get(0); + String valueAndClass = e == null ? null : e + " (" + e.getClass().getSimpleName() + ")"; + throw new AssertionError("Error present: " + valueAndClass, null); + } + if (s > 1) { + throw new AssertionError("Multiple errors: " + s, null); + } + return this; + } + + /** + * Assert no values have been received. + * + * @return this + */ + public final AssertSubscriber assertNoValues() { + if (valueCount != 0) { + throw new AssertionError( + "No values expected but received: [length = " + values.size() + "] " + values, null); + } + return this; + } + + /** + * Assert that the upstream was not a Fuseable source. + * + * @return this + */ + public final AssertSubscriber assertNonFuseableSource() { + if (qs != null) { + throw new AssertionError("Upstream was Fuseable"); + } + return this; + } + + /** + * Assert no complete successfully signal has been received. + * + * @return this + */ + public final AssertSubscriber assertNotComplete() { + int c = completionCount; + if (c == 1) { + throw new AssertionError("Completed", null); + } + if (c > 1) { + throw new AssertionError("Multiple completions: " + c, null); + } + return this; + } + + /** + * Assert no subscription occurred. + * + * @return this + */ + public final AssertSubscriber assertNotSubscribed() { + int s = subscriptionCount; + + if (s == 1) { + throw new AssertionError("OnSubscribe called once", null); + } + if (s > 1) { + throw new AssertionError("OnSubscribe called multiple times: " + s, null); + } + + return this; + } + + /** + * Assert no complete successfully or error signal has been received. + * + * @return this + */ + public final AssertSubscriber assertNotTerminated() { + if (cdl.getCount() == 0) { + throw new AssertionError("Terminated", null); + } + return this; + } + + /** + * Assert subscription occurred (once). + * + * @return this + */ + public final AssertSubscriber assertSubscribed() { + int s = subscriptionCount; + + if (s == 0) { + throw new AssertionError("OnSubscribe not called", null); + } + if (s > 1) { + throw new AssertionError("OnSubscribe called multiple times: " + s, null); + } + + return this; + } + + /** + * Assert either complete successfully or error signal has been received. + * + * @return this + */ + public final AssertSubscriber assertTerminated() { + if (cdl.getCount() != 0) { + throw new AssertionError("Not terminated", null); + } + return this; + } + + /** + * Assert {@code n} values has been received. + * + * @param n the expected value count + * @return this + */ + public final AssertSubscriber assertValueCount(long n) { + if (valueCount != n) { + throw new AssertionError( + "Different value count: expected = " + n + ", actual = " + valueCount, null); + } + return this; + } + + /** + * Assert the specified values have been received in the same order read by the passed {@link + * Iterable}. Values storage should be enabled to use this method. + * + * @param expectedSequence the values to assert + * @see #configureValuesStorage(boolean) + * @return this + */ + public final AssertSubscriber assertValueSequence(Iterable expectedSequence) { + if (!valuesStorage) { + throw new IllegalStateException("Using assertNoValues() requires enabling values storage"); + } + Iterator actual = values.iterator(); + Iterator expected = expectedSequence.iterator(); + int i = 0; + for (; ; ) { + boolean n1 = actual.hasNext(); + boolean n2 = expected.hasNext(); + if (n1 && n2) { + T t1 = actual.next(); + T t2 = expected.next(); + if (!Objects.equals(t1, t2)) { + throw new AssertionError( + "The element with index " + + i + + " does not match: expected = " + + valueAndClass(t2) + + ", actual = " + + valueAndClass(t1), + null); + } + i++; + } else if (n1 && !n2) { + throw new AssertionError("Actual contains more elements" + values, null); + } else if (!n1 && n2) { + throw new AssertionError("Actual contains fewer elements: " + values, null); + } else { + break; + } + } + return this; + } + + /** + * Assert the specified values have been received in the declared order. Values storage should be + * enabled to use this method. + * + * @param expectedValues the values to assert + * @return this + * @see #configureValuesStorage(boolean) + */ + @SafeVarargs + public final AssertSubscriber assertValues(T... expectedValues) { + return assertValueSequence(Arrays.asList(expectedValues)); + } + + /** + * Assert the specified values have been received in the declared order. Values storage should be + * enabled to use this method. + * + * @param expectations One or more methods that can verify the values and throw a exception (like + * an {@link AssertionError}) if the value is not valid. + * @return this + * @see #configureValuesStorage(boolean) + */ + @SafeVarargs + public final AssertSubscriber assertValuesWith(Consumer... expectations) { + if (!valuesStorage) { + throw new IllegalStateException("Using assertNoValues() requires enabling values storage"); + } + final int expectedValueCount = expectations.length; + if (expectedValueCount != values.size()) { + throw new AssertionError( + "Different value count: expected = " + expectedValueCount + ", actual = " + valueCount, + null); + } + for (int i = 0; i < expectedValueCount; i++) { + Consumer consumer = expectations[i]; + T actualValue = values.get(i); + consumer.accept(actualValue); + } + return this; + } + + // + // ============================================================================================================== + // Await methods + // + // ============================================================================================================== + + /** + * Blocking method that waits until a complete successfully or error signal is received. + * + * @return this + */ + public final AssertSubscriber await() { + if (cdl.getCount() == 0) { + return this; + } + try { + cdl.await(); + } catch (InterruptedException ex) { + throw new AssertionError("Wait interrupted", ex); + } + return this; + } + + /** + * Blocking method that waits until a complete successfully or error signal is received or until a + * timeout occurs. + * + * @param timeout The timeout value + * @return this + */ + public final AssertSubscriber await(Duration timeout) { + if (cdl.getCount() == 0) { + return this; + } + try { + if (!cdl.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) { + throw new AssertionError("No complete or error signal before timeout"); + } + return this; + } catch (InterruptedException ex) { + throw new AssertionError("Wait interrupted", ex); + } + } + + /** + * Blocking method that waits until {@code n} next values have been received. + * + * @param n the value count to assert + * @return this + */ + public final AssertSubscriber awaitAndAssertNextValueCount(final long n) { + await( + valuesTimeout, + () -> { + if (valuesStorage) { + return String.format( + "%d out of %d next values received within %d, " + "values : %s", + valueCount - nextValueAssertedCount, + n, + valuesTimeout.toMillis(), + values.toString()); + } + return String.format( + "%d out of %d next values received within %d", + valueCount - nextValueAssertedCount, n, valuesTimeout.toMillis()); + }, + () -> valueCount >= (nextValueAssertedCount + n)); + nextValueAssertedCount += n; + return this; + } + + /** + * Blocking method that waits until {@code n} next values have been received (n is the number of + * values provided) to assert them. + * + * @param values the values to assert + * @return this + */ + @SafeVarargs + @SuppressWarnings("unchecked") + public final AssertSubscriber awaitAndAssertNextValues(T... values) { + final int expectedNum = values.length; + final List> expectations = new ArrayList<>(); + for (int i = 0; i < expectedNum; i++) { + final T expectedValue = values[i]; + expectations.add( + actualValue -> { + if (!actualValue.equals(expectedValue)) { + throw new AssertionError( + String.format( + "Expected Next signal: %s, but got: %s", expectedValue, actualValue)); + } + }); + } + awaitAndAssertNextValuesWith(expectations.toArray((Consumer[]) new Consumer[0])); + return this; + } + + /** + * Blocking method that waits until {@code n} next values have been received (n is the number of + * expectations provided) to assert them. + * + * @param expectations One or more methods that can verify the values and throw a exception (like + * an {@link AssertionError}) if the value is not valid. + * @return this + */ + @SafeVarargs + public final AssertSubscriber awaitAndAssertNextValuesWith(Consumer... expectations) { + valuesStorage = true; + final int expectedValueCount = expectations.length; + await( + valuesTimeout, + () -> { + if (valuesStorage) { + return String.format( + "%d out of %d next values received within %d, " + "values : %s", + valueCount - nextValueAssertedCount, + expectedValueCount, + valuesTimeout.toMillis(), + values.toString()); + } + return String.format( + "%d out of %d next values received within %d ms", + valueCount - nextValueAssertedCount, expectedValueCount, valuesTimeout.toMillis()); + }, + () -> valueCount >= (nextValueAssertedCount + expectedValueCount)); + List nextValuesSnapshot; + List empty = new ArrayList<>(); + for (; ; ) { + nextValuesSnapshot = values; + if (NEXT_VALUES.compareAndSet(this, values, empty)) { + break; + } + } + if (nextValuesSnapshot.size() < expectedValueCount) { + throw new AssertionError( + String.format( + "Expected %d number of signals but received %d", + expectedValueCount, nextValuesSnapshot.size())); + } + for (int i = 0; i < expectedValueCount; i++) { + Consumer consumer = expectations[i]; + T actualValue = nextValuesSnapshot.get(i); + consumer.accept(actualValue); + } + nextValueAssertedCount += expectedValueCount; + return this; + } + + // + // ============================================================================================================== + // Overrides + // + // ============================================================================================================== + + @Override + public void cancel() { + Subscription a = s; + if (a != Operators.cancelledSubscription()) { + a = S.getAndSet(this, Operators.cancelledSubscription()); + if (a != null && a != Operators.cancelledSubscription()) { + a.cancel(); + + if (establishedFusionMode == Fuseable.ASYNC) { + final int previousState = markWorkAdded(); + if (!isWorkInProgress(previousState)) { + clearAndFinalize(); + } + } + } + } + } + + final boolean isCancelled() { + return s == Operators.cancelledSubscription(); + } + + public final boolean isTerminated() { + return cdl.getCount() == 0; + } + + @Override + public void onComplete() { + done = true; + completionCount++; + + if (establishedFusionMode == Fuseable.ASYNC) { + drain(); + return; + } + + cdl.countDown(); + } + + @Override + public void onError(Throwable t) { + done = true; + errors.add(t); + + if (establishedFusionMode == Fuseable.ASYNC) { + drain(); + return; + } + + cdl.countDown(); + } + + @Override + public void onNext(T t) { + if (establishedFusionMode == Fuseable.ASYNC) { + drain(); + } else { + valueCount++; + if (valuesStorage) { + List nextValuesSnapshot; + for (; ; ) { + nextValuesSnapshot = values; + nextValuesSnapshot.add(t); + if (NEXT_VALUES.compareAndSet(this, nextValuesSnapshot, nextValuesSnapshot)) { + break; + } + } + } + } + } + + 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; + } + + T t; + int m = 1; + for (; ; ) { + if (isCancelled()) { + clearAndFinalize(); + break; + } + boolean done = this.done; + t = qs.poll(); + if (t == null) { + if (done) { + clearAndFinalize(); + cdl.countDown(); + return; + } + m = WIP.addAndGet(this, -m); + if (m == 0) { + break; + } + continue; + } + valueCount++; + if (valuesStorage) { + List nextValuesSnapshot; + for (; ; ) { + nextValuesSnapshot = values; + nextValuesSnapshot.add(t); + if (NEXT_VALUES.compareAndSet(this, nextValuesSnapshot, nextValuesSnapshot)) { + break; + } + } + } + } + } + + @Override + @SuppressWarnings("unchecked") + public void onSubscribe(Subscription s) { + subscriptionCount++; + int requestMode = requestedFusionMode; + if (requestMode >= 0) { + if (s instanceof Fuseable.QueueSubscription) { + this.qs = (Fuseable.QueueSubscription) s; + + int m = qs.requestFusion(requestMode); + establishedFusionMode = m; + + if (!setWithoutRequesting(s)) { + qs.clear(); + if (!isCancelled()) { + errors.add(new IllegalStateException("Subscription already set: " + subscriptionCount)); + } + return; + } + + if (m == Fuseable.SYNC) { + for (; ; ) { + T v = qs.poll(); + if (v == null) { + onComplete(); + break; + } + + onNext(v); + } + } else { + requestDeferred(); + } + + return; + } + } + + if (!set(s)) { + if (!isCancelled()) { + errors.add(new IllegalStateException("Subscription already set: " + subscriptionCount)); + } + } + } + + @Override + public void request(long n) { + if (Operators.validate(n)) { + if (establishedFusionMode != Fuseable.SYNC) { + normalRequest(n); + } + } + } + + @Override + @NonNull + public Context currentContext() { + return context; + } + + /** + * Setup what fusion mode should be requested from the incoming Subscription if it happens to be + * QueueSubscription + * + * @param requestMode the mode to request, see Fuseable constants + * @return this + */ + public final AssertSubscriber requestedFusionMode(int requestMode) { + this.requestedFusionMode = requestMode; + return this; + } + + public Subscription upstream() { + return s; + } + + // + // ============================================================================================================== + // Non public methods + // + // ============================================================================================================== + + protected final void normalRequest(long n) { + Subscription a = s; + if (a != null) { + a.request(n); + } else { + Operators.addCap(REQUESTED, this, n); + + a = s; + + if (a != null) { + long r = REQUESTED.getAndSet(this, 0L); + + if (r != 0L) { + a.request(r); + } + } + } + } + + /** Requests the deferred amount if not zero. */ + protected final void requestDeferred() { + long r = REQUESTED.getAndSet(this, 0L); + + if (r != 0L) { + s.request(r); + } + } + + /** + * Atomically sets the single subscription and requests the missed amount from it. + * + * @param s + * @return false if this arbiter is cancelled or there was a subscription already set + */ + protected final boolean set(Subscription s) { + Objects.requireNonNull(s, "s"); + Subscription a = this.s; + if (a == Operators.cancelledSubscription()) { + s.cancel(); + return false; + } + if (a != null) { + s.cancel(); + Operators.reportSubscriptionSet(); + return false; + } + + if (S.compareAndSet(this, null, s)) { + + long r = REQUESTED.getAndSet(this, 0L); + + if (r != 0L) { + s.request(r); + } + + return true; + } + + a = this.s; + + if (a != Operators.cancelledSubscription()) { + s.cancel(); + return false; + } + + Operators.reportSubscriptionSet(); + return false; + } + + /** + * Sets the Subscription once but does not request anything. + * + * @param s the Subscription to set + * @return true if successful, false if the current subscription is not null + */ + protected final boolean setWithoutRequesting(Subscription s) { + Objects.requireNonNull(s, "s"); + for (; ; ) { + Subscription a = this.s; + if (a == Operators.cancelledSubscription()) { + s.cancel(); + return false; + } + if (a != null) { + s.cancel(); + Operators.reportSubscriptionSet(); + return false; + } + + if (S.compareAndSet(this, null, s)) { + return true; + } + } + } + + /** + * Prepares and throws an AssertionError exception based on the message, cause, the active state + * and the potential errors so far. + * + * @param message the message + * @param cause the optional Throwable cause + * @throws AssertionError as expected + */ + protected final void assertionError(String message, Throwable cause) { + StringBuilder b = new StringBuilder(); + + if (cdl.getCount() != 0) { + b.append("(active) "); + } + b.append(message); + + List err = errors; + if (!err.isEmpty()) { + b.append(" (+ ").append(err.size()).append(" errors)"); + } + AssertionError e = new AssertionError(b.toString(), cause); + + for (Throwable t : err) { + e.addSuppressed(t); + } + + throw e; + } + + protected final String fusionModeName(int mode) { + switch (mode) { + case -1: + return "Disabled"; + case Fuseable.NONE: + return "None"; + case Fuseable.SYNC: + return "Sync"; + case Fuseable.ASYNC: + return "Async"; + default: + return "Unknown(" + mode + ")"; + } + } + + protected final String valueAndClass(Object o) { + if (o == null) { + return null; + } + return o + " (" + o.getClass().getSimpleName() + ")"; + } + + public List values() { + return values; + } + + public final AssertSubscriber assertNoEvents() { + return assertNoValues().assertNoError().assertNotComplete(); + } + + @SafeVarargs + public final AssertSubscriber assertIncomplete(T... values) { + return assertValues(values).assertNotComplete().assertNoError(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java b/rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java new file mode 100644 index 000000000..d5b2eeb41 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.lease; + +import static org.junit.Assert.*; + +import io.netty.buffer.Unpooled; +import java.time.Duration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +public class LeaseImplTest { + + @Test + public void emptyLeaseNoAvailability() { + LeaseImpl empty = LeaseImpl.empty(); + Assertions.assertTrue(empty.isEmpty()); + Assertions.assertFalse(empty.isValid()); + Assertions.assertEquals(0.0, empty.availability(), 1e-5); + } + + @Test + public void emptyLeaseUseNoAvailability() { + LeaseImpl empty = LeaseImpl.empty(); + boolean success = empty.use(); + assertFalse(success); + Assertions.assertEquals(0.0, empty.availability(), 1e-5); + } + + @Test + public void leaseAvailability() { + LeaseImpl lease = LeaseImpl.create(2, 100, Unpooled.EMPTY_BUFFER); + Assertions.assertEquals(1.0, lease.availability(), 1e-5); + } + + @Test + public void leaseUseDecreasesAvailability() { + LeaseImpl lease = LeaseImpl.create(30_000, 2, Unpooled.EMPTY_BUFFER); + boolean success = lease.use(); + Assertions.assertTrue(success); + Assertions.assertEquals(0.5, lease.availability(), 1e-5); + Assertions.assertTrue(lease.isValid()); + success = lease.use(); + Assertions.assertTrue(success); + Assertions.assertEquals(0.0, lease.availability(), 1e-5); + Assertions.assertFalse(lease.isValid()); + Assertions.assertEquals(0, lease.getAllowedRequests()); + success = lease.use(); + Assertions.assertFalse(success); + } + + @Test + public void leaseTimeout() { + int numberOfRequests = 1; + LeaseImpl lease = LeaseImpl.create(1, numberOfRequests, Unpooled.EMPTY_BUFFER); + Mono.delay(Duration.ofMillis(100)).block(); + boolean success = lease.use(); + Assertions.assertFalse(success); + Assertions.assertTrue(lease.isExpired()); + Assertions.assertEquals(numberOfRequests, lease.getAllowedRequests()); + Assertions.assertFalse(lease.isValid()); + } + + @Test + public void useLeaseChangesAllowedRequests() { + int numberOfRequests = 2; + LeaseImpl lease = LeaseImpl.create(30_000, numberOfRequests, Unpooled.EMPTY_BUFFER); + lease.use(); + assertEquals(numberOfRequests - 1, lease.getAllowedRequests()); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java new file mode 100644 index 000000000..bd5e4295a --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java @@ -0,0 +1,554 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.metadata; + +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeAndContentBuffersSlices; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeIdFromMimeBuffer; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer; +import static org.assertj.core.api.Assertions.*; + +import io.netty.buffer.*; +import io.netty.util.CharsetUtil; +import io.rsocket.test.util.ByteBufUtils; +import io.rsocket.util.NumberUtils; +import org.junit.jupiter.api.Test; + +class CompositeMetadataFlyweightTest { + + static String byteToBitsString(byte b) { + return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'); + } + + static String toHeaderBits(ByteBuf encoded) { + encoded.markReaderIndex(); + byte headerByte = encoded.readByte(); + String byteAsString = byteToBitsString(headerByte); + encoded.resetReaderIndex(); + return byteAsString; + } + // ==== + + @Test + void customMimeHeaderLatin1_encodingFails() { + String mimeNotAscii = "mime/typé"; + + assertThatIllegalArgumentException() + .isThrownBy( + () -> + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + .withMessage("custom mime type must be US_ASCII characters only"); + } + + @Test + void customMimeHeaderLength0_encodingFails() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "", 0)) + .withMessage( + "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + @Test + void customMimeHeaderLength127() { + StringBuilder builder = new StringBuilder(127); + for (int i = 0; i < 127; i++) { + builder.append('a'); + } + String mimeString = builder.toString(); + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + + // remember actual length = encoded length + 1 + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111110"); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isGreaterThan(1); + + assertThat((int) header.readByte()) + .as("mime length") + .isEqualTo(127 - 1); // encoded as actual length - 1 + + assertThat(header.readCharSequence(127, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); + + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void customMimeHeaderLength128() { + StringBuilder builder = new StringBuilder(128); + for (int i = 0; i < 128; i++) { + builder.append('a'); + } + String mimeString = builder.toString(); + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + + // remember actual length = encoded length + 1 + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111111"); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isGreaterThan(1); + + assertThat((int) header.readByte()) + .as("mime length") + .isEqualTo(128 - 1); // encoded as actual length - 1 + + assertThat(header.readCharSequence(128, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); + + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void customMimeHeaderLength129_encodingFails() { + StringBuilder builder = new StringBuilder(129); + for (int i = 0; i < 129; i++) { + builder.append('a'); + } + + assertThatIllegalArgumentException() + .isThrownBy( + () -> + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, builder.toString(), 0)) + .withMessage( + "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + @Test + void customMimeHeaderLengthOne() { + String mimeString = "w"; + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + + // remember actual length = encoded length + 1 + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000000"); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isGreaterThan(1); + + assertThat((int) header.readByte()).as("mime length").isZero(); // encoded as actual length - 1 + + assertThat(header.readCharSequence(1, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); + + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void customMimeHeaderLengthTwo() { + String mimeString = "ww"; + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + + // remember actual length = encoded length + 1 + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000001"); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isGreaterThan(1); + + assertThat((int) header.readByte()) + .as("mime length") + .isEqualTo(2 - 1); // encoded as actual length - 1 + + assertThat(header.readCharSequence(2, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); + + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void customMimeHeaderUtf8_encodingFails() { + String mimeNotAscii = + "mime/tyࠒe"; // this is the SAMARITAN LETTER QUF u+0812 represented on 3 bytes + assertThatIllegalArgumentException() + .isThrownBy( + () -> + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + .withMessage("custom mime type must be US_ASCII characters only"); + } + + @Test + void decodeEntryAtEndOfBuffer() { + ByteBuf fakeEntry = Unpooled.buffer(); + + assertThatIllegalArgumentException() + .isThrownBy(() -> decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)); + } + + @Test + void decodeEntryHasNoContentLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(0); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)); + } + + @Test + void decodeEntryTooShortForContentLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(1); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + NumberUtils.encodeUnsignedMedium(fakeEntry, 456); + fakeEntry.writeChar('w'); + + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)); + } + + @Test + void decodeEntryTooShortForMimeLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(120); + + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)); + } + + @Test + void decodeIdMinusTwoWhenMoreThanOneByte() { + ByteBuf fakeIdBuffer = Unpooled.buffer(2); + fakeIdBuffer.writeInt(200); + + assertThat(decodeMimeIdFromMimeBuffer(fakeIdBuffer)) + .isEqualTo((WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier())); + } + + @Test + void decodeIdMinusTwoWhenZeroByte() { + ByteBuf fakeIdBuffer = Unpooled.buffer(0); + + assertThat(decodeMimeIdFromMimeBuffer(fakeIdBuffer)) + .isEqualTo((WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier())); + } + + @Test + void decodeStringNullIfLengthOne() { + ByteBuf fakeTypeBuffer = Unpooled.buffer(2); + fakeTypeBuffer.writeByte(1); + + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)); + } + + @Test + void decodeStringNullIfLengthZero() { + ByteBuf fakeTypeBuffer = Unpooled.buffer(2); + + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)); + } + + @Test + void decodeTypeSkipsFirstByte() { + ByteBuf fakeTypeBuffer = Unpooled.buffer(2); + fakeTypeBuffer.writeByte(128); + fakeTypeBuffer.writeCharSequence("example", CharsetUtil.US_ASCII); + + assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)).hasToString("example"); + } + + @Test + void encodeMetadataCustomTypeDelegates() { + ByteBuf expected = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo", 2); + + CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadata( + test, ByteBufAllocator.DEFAULT, "foo", ByteBufUtils.getRandomByteBuf(2)); + + assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + } + + @Test + void encodeMetadataKnownTypeDelegates() { + ByteBuf expected = + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, + WellKnownMimeType.APPLICATION_OCTET_STREAM.getIdentifier(), + 2); + + CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadata( + test, + ByteBufAllocator.DEFAULT, + WellKnownMimeType.APPLICATION_OCTET_STREAM, + ByteBufUtils.getRandomByteBuf(2)); + + assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + } + + @Test + void encodeMetadataReservedTypeDelegates() { + ByteBuf expected = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, (byte) 120, 2); + + CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadata( + test, ByteBufAllocator.DEFAULT, (byte) 120, ByteBufUtils.getRandomByteBuf(2)); + + assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + } + + @Test + void encodeTryCompressWithCompressableType() { + ByteBuf metadata = ByteBufUtils.getRandomByteBuf(2); + CompositeByteBuf target = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadataWithCompression( + target, + UnpooledByteBufAllocator.DEFAULT, + WellKnownMimeType.APPLICATION_AVRO.getString(), + metadata); + + assertThat(target.readableBytes()).as("readableBytes 1 + 3 + 2").isEqualTo(6); + } + + @Test + void encodeTryCompressWithCustomType() { + ByteBuf metadata = ByteBufUtils.getRandomByteBuf(2); + CompositeByteBuf target = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadataWithCompression( + target, UnpooledByteBufAllocator.DEFAULT, "custom/example", metadata); + + assertThat(target.readableBytes()).as("readableBytes 1 + 14 + 3 + 2").isEqualTo(20); + } + + @Test + void hasEntry() { + WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; + + CompositeByteBuf buffer = + Unpooled.compositeBuffer() + .addComponent( + true, + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0)) + .addComponent( + true, + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0)); + + assertThat(CompositeMetadataFlyweight.hasEntry(buffer, 0)).isTrue(); + assertThat(CompositeMetadataFlyweight.hasEntry(buffer, 4)).isTrue(); + assertThat(CompositeMetadataFlyweight.hasEntry(buffer, 8)).isFalse(); + } + + @Test + void isWellKnownMimeType() { + ByteBuf wellKnown = Unpooled.buffer().writeByte(0); + assertThat(CompositeMetadataFlyweight.isWellKnownMimeType(wellKnown)).isTrue(); + + ByteBuf explicit = Unpooled.buffer().writeByte(2).writeChar('a'); + assertThat(CompositeMetadataFlyweight.isWellKnownMimeType(explicit)).isFalse(); + } + + @Test + void knownMimeHeader120_reserved() { + byte mime = (byte) 120; + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); + + assertThat(mime) + .as("smoke test RESERVED_120 unsigned 7 bits representation") + .isEqualTo((byte) 0b01111000); + + assertThat(toHeaderBits(encoded)).startsWith("1").isEqualTo("11111000"); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isOne(); + + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("11111000"); + + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)).as("decoded mime id").isEqualTo(mime); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void knownMimeHeader127_compositeMetadata() { + WellKnownMimeType mime = WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA; + assertThat(mime.getIdentifier()) + .as("smoke test COMPOSITE unsigned 7 bits representation") + .isEqualTo((byte) 127) + .isEqualTo((byte) 0b01111111); + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); + + assertThat(toHeaderBits(encoded)) + .startsWith("1") + .isEqualTo("11111111") + .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isOne(); + + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("11111111"); + + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)) + .as("decoded mime id") + .isEqualTo(mime.getIdentifier()); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void knownMimeHeaderZero_avro() { + WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; + assertThat(mime.getIdentifier()) + .as("smoke test AVRO unsigned 7 bits representation") + .isEqualTo((byte) 0) + .isEqualTo((byte) 0b00000000); + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); + + assertThat(toHeaderBits(encoded)) + .startsWith("1") + .isEqualTo("10000000") + .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isOne(); + + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("10000000"); + + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)) + .as("decoded mime id") + .isEqualTo(mime.getIdentifier()); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void encodeCustomHeaderAsciiCheckSkipsFirstByte() { + final ByteBuf badBuf = Unpooled.copiedBuffer("é00000000000", CharsetUtil.UTF_8); + badBuf.writerIndex(0); + assertThat(badBuf.readerIndex()).isZero(); + + ByteBufAllocator allocator = + new AbstractByteBufAllocator() { + @Override + public boolean isDirectBufferPooled() { + return false; + } + + @Override + protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) { + return badBuf; + } + + @Override + protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) { + return badBuf; + } + }; + + assertThatCode( + () -> CompositeMetadataFlyweight.encodeMetadataHeader(allocator, "custom/type", 0)) + .doesNotThrowAnyException(); + + assertThat(badBuf.readByte()).isEqualTo((byte) 10); + assertThat(badBuf.readCharSequence(11, CharsetUtil.UTF_8)).hasToString("custom/type"); + assertThat(badBuf.readUnsignedMedium()).isEqualTo(0); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java new file mode 100644 index 000000000..f06bdcc0c --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java @@ -0,0 +1,178 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.metadata; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.rsocket.metadata.CompositeMetadata.Entry; +import io.rsocket.metadata.CompositeMetadata.ReservedMimeTypeEntry; +import io.rsocket.metadata.CompositeMetadata.WellKnownMimeTypeEntry; +import io.rsocket.test.util.ByteBufUtils; +import io.rsocket.util.NumberUtils; +import java.util.Iterator; +import java.util.Spliterator; +import org.junit.jupiter.api.Test; + +class CompositeMetadataTest { + + @Test + void decodeEntryHasNoContentLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(0); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + CompositeMetadata compositeMetadata = new CompositeMetadata(fakeEntry, false); + + assertThatIllegalStateException() + .isThrownBy(() -> compositeMetadata.iterator().next()) + .withMessage("metadata is malformed"); + } + + @Test + void decodeEntryOnDoneBufferThrowsIllegalArgument() { + ByteBuf fakeBuffer = ByteBufUtils.getRandomByteBuf(0); + CompositeMetadata compositeMetadata = new CompositeMetadata(fakeBuffer, false); + + assertThatIllegalArgumentException() + .isThrownBy(() -> compositeMetadata.iterator().next()) + .withMessage("entry index 0 is larger than buffer size"); + } + + @Test + void decodeEntryTooShortForContentLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(1); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + NumberUtils.encodeUnsignedMedium(fakeEntry, 456); + fakeEntry.writeChar('w'); + CompositeMetadata compositeMetadata = new CompositeMetadata(fakeEntry, false); + + assertThatIllegalStateException() + .isThrownBy(() -> compositeMetadata.iterator().next()) + .withMessage("metadata is malformed"); + } + + @Test + void decodeEntryTooShortForMimeLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(120); + CompositeMetadata compositeMetadata = new CompositeMetadata(fakeEntry, false); + + assertThatIllegalStateException() + .isThrownBy(() -> compositeMetadata.iterator().next()) + .withMessage("metadata is malformed"); + } + + @Test + void decodeThreeEntries() { + // metadata 1: well known + WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; + ByteBuf metadata1 = Unpooled.buffer(); + metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + + // metadata 2: custom + String mimeType2 = "application/custom"; + ByteBuf metadata2 = Unpooled.buffer(); + metadata2.writeChar('E'); + metadata2.writeChar('∑'); + metadata2.writeChar('é'); + metadata2.writeBoolean(true); + metadata2.writeChar('W'); + + // metadata 3: reserved but unknown + byte reserved = 120; + assertThat(WellKnownMimeType.fromIdentifier(reserved)) + .as("ensure UNKNOWN RESERVED used in test") + .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); + ByteBuf metadata3 = Unpooled.buffer(); + metadata3.writeByte(88); + + CompositeByteBuf compositeMetadataBuffer = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadataBuffer, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadataBuffer, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadataBuffer, ByteBufAllocator.DEFAULT, reserved, metadata3); + + Iterator iterator = new CompositeMetadata(compositeMetadataBuffer, true).iterator(); + + assertThat(iterator.next()) + .as("entry1") + .isNotNull() + .satisfies( + e -> + assertThat(e.getMimeType()).as("entry1 mime type").isEqualTo(mimeType1.getString())) + .satisfies( + e -> + assertThat(((WellKnownMimeTypeEntry) e).getType()) + .as("entry1 mime id") + .isEqualTo(WellKnownMimeType.APPLICATION_PDF)) + .satisfies( + e -> + assertThat(e.getContent().toString(CharsetUtil.UTF_8)) + .as("entry1 decoded") + .isEqualTo("abcdefghijkl")); + + assertThat(iterator.next()) + .as("entry2") + .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()).as("entry2 mime type").isEqualTo(mimeType2)) + .satisfies( + e -> assertThat(e.getContent()).as("entry2 decoded").isEqualByComparingTo(metadata2)); + + assertThat(iterator.next()) + .as("entry3") + .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()).as("entry3 mime type").isNull()) + .satisfies( + e -> + assertThat(((ReservedMimeTypeEntry) e).getType()) + .as("entry3 mime id") + .isEqualTo(reserved)) + .satisfies( + e -> assertThat(e.getContent()).as("entry3 decoded").isEqualByComparingTo(metadata3)); + + assertThat(iterator.hasNext()).as("has no more than 3 entries").isFalse(); + } + + @Test + void streamIsNotParallel() { + final CompositeMetadata metadata = + new CompositeMetadata(ByteBufUtils.getRandomByteBuf(5), false); + + assertThat(metadata.stream().isParallel()).as("isParallel").isFalse(); + } + + @Test + void streamSpliteratorCharacteristics() { + final CompositeMetadata metadata = + new CompositeMetadata(ByteBufUtils.getRandomByteBuf(5), false); + + assertThat(metadata.stream().spliterator()) + .matches(s -> s.hasCharacteristics(Spliterator.ORDERED), "ORDERED") + .matches(s -> s.hasCharacteristics(Spliterator.DISTINCT), "DISTINCT") + .matches(s -> s.hasCharacteristics(Spliterator.NONNULL), "NONNULL") + .matches(s -> !s.hasCharacteristics(Spliterator.SIZED), "not SIZED"); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/TaggingMetadataTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/TaggingMetadataTest.java new file mode 100644 index 000000000..d1fbb50b0 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/TaggingMetadataTest.java @@ -0,0 +1,47 @@ +package io.rsocket.metadata; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.netty.buffer.ByteBufAllocator; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +/** + * Tagging metadata test + * + * @author linux_china + */ +public class TaggingMetadataTest { + private ByteBufAllocator byteBufAllocator = ByteBufAllocator.DEFAULT; + + @Test + public void testParseTags() { + List tags = + Arrays.asList( + "ws://localhost:8080/rsocket", String.join("", Collections.nCopies(129, "x"))); + TaggingMetadata taggingMetadata = + TaggingMetadataFlyweight.createTaggingMetadata( + byteBufAllocator, "message/x.rsocket.routing.v0", tags); + TaggingMetadata taggingMetadataCopy = + new TaggingMetadata("message/x.rsocket.routing.v0", taggingMetadata.getContent()); + assertThat(tags) + .containsExactlyElementsOf(taggingMetadataCopy.stream().collect(Collectors.toList())); + } + + @Test + public void testEmptyTagAndOverLengthTag() { + List tags = + Arrays.asList( + "ws://localhost:8080/rsocket", "", String.join("", Collections.nCopies(256, "x"))); + TaggingMetadata taggingMetadata = + TaggingMetadataFlyweight.createTaggingMetadata( + byteBufAllocator, "message/x.rsocket.routing.v0", tags); + TaggingMetadata taggingMetadataCopy = + new TaggingMetadata("message/x.rsocket.routing.v0", taggingMetadata.getContent()); + assertThat(tags.subList(0, 1)) + .containsExactlyElementsOf(taggingMetadataCopy.stream().collect(Collectors.toList())); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/TracingMetadataCodecTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/TracingMetadataCodecTest.java new file mode 100644 index 000000000..cb8478c13 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/TracingMetadataCodecTest.java @@ -0,0 +1,209 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.ReferenceCounted; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class TracingMetadataCodecTest { + + private static Stream flags() { + return Stream.of(TracingMetadataCodec.Flags.values()); + } + + @ParameterizedTest + @MethodSource("flags") + public void shouldEncodeEmptyTrace(TracingMetadataCodec.Flags expectedFlag) { + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + ByteBuf byteBuf = TracingMetadataCodec.encodeEmpty(allocator, expectedFlag); + + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(byteBuf); + + Assertions.assertThat(tracingMetadata) + .matches(TracingMetadata::isEmpty) + .matches( + tm -> { + switch (expectedFlag) { + case UNDECIDED: + return !tm.isDecided(); + case NOT_SAMPLE: + return tm.isDecided() && !tm.isSampled(); + case SAMPLE: + return tm.isDecided() && tm.isSampled(); + case DEBUG: + return tm.isDecided() && tm.isDebug(); + } + return false; + }); + Assertions.assertThat(byteBuf).matches(ReferenceCounted::release); + allocator.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("flags") + public void shouldEncodeTrace64WithParent(TracingMetadataCodec.Flags expectedFlag) { + long traceId = ThreadLocalRandom.current().nextLong(); + long spanId = ThreadLocalRandom.current().nextLong(); + long parentId = ThreadLocalRandom.current().nextLong(); + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + ByteBuf byteBuf = + TracingMetadataCodec.encode64(allocator, traceId, spanId, parentId, expectedFlag); + + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(byteBuf); + + Assertions.assertThat(tracingMetadata) + .matches(metadata -> !metadata.isEmpty()) + .matches(tm -> tm.traceIdHigh() == 0) + .matches(tm -> tm.traceId() == traceId) + .matches(tm -> tm.spanId() == spanId) + .matches(tm -> tm.hasParent()) + .matches(tm -> tm.parentId() == parentId) + .matches( + tm -> { + switch (expectedFlag) { + case UNDECIDED: + return !tm.isDecided(); + case NOT_SAMPLE: + return tm.isDecided() && !tm.isSampled(); + case SAMPLE: + return tm.isDecided() && tm.isSampled(); + case DEBUG: + return tm.isDecided() && tm.isDebug(); + } + return false; + }); + Assertions.assertThat(byteBuf).matches(ReferenceCounted::release); + allocator.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("flags") + public void shouldEncodeTrace64(TracingMetadataCodec.Flags expectedFlag) { + long traceId = ThreadLocalRandom.current().nextLong(); + long spanId = ThreadLocalRandom.current().nextLong(); + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + ByteBuf byteBuf = TracingMetadataCodec.encode64(allocator, traceId, spanId, expectedFlag); + + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(byteBuf); + + Assertions.assertThat(tracingMetadata) + .matches(metadata -> !metadata.isEmpty()) + .matches(tm -> tm.traceIdHigh() == 0) + .matches(tm -> tm.traceId() == traceId) + .matches(tm -> tm.spanId() == spanId) + .matches(tm -> !tm.hasParent()) + .matches(tm -> tm.parentId() == 0) + .matches( + tm -> { + switch (expectedFlag) { + case UNDECIDED: + return !tm.isDecided(); + case NOT_SAMPLE: + return tm.isDecided() && !tm.isSampled(); + case SAMPLE: + return tm.isDecided() && tm.isSampled(); + case DEBUG: + return tm.isDecided() && tm.isDebug(); + } + return false; + }); + Assertions.assertThat(byteBuf).matches(ReferenceCounted::release); + allocator.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("flags") + public void shouldEncodeTrace128WithParent(TracingMetadataCodec.Flags expectedFlag) { + long traceIdHighLocal; + do { + traceIdHighLocal = ThreadLocalRandom.current().nextLong(); + + } while (traceIdHighLocal == 0); + long traceIdHigh = traceIdHighLocal; + long traceId = ThreadLocalRandom.current().nextLong(); + long spanId = ThreadLocalRandom.current().nextLong(); + long parentId = ThreadLocalRandom.current().nextLong(); + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + ByteBuf byteBuf = + TracingMetadataCodec.encode128( + allocator, traceIdHigh, traceId, spanId, parentId, expectedFlag); + + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(byteBuf); + + Assertions.assertThat(tracingMetadata) + .matches(metadata -> !metadata.isEmpty()) + .matches(tm -> tm.traceIdHigh() == traceIdHigh) + .matches(tm -> tm.traceId() == traceId) + .matches(tm -> tm.spanId() == spanId) + .matches(tm -> tm.hasParent()) + .matches(tm -> tm.parentId() == parentId) + .matches( + tm -> { + switch (expectedFlag) { + case UNDECIDED: + return !tm.isDecided(); + case NOT_SAMPLE: + return tm.isDecided() && !tm.isSampled(); + case SAMPLE: + return tm.isDecided() && tm.isSampled(); + case DEBUG: + return tm.isDecided() && tm.isDebug(); + } + return false; + }); + Assertions.assertThat(byteBuf).matches(ReferenceCounted::release); + allocator.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("flags") + public void shouldEncodeTrace128(TracingMetadataCodec.Flags expectedFlag) { + long traceIdHighLocal; + do { + traceIdHighLocal = ThreadLocalRandom.current().nextLong(); + + } while (traceIdHighLocal == 0); + long traceIdHigh = traceIdHighLocal; + long traceId = ThreadLocalRandom.current().nextLong(); + long spanId = ThreadLocalRandom.current().nextLong(); + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + ByteBuf byteBuf = + TracingMetadataCodec.encode128(allocator, traceIdHigh, traceId, spanId, expectedFlag); + + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(byteBuf); + + Assertions.assertThat(tracingMetadata) + .matches(metadata -> !metadata.isEmpty()) + .matches(tm -> tm.traceIdHigh() == traceIdHigh) + .matches(tm -> tm.traceId() == traceId) + .matches(tm -> tm.spanId() == spanId) + .matches(tm -> !tm.hasParent()) + .matches(tm -> tm.parentId() == 0) + .matches( + tm -> { + switch (expectedFlag) { + case UNDECIDED: + return !tm.isDecided(); + case NOT_SAMPLE: + return tm.isDecided() && !tm.isSampled(); + case SAMPLE: + return tm.isDecided() && tm.isSampled(); + case DEBUG: + return tm.isDecided() && tm.isDebug(); + } + return false; + }); + Assertions.assertThat(byteBuf).matches(ReferenceCounted::release); + allocator.assertHasNoLeaks(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java new file mode 100644 index 000000000..316aaf091 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.metadata; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class WellKnownMimeTypeTest { + + @Test + void fromIdentifierGreaterThan127() { + assertThat(WellKnownMimeType.fromIdentifier(128)) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + } + + @Test + void fromIdentifierMatchFromMimeType() { + for (WellKnownMimeType mimeType : WellKnownMimeType.values()) { + if (mimeType == WellKnownMimeType.UNPARSEABLE_MIME_TYPE + || mimeType == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { + continue; + } + assertThat(WellKnownMimeType.fromString(mimeType.toString())) + .as("mimeType string for " + mimeType.name()) + .isSameAs(mimeType); + + assertThat(WellKnownMimeType.fromIdentifier(mimeType.getIdentifier())) + .as("mimeType ID for " + mimeType.name()) + .isSameAs(mimeType); + } + } + + @Test + void fromIdentifierNegative() { + assertThat(WellKnownMimeType.fromIdentifier(-1)) + .isSameAs(WellKnownMimeType.fromIdentifier(-2)) + .isSameAs(WellKnownMimeType.fromIdentifier(-12)) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + } + + @Test + void fromIdentifierReserved() { + assertThat(WellKnownMimeType.fromIdentifier(120)) + .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); + } + + @Test + void fromStringUnknown() { + assertThat(WellKnownMimeType.fromString("foo/bar")) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + } + + @Test + void fromStringUnknownReservedStillReturnsUnparseable() { + assertThat( + WellKnownMimeType.fromString(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE.getString())) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/security/AuthMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/security/AuthMetadataFlyweightTest.java new file mode 100644 index 000000000..93d0f8b12 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/security/AuthMetadataFlyweightTest.java @@ -0,0 +1,476 @@ +package io.rsocket.metadata.security; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCountUtil; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class AuthMetadataFlyweightTest { + + public static final int AUTH_TYPE_ID_LENGTH = 1; + public static final int USER_NAME_BYTES_LENGTH = 2; + public static final String TEST_BEARER_TOKEN = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJpYXQxIjoxNTE2MjM5MDIyLCJpYXQyIjoxNTE2MjM5MDIyLCJpYXQzIjoxNTE2MjM5MDIyLCJpYXQ0IjoxNTE2MjM5MDIyfQ.ljYuH-GNyyhhLcx-rHMchRkGbNsR2_4aSxo8XjrYrSM"; + + @Test + void shouldCorrectlyEncodeData() { + String username = "test"; + String password = "tset1234"; + + int usernameLength = username.length(); + int passwordLength = password.length(); + + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeSimpleMetadata( + ByteBufAllocator.DEFAULT, username.toCharArray(), password.toCharArray()); + + byteBuf.markReaderIndex(); + checkSimpleAuthMetadataEncoding( + username, password, usernameLength, passwordLength, byteBuf.retain()); + byteBuf.resetReaderIndex(); + checkSimpleAuthMetadataEncodingUsingDecoders( + username, password, usernameLength, passwordLength, byteBuf); + } + + @Test + void shouldCorrectlyEncodeData1() { + String username = "𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎"; + String password = "tset1234"; + + int usernameLength = username.getBytes(CharsetUtil.UTF_8).length; + int passwordLength = password.length(); + + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeSimpleMetadata( + ByteBufAllocator.DEFAULT, username.toCharArray(), password.toCharArray()); + + byteBuf.markReaderIndex(); + checkSimpleAuthMetadataEncoding( + username, password, usernameLength, passwordLength, byteBuf.retain()); + byteBuf.resetReaderIndex(); + checkSimpleAuthMetadataEncodingUsingDecoders( + username, password, usernameLength, passwordLength, byteBuf); + } + + @Test + void shouldCorrectlyEncodeData2() { + String username = "𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎1234567#4? "; + String password = "tset1234"; + + int usernameLength = username.getBytes(CharsetUtil.UTF_8).length; + int passwordLength = password.length(); + + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeSimpleMetadata( + ByteBufAllocator.DEFAULT, username.toCharArray(), password.toCharArray()); + + byteBuf.markReaderIndex(); + checkSimpleAuthMetadataEncoding( + username, password, usernameLength, passwordLength, byteBuf.retain()); + byteBuf.resetReaderIndex(); + checkSimpleAuthMetadataEncodingUsingDecoders( + username, password, usernameLength, passwordLength, byteBuf); + } + + private static void checkSimpleAuthMetadataEncoding( + String username, String password, int usernameLength, int passwordLength, ByteBuf byteBuf) { + Assertions.assertThat(byteBuf.capacity()) + .isEqualTo(AUTH_TYPE_ID_LENGTH + USER_NAME_BYTES_LENGTH + usernameLength + passwordLength); + + Assertions.assertThat(byteBuf.readUnsignedByte() & ~0x80) + .isEqualTo(WellKnownAuthType.SIMPLE.getIdentifier()); + Assertions.assertThat(byteBuf.readUnsignedShort()).isEqualTo((short) usernameLength); + + Assertions.assertThat(byteBuf.readCharSequence(usernameLength, CharsetUtil.UTF_8)) + .isEqualTo(username); + Assertions.assertThat(byteBuf.readCharSequence(passwordLength, CharsetUtil.UTF_8)) + .isEqualTo(password); + + ReferenceCountUtil.release(byteBuf); + } + + private static void checkSimpleAuthMetadataEncodingUsingDecoders( + String username, String password, int usernameLength, int passwordLength, ByteBuf byteBuf) { + Assertions.assertThat(byteBuf.capacity()) + .isEqualTo(AUTH_TYPE_ID_LENGTH + USER_NAME_BYTES_LENGTH + usernameLength + passwordLength); + + Assertions.assertThat(AuthMetadataFlyweight.decodeWellKnownAuthType(byteBuf)) + .isEqualTo(WellKnownAuthType.SIMPLE); + byteBuf.markReaderIndex(); + Assertions.assertThat(AuthMetadataFlyweight.decodeUsername(byteBuf).toString(CharsetUtil.UTF_8)) + .isEqualTo(username); + Assertions.assertThat(AuthMetadataFlyweight.decodePassword(byteBuf).toString(CharsetUtil.UTF_8)) + .isEqualTo(password); + byteBuf.resetReaderIndex(); + + Assertions.assertThat(new String(AuthMetadataFlyweight.decodeUsernameAsCharArray(byteBuf))) + .isEqualTo(username); + Assertions.assertThat(new String(AuthMetadataFlyweight.decodePasswordAsCharArray(byteBuf))) + .isEqualTo(password); + + ReferenceCountUtil.release(byteBuf); + } + + @Test + void shouldThrowExceptionIfUsernameLengthExitsAllowedBounds() { + StringBuilder usernameBuilder = new StringBuilder(); + String usernamePart = + "𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎𠸏𠹷𠺝𠺢𠻗𠻹𠻺𠼭𠼮𠽌𠾴𠾼𠿪𡁜𡁯𡁵𡁶𡁻𡃁𡃉𡇙𢃇𢞵𢫕𢭃𢯊𢱑𢱕𢳂𢴈𢵌𢵧𢺳𣲷𤓓𤶸𤷪𥄫𦉘𦟌𦧲𦧺𧨾𨅝𨈇𨋢𨳊𨳍𨳒𩶘𠜎𠜱𠝹"; + for (int i = 0; i < 65535 / usernamePart.length(); i++) { + usernameBuilder.append(usernamePart); + } + String password = "tset1234"; + + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.encodeSimpleMetadata( + ByteBufAllocator.DEFAULT, + usernameBuilder.toString().toCharArray(), + password.toCharArray())) + .hasMessage( + "Username should be shorter than or equal to 65535 bytes length in UTF-8 encoding"); + } + + @Test + void shouldEncodeBearerMetadata() { + String testToken = TEST_BEARER_TOKEN; + + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeBearerMetadata( + ByteBufAllocator.DEFAULT, testToken.toCharArray()); + + byteBuf.markReaderIndex(); + checkBearerAuthMetadataEncoding(testToken, byteBuf); + byteBuf.resetReaderIndex(); + checkBearerAuthMetadataEncodingUsingDecoders(testToken, byteBuf); + } + + private static void checkBearerAuthMetadataEncoding(String testToken, ByteBuf byteBuf) { + Assertions.assertThat(byteBuf.capacity()) + .isEqualTo(testToken.getBytes(CharsetUtil.UTF_8).length + AUTH_TYPE_ID_LENGTH); + Assertions.assertThat( + byteBuf.readUnsignedByte() & ~AuthMetadataFlyweight.STREAM_METADATA_KNOWN_MASK) + .isEqualTo(WellKnownAuthType.BEARER.getIdentifier()); + Assertions.assertThat(byteBuf.readSlice(byteBuf.capacity() - 1).toString(CharsetUtil.UTF_8)) + .isEqualTo(testToken); + } + + private static void checkBearerAuthMetadataEncodingUsingDecoders( + String testToken, ByteBuf byteBuf) { + Assertions.assertThat(byteBuf.capacity()) + .isEqualTo(testToken.getBytes(CharsetUtil.UTF_8).length + AUTH_TYPE_ID_LENGTH); + Assertions.assertThat(AuthMetadataFlyweight.isWellKnownAuthType(byteBuf)).isTrue(); + Assertions.assertThat(AuthMetadataFlyweight.decodeWellKnownAuthType(byteBuf)) + .isEqualTo(WellKnownAuthType.BEARER); + byteBuf.markReaderIndex(); + Assertions.assertThat(new String(AuthMetadataFlyweight.decodeBearerTokenAsCharArray(byteBuf))) + .isEqualTo(testToken); + byteBuf.resetReaderIndex(); + Assertions.assertThat( + AuthMetadataFlyweight.decodePayload(byteBuf).toString(CharsetUtil.UTF_8).toString()) + .isEqualTo(testToken); + } + + @Test + void shouldEncodeCustomAuth() { + String payloadAsAText = "testsecuritybuffer"; + ByteBuf testSecurityPayload = + Unpooled.wrappedBuffer(payloadAsAText.getBytes(CharsetUtil.UTF_8)); + + String customAuthType = "myownauthtype"; + ByteBuf buffer = + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, customAuthType, testSecurityPayload); + + checkCustomAuthMetadataEncoding(testSecurityPayload, customAuthType, buffer); + } + + private static void checkCustomAuthMetadataEncoding( + ByteBuf testSecurityPayload, String customAuthType, ByteBuf buffer) { + Assertions.assertThat(buffer.capacity()) + .isEqualTo(1 + customAuthType.length() + testSecurityPayload.capacity()); + Assertions.assertThat(buffer.readUnsignedByte()) + .isEqualTo((short) (customAuthType.length() - 1)); + Assertions.assertThat( + buffer.readCharSequence(customAuthType.length(), CharsetUtil.US_ASCII).toString()) + .isEqualTo(customAuthType); + Assertions.assertThat(buffer.readSlice(testSecurityPayload.capacity())) + .isEqualTo(testSecurityPayload); + + ReferenceCountUtil.release(buffer); + } + + @Test + void shouldThrowOnNonASCIIChars() { + ByteBuf testSecurityPayload = ByteBufAllocator.DEFAULT.buffer(); + String customAuthType = "1234567#4? 𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎"; + + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, customAuthType, testSecurityPayload)) + .hasMessage("custom auth type must be US_ASCII characters only"); + } + + @Test + void shouldThrowOnOutOfAllowedSizeType() { + ByteBuf testSecurityPayload = ByteBufAllocator.DEFAULT.buffer(); + // 130 chars + String customAuthType = + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"; + + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, customAuthType, testSecurityPayload)) + .hasMessage( + "custom auth type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + @Test + void shouldThrowOnOutOfAllowedSizeType1() { + ByteBuf testSecurityPayload = ByteBufAllocator.DEFAULT.buffer(); + String customAuthType = ""; + + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, customAuthType, testSecurityPayload)) + .hasMessage( + "custom auth type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + @Test + void shouldEncodeUsingWellKnownAuthType() { + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, + WellKnownAuthType.SIMPLE, + ByteBufAllocator.DEFAULT.buffer().writeShort(1).writeByte('u').writeByte('p')); + + checkSimpleAuthMetadataEncoding("u", "p", 1, 1, byteBuf); + } + + @Test + void shouldEncodeUsingWellKnownAuthType1() { + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, + WellKnownAuthType.SIMPLE, + ByteBufAllocator.DEFAULT.buffer().writeShort(1).writeByte('u').writeByte('p')); + + checkSimpleAuthMetadataEncoding("u", "p", 1, 1, byteBuf); + } + + @Test + void shouldEncodeUsingWellKnownAuthType2() { + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, + WellKnownAuthType.BEARER, + Unpooled.copiedBuffer(TEST_BEARER_TOKEN, CharsetUtil.UTF_8)); + + byteBuf.markReaderIndex(); + checkBearerAuthMetadataEncoding(TEST_BEARER_TOKEN, byteBuf); + byteBuf.resetReaderIndex(); + checkBearerAuthMetadataEncodingUsingDecoders(TEST_BEARER_TOKEN, byteBuf); + } + + @Test + void shouldThrowIfWellKnownAuthTypeIsUnsupportedOrUnknown() { + ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); + + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, WellKnownAuthType.UNPARSEABLE_AUTH_TYPE, buffer)) + .hasMessage("only allowed AuthType should be used"); + + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, WellKnownAuthType.UNPARSEABLE_AUTH_TYPE, buffer)) + .hasMessage("only allowed AuthType should be used"); + + buffer.release(); + } + + @Test + void shouldCompressMetadata() { + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeMetadataWithCompression( + ByteBufAllocator.DEFAULT, + "simple", + ByteBufAllocator.DEFAULT.buffer().writeShort(1).writeByte('u').writeByte('p')); + + checkSimpleAuthMetadataEncoding("u", "p", 1, 1, byteBuf); + } + + @Test + void shouldCompressMetadata1() { + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeMetadataWithCompression( + ByteBufAllocator.DEFAULT, + "bearer", + Unpooled.copiedBuffer(TEST_BEARER_TOKEN, CharsetUtil.UTF_8)); + + byteBuf.markReaderIndex(); + checkBearerAuthMetadataEncoding(TEST_BEARER_TOKEN, byteBuf); + byteBuf.resetReaderIndex(); + checkBearerAuthMetadataEncodingUsingDecoders(TEST_BEARER_TOKEN, byteBuf); + } + + @Test + void shouldNotCompressMetadata() { + ByteBuf testMetadataPayload = + Unpooled.wrappedBuffer(TEST_BEARER_TOKEN.getBytes(CharsetUtil.UTF_8)); + String customAuthType = "testauthtype"; + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeMetadataWithCompression( + ByteBufAllocator.DEFAULT, customAuthType, testMetadataPayload); + + checkCustomAuthMetadataEncoding(testMetadataPayload, customAuthType, byteBuf); + } + + @Test + void shouldConfirmWellKnownAuthType() { + ByteBuf metadata = + AuthMetadataFlyweight.encodeMetadataWithCompression( + ByteBufAllocator.DEFAULT, "simple", Unpooled.EMPTY_BUFFER); + + int initialReaderIndex = metadata.readerIndex(); + + Assertions.assertThat(AuthMetadataFlyweight.isWellKnownAuthType(metadata)).isTrue(); + Assertions.assertThat(metadata.readerIndex()).isEqualTo(initialReaderIndex); + + ReferenceCountUtil.release(metadata); + } + + @Test + void shouldConfirmGivenMetadataIsNotAWellKnownAuthType() { + ByteBuf metadata = + AuthMetadataFlyweight.encodeMetadataWithCompression( + ByteBufAllocator.DEFAULT, "simple/afafgafadf", Unpooled.EMPTY_BUFFER); + + int initialReaderIndex = metadata.readerIndex(); + + Assertions.assertThat(AuthMetadataFlyweight.isWellKnownAuthType(metadata)).isFalse(); + Assertions.assertThat(metadata.readerIndex()).isEqualTo(initialReaderIndex); + + ReferenceCountUtil.release(metadata); + } + + @Test + void shouldReadSimpleWellKnownAuthType() { + ByteBuf metadata = + AuthMetadataFlyweight.encodeMetadataWithCompression( + ByteBufAllocator.DEFAULT, "simple", Unpooled.EMPTY_BUFFER); + WellKnownAuthType expectedType = WellKnownAuthType.SIMPLE; + checkDecodeWellKnowAuthTypeCorrectly(metadata, expectedType); + } + + @Test + void shouldReadSimpleWellKnownAuthType1() { + ByteBuf metadata = + AuthMetadataFlyweight.encodeMetadataWithCompression( + ByteBufAllocator.DEFAULT, "bearer", Unpooled.EMPTY_BUFFER); + WellKnownAuthType expectedType = WellKnownAuthType.BEARER; + checkDecodeWellKnowAuthTypeCorrectly(metadata, expectedType); + } + + @Test + void shouldReadSimpleWellKnownAuthType2() { + ByteBuf metadata = + ByteBufAllocator.DEFAULT + .buffer() + .writeByte(3 | AuthMetadataFlyweight.STREAM_METADATA_KNOWN_MASK); + WellKnownAuthType expectedType = WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE; + checkDecodeWellKnowAuthTypeCorrectly(metadata, expectedType); + } + + @Test + void shouldNotReadSimpleWellKnownAuthTypeIfEncodedLength() { + ByteBuf metadata = ByteBufAllocator.DEFAULT.buffer().writeByte(3); + WellKnownAuthType expectedType = WellKnownAuthType.UNPARSEABLE_AUTH_TYPE; + checkDecodeWellKnowAuthTypeCorrectly(metadata, expectedType); + } + + @Test + void shouldNotReadSimpleWellKnownAuthTypeIfEncodedLength1() { + ByteBuf metadata = + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, "testmetadataauthtype", Unpooled.EMPTY_BUFFER); + WellKnownAuthType expectedType = WellKnownAuthType.UNPARSEABLE_AUTH_TYPE; + checkDecodeWellKnowAuthTypeCorrectly(metadata, expectedType); + } + + @Test + void shouldThrowExceptionIsNotEnoughReadableBytes() { + Assertions.assertThatThrownBy( + () -> AuthMetadataFlyweight.decodeWellKnownAuthType(Unpooled.EMPTY_BUFFER)) + .hasMessage("Unable to decode Well Know Auth type. Not enough readable bytes"); + } + + private static void checkDecodeWellKnowAuthTypeCorrectly( + ByteBuf metadata, WellKnownAuthType expectedType) { + int initialReaderIndex = metadata.readerIndex(); + + WellKnownAuthType wellKnownAuthType = AuthMetadataFlyweight.decodeWellKnownAuthType(metadata); + + Assertions.assertThat(wellKnownAuthType).isEqualTo(expectedType); + Assertions.assertThat(metadata.readerIndex()) + .isNotEqualTo(initialReaderIndex) + .isEqualTo(initialReaderIndex + 1); + + ReferenceCountUtil.release(metadata); + } + + @Test + void shouldReadCustomEncodedAuthType() { + String testAuthType = "TestAuthType"; + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, testAuthType, Unpooled.EMPTY_BUFFER); + checkDecodeCustomAuthTypeCorrectly(testAuthType, byteBuf); + } + + @Test + void shouldThrowExceptionOnEmptyMetadata() { + Assertions.assertThatThrownBy( + () -> AuthMetadataFlyweight.decodeCustomAuthType(Unpooled.EMPTY_BUFFER)) + .hasMessage("Unable to decode custom Auth type. Not enough readable bytes"); + } + + @Test + void shouldThrowExceptionOnMalformedMetadata_wellknowninstead() { + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.decodeCustomAuthType( + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, + WellKnownAuthType.BEARER, + Unpooled.copiedBuffer(new byte[] {'a', 'b'})))) + .hasMessage("Unable to decode custom Auth type. Incorrect auth type length"); + } + + @Test + void shouldThrowExceptionOnMalformedMetadata_length() { + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.decodeCustomAuthType( + ByteBufAllocator.DEFAULT.buffer().writeByte(127).writeChar('a').writeChar('b'))) + .hasMessage("Unable to decode custom Auth type. Malformed length or auth type string"); + } + + private static void checkDecodeCustomAuthTypeCorrectly(String testAuthType, ByteBuf byteBuf) { + int initialReaderIndex = byteBuf.readerIndex(); + + Assertions.assertThat(AuthMetadataFlyweight.decodeCustomAuthType(byteBuf).toString()) + .isEqualTo(testAuthType); + Assertions.assertThat(byteBuf.readerIndex()) + .isEqualTo(initialReaderIndex + testAuthType.length() + 1); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java b/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java new file mode 100644 index 000000000..9da66d424 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java @@ -0,0 +1,93 @@ +package io.rsocket.resume; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.Arrays; +import org.junit.Assert; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +public class InMemoryResumeStoreTest { + + @Test + void saveWithoutTailRemoval() { + InMemoryResumableFramesStore store = inMemoryStore(25); + ByteBuf frame = frameMock(10); + store.saveFrames(Flux.just(frame)).block(); + Assert.assertEquals(1, store.cachedFrames.size()); + Assert.assertEquals(frame.readableBytes(), store.cacheSize); + Assert.assertEquals(0, store.position); + } + + @Test + void saveRemoveOneFromTail() { + InMemoryResumableFramesStore store = inMemoryStore(25); + ByteBuf frame1 = frameMock(20); + ByteBuf frame2 = frameMock(10); + store.saveFrames(Flux.just(frame1, frame2)).block(); + Assert.assertEquals(1, store.cachedFrames.size()); + Assert.assertEquals(frame2.readableBytes(), store.cacheSize); + Assert.assertEquals(frame1.readableBytes(), store.position); + } + + @Test + void saveRemoveTwoFromTail() { + InMemoryResumableFramesStore store = inMemoryStore(25); + ByteBuf frame1 = frameMock(10); + ByteBuf frame2 = frameMock(10); + ByteBuf frame3 = frameMock(20); + store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); + Assert.assertEquals(1, store.cachedFrames.size()); + Assert.assertEquals(frame3.readableBytes(), store.cacheSize); + Assert.assertEquals(size(frame1, frame2), store.position); + } + + @Test + void saveBiggerThanStore() { + InMemoryResumableFramesStore store = inMemoryStore(25); + ByteBuf frame1 = frameMock(10); + ByteBuf frame2 = frameMock(10); + ByteBuf frame3 = frameMock(30); + store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); + Assert.assertEquals(0, store.cachedFrames.size()); + Assert.assertEquals(0, store.cacheSize); + Assert.assertEquals(size(frame1, frame2, frame3), store.position); + } + + @Test + void releaseFrames() { + InMemoryResumableFramesStore store = inMemoryStore(100); + ByteBuf frame1 = frameMock(10); + ByteBuf frame2 = frameMock(10); + ByteBuf frame3 = frameMock(30); + store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); + store.releaseFrames(20); + Assert.assertEquals(1, store.cachedFrames.size()); + Assert.assertEquals(frame3.readableBytes(), store.cacheSize); + Assert.assertEquals(size(frame1, frame2), store.position); + } + + @Test + void receiveImpliedPosition() { + InMemoryResumableFramesStore store = inMemoryStore(100); + ByteBuf frame1 = frameMock(10); + ByteBuf frame2 = frameMock(30); + store.resumableFrameReceived(frame1); + store.resumableFrameReceived(frame2); + Assert.assertEquals(size(frame1, frame2), store.frameImpliedPosition()); + } + + private int size(ByteBuf... byteBufs) { + return Arrays.stream(byteBufs).mapToInt(ByteBuf::readableBytes).sum(); + } + + private static InMemoryResumableFramesStore inMemoryStore(int size) { + return new InMemoryResumableFramesStore("test", size); + } + + private static ByteBuf frameMock(int size) { + byte[] bytes = new byte[size]; + Arrays.fill(bytes, (byte) 7); + return Unpooled.wrappedBuffer(bytes); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java new file mode 100644 index 000000000..7d2a7bcc8 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.resume; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ResumeCalculatorTest { + + @BeforeEach + void setUp() {} + + @Test + void clientResumeSuccess() { + long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 42, -1, 3); + Assertions.assertEquals(3, position); + } + + @Test + void clientResumeError() { + long position = ResumableDuplexConnection.calculateRemoteImpliedPos(4, 42, -1, 3); + Assertions.assertEquals(-1, position); + } + + @Test + void serverResumeSuccess() { + long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 42, 4, 23); + Assertions.assertEquals(23, position); + } + + @Test + void serverResumeErrorClientState() { + long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 3, 4, 23); + Assertions.assertEquals(-1, position); + } + + @Test + void serverResumeErrorServerState() { + long position = ResumableDuplexConnection.calculateRemoteImpliedPos(4, 42, 4, 1); + Assertions.assertEquals(-1, position); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ResumeExpBackoffTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ResumeExpBackoffTest.java new file mode 100644 index 000000000..d86276466 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/resume/ResumeExpBackoffTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.resume; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.Duration; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +public class ResumeExpBackoffTest { + + @Test + void backOffSeries() { + Duration firstBackoff = Duration.ofSeconds(1); + Duration maxBackoff = Duration.ofSeconds(32); + int factor = 2; + ExponentialBackoffResumeStrategy strategy = + new ExponentialBackoffResumeStrategy(firstBackoff, maxBackoff, factor); + + List expected = + Flux.just(1, 2, 4, 8, 16, 32, 32).map(Duration::ofSeconds).collectList().block(); + + List actual = Flux.range(1, 7).map(v -> strategy.next()).collectList().block(); + + Assertions.assertThat(actual).isEqualTo(expected); + } + + @Test + void nullFirstBackoff() { + assertThrows( + NullPointerException.class, + () -> { + ExponentialBackoffResumeStrategy strategy = + new ExponentialBackoffResumeStrategy(Duration.ofSeconds(1), null, 42); + }); + } + + @Test + void nullMaxBackoff() { + assertThrows( + NullPointerException.class, + () -> { + ExponentialBackoffResumeStrategy strategy = + new ExponentialBackoffResumeStrategy(null, Duration.ofSeconds(1), 42); + }); + } + + @Test + void negativeFactor() { + assertThrows( + IllegalArgumentException.class, + () -> { + ExponentialBackoffResumeStrategy strategy = + new ExponentialBackoffResumeStrategy( + Duration.ofSeconds(1), Duration.ofSeconds(32), -1); + }); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/ByteBufUtils.java b/rsocket-core/src/test/java/io/rsocket/test/util/ByteBufUtils.java new file mode 100644 index 000000000..9bed415ae --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/test/util/ByteBufUtils.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.test.util; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.concurrent.ThreadLocalRandom; + +public final class ByteBufUtils { + + private ByteBufUtils() {} + + public static ByteBuf getRandomByteBuf(int size) { + byte[] bytes = new byte[size]; + ThreadLocalRandom.current().nextBytes(bytes); + return Unpooled.wrappedBuffer(bytes); + } +} 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 3e8a0e551..58323c066 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/LocalDuplexConnection.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/LocalDuplexConnection.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,8 +16,9 @@ package io.rsocket.test.util; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; -import io.rsocket.Frame; import org.reactivestreams.Publisher; import reactor.core.publisher.DirectProcessor; import reactor.core.publisher.Flux; @@ -25,21 +26,26 @@ import reactor.core.publisher.MonoProcessor; public class LocalDuplexConnection implements DuplexConnection { - private final DirectProcessor send; - private final DirectProcessor receive; - private final MonoProcessor closeNotifier; + private final ByteBufAllocator allocator; + private final DirectProcessor send; + private final DirectProcessor receive; + private final MonoProcessor onClose; private final String name; public LocalDuplexConnection( - String name, DirectProcessor send, DirectProcessor receive) { + String name, + ByteBufAllocator allocator, + DirectProcessor send, + DirectProcessor receive) { this.name = name; + this.allocator = allocator; this.send = send; this.receive = receive; - closeNotifier = MonoProcessor.create(); + this.onClose = MonoProcessor.create(); } @Override - public Mono send(Publisher frame) { + public Mono send(Publisher frame) { return Flux.from(frame) .doOnNext(f -> System.out.println(name + " - " + f.toString())) .doOnNext(send::onNext) @@ -48,26 +54,27 @@ public Mono send(Publisher frame) { } @Override - public Flux receive() { + public Flux receive() { return receive.doOnNext(f -> System.out.println(name + " - " + f.toString())); } @Override - public double availability() { - return 1; + public ByteBufAllocator alloc() { + return allocator; } @Override - public Mono close() { - return Mono.defer( - () -> { - closeNotifier.onComplete(); - return Mono.empty(); - }); + public void dispose() { + onClose.onComplete(); + } + + @Override + public boolean isDisposed() { + return onClose.isDisposed(); } @Override public Mono onClose() { - return closeNotifier; + return onClose; } } 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 e0ee1713c..179afff58 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 @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -77,8 +77,13 @@ public double availability() { } @Override - public Mono close() { - return delegate.close(); + public void dispose() { + delegate.dispose(); + } + + @Override + public boolean isDisposed() { + return delegate.isDisposed(); } @Override diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/StringUtils.java b/rsocket-core/src/test/java/io/rsocket/test/util/StringUtils.java new file mode 100644 index 000000000..403eacb6d --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/test/util/StringUtils.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.test.util; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +public final class StringUtils { + + private static final String CANDIDATE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + + private StringUtils() {} + + public static String getRandomString(int size) { + return ThreadLocalRandom.current() + .ints(size, 0, CANDIDATE_CHARS.length()) + .mapToObj(index -> ((Character) CANDIDATE_CHARS.charAt(index)).toString()) + .collect(Collectors.joining()); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java new file mode 100644 index 000000000..88694d209 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java @@ -0,0 +1,40 @@ +package io.rsocket.test.util; + +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.transport.ClientTransport; +import reactor.core.publisher.Mono; + +public class TestClientTransport implements ClientTransport { + private final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + private final TestDuplexConnection testDuplexConnection = new TestDuplexConnection(allocator); + + int maxFrameLength = FRAME_LENGTH_MASK; + + @Override + public Mono connect() { + return Mono.just(testDuplexConnection); + } + + public TestDuplexConnection testConnection() { + return testDuplexConnection; + } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } + + public TestClientTransport withMaxFrameLength(int maxFrameLength) { + this.maxFrameLength = maxFrameLength; + return this; + } + + @Override + public int maxFrameLength() { + return maxFrameLength; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java index dc7914915..17a19b8c9 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,8 +16,9 @@ package io.rsocket.test.util; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; -import io.rsocket.Frame; import java.util.Collection; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -27,6 +28,7 @@ import org.slf4j.LoggerFactory; import reactor.core.publisher.DirectProcessor; import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; @@ -38,65 +40,78 @@ public class TestDuplexConnection implements DuplexConnection { private static final Logger logger = LoggerFactory.getLogger(TestDuplexConnection.class); - private final LinkedBlockingQueue sent; - private final DirectProcessor sentPublisher; - private final DirectProcessor received; - private final MonoProcessor close; - private final ConcurrentLinkedQueue> sendSubscribers; + private final LinkedBlockingQueue sent; + private final DirectProcessor sentPublisher; + private final FluxSink sendSink; + private final DirectProcessor received; + private final FluxSink receivedSink; + private final MonoProcessor onClose; + private final ConcurrentLinkedQueue> sendSubscribers; + private final ByteBufAllocator allocator; private volatile double availability = 1; private volatile int initialSendRequestN = Integer.MAX_VALUE; - public TestDuplexConnection() { - sent = new LinkedBlockingQueue<>(); - received = DirectProcessor.create(); - sentPublisher = DirectProcessor.create(); - sendSubscribers = new ConcurrentLinkedQueue<>(); - close = MonoProcessor.create(); + public TestDuplexConnection(ByteBufAllocator allocator) { + this.allocator = allocator; + this.sent = new LinkedBlockingQueue<>(); + this.received = DirectProcessor.create(); + this.receivedSink = received.sink(); + this.sentPublisher = DirectProcessor.create(); + this.sendSink = sentPublisher.sink(); + this.sendSubscribers = new ConcurrentLinkedQueue<>(); + this.onClose = MonoProcessor.create(); } @Override - public Mono send(Publisher frames) { + public Mono send(Publisher frames) { if (availability <= 0) { return Mono.error( new IllegalStateException("RSocket not available. Availability: " + availability)); } - Subscriber subscriber = TestSubscriber.create(initialSendRequestN); + Subscriber subscriber = TestSubscriber.create(initialSendRequestN); Flux.from(frames) .doOnNext( frame -> { sent.offer(frame); - sentPublisher.onNext(frame); - }) - .doOnError( - throwable -> { - logger.error("Error in send stream on test connection.", throwable); + sendSink.next(frame); }) + .doOnError(throwable -> logger.error("Error in send stream on test connection.", throwable)) .subscribe(subscriber); sendSubscribers.add(subscriber); return Mono.empty(); } @Override - public Flux receive() { + public Flux receive() { return received; } + @Override + public ByteBufAllocator alloc() { + return allocator; + } + @Override public double availability() { return availability; } @Override - public Mono close() { - return close; + public void dispose() { + onClose.onComplete(); + } + + @Override + public boolean isDisposed() { + return onClose.isDisposed(); } @Override public Mono onClose() { - return close(); + return onClose; } - public Frame awaitSend() throws InterruptedException { + public ByteBuf awaitSend() throws InterruptedException { return sent.take(); } @@ -104,17 +119,17 @@ public void setAvailability(double availability) { this.availability = availability; } - public Collection getSent() { + public Collection getSent() { return sent; } - public Publisher getSentAsPublisher() { + public Publisher getSentAsPublisher() { return sentPublisher; } - public void addToReceivedBuffer(Frame... received) { - for (Frame frame : received) { - this.received.onNext(frame); + public void addToReceivedBuffer(ByteBuf... received) { + for (ByteBuf frame : received) { + this.receivedSink.next(frame); } } @@ -127,7 +142,7 @@ public void setInitialSendRequestN(int initialSendRequestN) { this.initialSendRequestN = initialSendRequestN; } - public Collection> getSendSubscribers() { + public Collection> getSendSubscribers() { return sendSubscribers; } } diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestServerTransport.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestServerTransport.java new file mode 100644 index 000000000..0f9ea8e48 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestServerTransport.java @@ -0,0 +1,68 @@ +package io.rsocket.test.util; + +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Closeable; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.transport.ServerTransport; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; + +public class TestServerTransport implements ServerTransport { + private final MonoProcessor conn = MonoProcessor.create(); + private final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + + int maxFrameLength = FRAME_LENGTH_MASK; + + @Override + public Mono start(ConnectionAcceptor acceptor) { + conn.flatMap(acceptor::apply) + .subscribe(ignored -> {}, err -> disposeConnection(), this::disposeConnection); + return Mono.just( + new Closeable() { + @Override + public Mono onClose() { + return conn.then(); + } + + @Override + public void dispose() { + conn.onComplete(); + } + + @Override + public boolean isDisposed() { + return conn.isTerminated(); + } + }); + } + + private void disposeConnection() { + TestDuplexConnection c = conn.peek(); + if (c != null) { + c.dispose(); + } + } + + public TestDuplexConnection connect() { + TestDuplexConnection c = new TestDuplexConnection(allocator); + conn.onNext(c); + return c; + } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } + + public TestServerTransport withMaxFrameLength(int maxFrameLength) { + this.maxFrameLength = maxFrameLength; + return this; + } + + @Override + public int maxFrameLength() { + return maxFrameLength; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestSubscriber.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestSubscriber.java index 0b6b28dc1..e88b39648 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/TestSubscriber.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestSubscriber.java @@ -1,6 +1,22 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.rsocket.test.util; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import io.rsocket.Payload; @@ -14,11 +30,14 @@ public static Subscriber create() { } public static Subscriber create(long initialRequest) { - Subscriber mock = mock(Subscriber.class); + @SuppressWarnings("unchecked") + Subscriber mock = mock(Subscriber.class); Mockito.doAnswer( invocation -> { - ((Subscription) invocation.getArguments()[0]).request(initialRequest); + if (initialRequest > 0) { + ((Subscription) invocation.getArguments()[0]).request(initialRequest); + } return null; }) .when(mock) @@ -32,11 +51,11 @@ public static Payload anyPayload() { } public static Subscriber createCancelling() { - Subscriber mock = mock(Subscriber.class); + @SuppressWarnings("unchecked") + Subscriber mock = mock(Subscriber.class); Mockito.doAnswer( invocation -> { - System.out.println("cancel"); ((Subscription) invocation.getArguments()[0]).cancel(); return null; }) diff --git a/rsocket-core/src/test/java/io/rsocket/uri/TestUriHandler.java b/rsocket-core/src/test/java/io/rsocket/uri/TestUriHandler.java deleted file mode 100644 index ba1dff548..000000000 --- a/rsocket-core/src/test/java/io/rsocket/uri/TestUriHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.uri; - -import io.rsocket.test.util.TestDuplexConnection; -import io.rsocket.transport.ClientTransport; -import java.net.URI; -import java.util.Optional; -import reactor.core.publisher.Mono; - -public class TestUriHandler implements UriHandler { - @Override - public Optional buildClient(URI uri) { - if ("test".equals(uri.getScheme())) { - return Optional.of(() -> Mono.just(new TestDuplexConnection())); - } - return UriHandler.super.buildClient(uri); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/uri/UriTransportRegistryTest.java b/rsocket-core/src/test/java/io/rsocket/uri/UriTransportRegistryTest.java deleted file mode 100644 index 5b90d64f0..000000000 --- a/rsocket-core/src/test/java/io/rsocket/uri/UriTransportRegistryTest.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.uri; - -import static org.junit.Assert.assertTrue; - -import io.rsocket.DuplexConnection; -import io.rsocket.test.util.TestDuplexConnection; -import io.rsocket.transport.ClientTransport; -import org.junit.Test; - -public class UriTransportRegistryTest { - @Test - public void testTestRegistered() { - ClientTransport test = UriTransportRegistry.clientForUri("test://test"); - - DuplexConnection duplexConnection = test.connect().block(); - - assertTrue(duplexConnection instanceof TestDuplexConnection); - } - - @Test(expected = UnsupportedOperationException.class) - public void testTestUnregistered() { - ClientTransport test = UriTransportRegistry.clientForUri("mailto://bonson@baulsupp.net"); - - test.connect().block(); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/util/ByteBufPayloadTest.java b/rsocket-core/src/test/java/io/rsocket/util/ByteBufPayloadTest.java new file mode 100644 index 000000000..2ad944d09 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/util/ByteBufPayloadTest.java @@ -0,0 +1,64 @@ +package io.rsocket.util; + +import io.netty.buffer.Unpooled; +import io.netty.util.IllegalReferenceCountException; +import io.rsocket.Payload; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ByteBufPayloadTest { + + @Test + public void shouldIndicateThatItHasMetadata() { + Payload payload = ByteBufPayload.create("data", "metadata"); + + Assertions.assertThat(payload.hasMetadata()).isTrue(); + Assertions.assertThat(payload.release()).isTrue(); + } + + @Test + public void shouldIndicateThatItHasNotMetadata() { + Payload payload = ByteBufPayload.create("data"); + + Assertions.assertThat(payload.hasMetadata()).isFalse(); + Assertions.assertThat(payload.release()).isTrue(); + } + + @Test + public void shouldIndicateThatItHasMetadata1() { + Payload payload = + ByteBufPayload.create(Unpooled.wrappedBuffer("data".getBytes()), Unpooled.EMPTY_BUFFER); + + Assertions.assertThat(payload.hasMetadata()).isTrue(); + Assertions.assertThat(payload.release()).isTrue(); + } + + @Test + public void shouldThrowExceptionIfAccessAfterRelease() { + Payload payload = ByteBufPayload.create("data", "metadata"); + + Assertions.assertThat(payload.release()).isTrue(); + + Assertions.assertThatThrownBy(payload::hasMetadata) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::data).isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::metadata) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::sliceData) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::sliceMetadata) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::touch) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(() -> payload.touch("test")) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::getData) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::getMetadata) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::getDataUtf8) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::getMetadataUtf8) + .isInstanceOf(IllegalReferenceCountException.class); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/util/DefaultPayloadTest.java b/rsocket-core/src/test/java/io/rsocket/util/DefaultPayloadTest.java new file mode 100644 index 000000000..3f97ab9dc --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/util/DefaultPayloadTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.util; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.Payload; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import java.nio.ByteBuffer; +import java.util.concurrent.ThreadLocalRandom; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public class DefaultPayloadTest { + public static final String DATA_VAL = "data"; + public static final String METADATA_VAL = "metadata"; + + @Test + public void testReuse() { + Payload p = DefaultPayload.create(DATA_VAL, METADATA_VAL); + assertDataAndMetadata(p, DATA_VAL, METADATA_VAL); + assertDataAndMetadata(p, DATA_VAL, METADATA_VAL); + } + + public void assertDataAndMetadata(Payload p, String dataVal, String metadataVal) { + assertThat("Unexpected data.", p.getDataUtf8(), equalTo(dataVal)); + if (metadataVal == null) { + assertThat("Non-null metadata", p.hasMetadata(), equalTo(false)); + } else { + assertThat("Null metadata", p.hasMetadata(), equalTo(true)); + assertThat("Unexpected metadata.", p.getMetadataUtf8(), equalTo(metadataVal)); + } + } + + @Test + public void staticMethods() { + assertDataAndMetadata(DefaultPayload.create(DATA_VAL, METADATA_VAL), DATA_VAL, METADATA_VAL); + assertDataAndMetadata(DefaultPayload.create(DATA_VAL), DATA_VAL, null); + } + + @Test + public void shouldIndicateThatItHasNotMetadata() { + Payload payload = DefaultPayload.create("data"); + + Assertions.assertThat(payload.hasMetadata()).isFalse(); + } + + @Test + public void shouldIndicateThatItHasMetadata1() { + Payload payload = + DefaultPayload.create(Unpooled.wrappedBuffer("data".getBytes()), Unpooled.EMPTY_BUFFER); + + Assertions.assertThat(payload.hasMetadata()).isTrue(); + } + + @Test + public void shouldIndicateThatItHasMetadata2() { + Payload payload = + DefaultPayload.create(ByteBuffer.wrap("data".getBytes()), ByteBuffer.allocate(0)); + + Assertions.assertThat(payload.hasMetadata()).isTrue(); + } + + @Test + public void shouldReleaseGivenByteBufDataAndMetadataUpOnPayloadCreation() { + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + for (byte i = 0; i < 126; i++) { + ByteBuf data = allocator.buffer(); + data.writeByte(i); + + boolean metadataPresent = ThreadLocalRandom.current().nextBoolean(); + ByteBuf metadata = null; + if (metadataPresent) { + metadata = allocator.buffer(); + metadata.writeByte(i + 1); + } + + Payload payload = DefaultPayload.create(data, metadata); + + Assertions.assertThat(payload.getData()).isEqualTo(ByteBuffer.wrap(new byte[] {i})); + + Assertions.assertThat(payload.getMetadata()) + .isEqualTo( + metadataPresent + ? ByteBuffer.wrap(new byte[] {(byte) (i + 1)}) + : DefaultPayload.EMPTY_BUFFER); + allocator.assertHasNoLeaks(); + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java b/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java new file mode 100644 index 000000000..46e0f77f4 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java @@ -0,0 +1,187 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.util; + +import static org.assertj.core.api.Assertions.*; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +final class NumberUtilsTest { + + @DisplayName("returns int value with postitive int") + @Test + void requireNonNegativeInt() { + assertThat(NumberUtils.requireNonNegative(Integer.MAX_VALUE, "test-message")) + .isEqualTo(Integer.MAX_VALUE); + } + + @DisplayName( + "requireNonNegative with int argument throws IllegalArgumentException with negative value") + @Test + void requireNonNegativeIntNegative() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requireNonNegative(Integer.MIN_VALUE, "test-message")) + .withMessage("test-message"); + } + + @DisplayName("requireNonNegative with int argument throws NullPointerException with null message") + @Test + void requireNonNegativeIntNullMessage() { + assertThatNullPointerException() + .isThrownBy(() -> NumberUtils.requireNonNegative(Integer.MIN_VALUE, null)) + .withMessage("message must not be null"); + } + + @DisplayName("requireNonNegative returns int value with zero") + @Test + void requireNonNegativeIntZero() { + assertThat(NumberUtils.requireNonNegative(0, "test-message")).isEqualTo(0); + } + + @DisplayName("requirePositive returns int value with positive int") + @Test + void requirePositiveInt() { + assertThat(NumberUtils.requirePositive(Integer.MAX_VALUE, "test-message")) + .isEqualTo(Integer.MAX_VALUE); + } + + @DisplayName( + "requirePositive with int argument throws IllegalArgumentException with negative value") + @Test + void requirePositiveIntNegative() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requirePositive(Integer.MIN_VALUE, "test-message")) + .withMessage("test-message"); + } + + @DisplayName("requirePositive with int argument throws NullPointerException with null message") + @Test + void requirePositiveIntNullMessage() { + assertThatNullPointerException() + .isThrownBy(() -> NumberUtils.requirePositive(Integer.MIN_VALUE, null)) + .withMessage("message must not be null"); + } + + @DisplayName("requirePositive with int argument throws IllegalArgumentException with zero value") + @Test + void requirePositiveIntZero() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requirePositive(0, "test-message")) + .withMessage("test-message"); + } + + @DisplayName("requirePositive returns long value with positive long") + @Test + void requirePositiveLong() { + assertThat(NumberUtils.requirePositive(Long.MAX_VALUE, "test-message")) + .isEqualTo(Long.MAX_VALUE); + } + + @DisplayName( + "requirePositive with long argument throws IllegalArgumentException with negative value") + @Test + void requirePositiveLongNegative() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requirePositive(Long.MIN_VALUE, "test-message")) + .withMessage("test-message"); + } + + @DisplayName("requirePositive with long argument throws NullPointerException with null message") + @Test + void requirePositiveLongNullMessage() { + assertThatNullPointerException() + .isThrownBy(() -> NumberUtils.requirePositive(Long.MIN_VALUE, null)) + .withMessage("message must not be null"); + } + + @DisplayName("requirePositive with long argument throws IllegalArgumentException with zero value") + @Test + void requirePositiveLongZero() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requirePositive(0L, "test-message")) + .withMessage("test-message"); + } + + @DisplayName("requireUnsignedByte returns length if 255") + @Test + void requireUnsignedByte() { + assertThat(NumberUtils.requireUnsignedByte((1 << 8) - 1)).isEqualTo(255); + } + + @DisplayName("requireUnsignedByte throws IllegalArgumentException if larger than 255") + @Test + void requireUnsignedByteOverFlow() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requireUnsignedByte(1 << 8)) + .withMessage("%d is larger than 8 bits", 1 << 8); + } + + @DisplayName("requireUnsignedMedium returns length if 16_777_215") + @Test + void requireUnsignedMedium() { + assertThat(NumberUtils.requireUnsignedMedium((1 << 24) - 1)).isEqualTo(16_777_215); + } + + @DisplayName("requireUnsignedMedium throws IllegalArgumentException if larger than 16_777_215") + @Test + void requireUnsignedMediumOverFlow() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requireUnsignedMedium(1 << 24)) + .withMessage("%d is larger than 24 bits", 1 << 24); + } + + @DisplayName("requireUnsignedShort returns length if 65_535") + @Test + void requireUnsignedShort() { + assertThat(NumberUtils.requireUnsignedShort((1 << 16) - 1)).isEqualTo(65_535); + } + + @DisplayName("requireUnsignedShort throws IllegalArgumentException if larger than 65_535") + @Test + void requireUnsignedShortOverFlow() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requireUnsignedShort(1 << 16)) + .withMessage("%d is larger than 16 bits", 1 << 16); + } + + @Test + void encodeUnsignedMedium() { + ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); + NumberUtils.encodeUnsignedMedium(buffer, 129); + buffer.markReaderIndex(); + + assertThat(buffer.readUnsignedMedium()).as("reading as unsigned medium").isEqualTo(129); + + buffer.resetReaderIndex(); + assertThat(buffer.readMedium()).as("reading as signed medium").isEqualTo(129); + } + + @Test + void encodeUnsignedMediumLarge() { + ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); + NumberUtils.encodeUnsignedMedium(buffer, 0xFFFFFC); + buffer.markReaderIndex(); + + assertThat(buffer.readUnsignedMedium()).as("reading as unsigned medium").isEqualTo(16777212); + + buffer.resetReaderIndex(); + assertThat(buffer.readMedium()).as("reading as signed medium").isEqualTo(-4); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/util/PayloadImplTest.java b/rsocket-core/src/test/java/io/rsocket/util/PayloadImplTest.java deleted file mode 100644 index bf029974b..000000000 --- a/rsocket-core/src/test/java/io/rsocket/util/PayloadImplTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package io.rsocket.util; - -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; - -import io.rsocket.TestUtil; -import org.junit.Test; - -public class PayloadImplTest { - public static final String DATA_VAL = "data"; - public static final String METADATA_VAL = "metadata"; - - @Test - public void testReuse() throws Exception { - PayloadImpl p = new PayloadImpl(DATA_VAL, METADATA_VAL); - assertDataAndMetadata(p); - assertDataAndMetadata(p); - } - - @Test - public void testReuseWithExternalMark() throws Exception { - PayloadImpl p = new PayloadImpl(DATA_VAL, METADATA_VAL); - assertDataAndMetadata(p); - p.getData().position(2).mark(); - assertDataAndMetadata(p); - } - - public void assertDataAndMetadata(PayloadImpl p) { - assertThat("Unexpected data.", TestUtil.byteToString(p.getData()), equalTo(DATA_VAL)); - assertThat( - "Unexpected metadata.", TestUtil.byteToString(p.getMetadata()), equalTo(METADATA_VAL)); - } -} diff --git a/rsocket-core/src/test/resources/META-INF/services/io.rsocket.uri.UriHandler b/rsocket-core/src/test/resources/META-INF/services/io.rsocket.uri.UriHandler deleted file mode 100644 index 4a10f7ed6..000000000 --- a/rsocket-core/src/test/resources/META-INF/services/io.rsocket.uri.UriHandler +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2016 Netflix, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -io.rsocket.uri.TestUriHandler diff --git a/rsocket-examples/src/main/resources/log4j.properties b/rsocket-core/src/test/resources/META-INF/services/org.assertj.core.presentation.Representation similarity index 61% rename from rsocket-examples/src/main/resources/log4j.properties rename to rsocket-core/src/test/resources/META-INF/services/org.assertj.core.presentation.Representation index 6bd4c8540..9ac418a0c 100644 --- a/rsocket-examples/src/main/resources/log4j.properties +++ b/rsocket-core/src/test/resources/META-INF/services/org.assertj.core.presentation.Representation @@ -1,11 +1,11 @@ # -# Copyright 2016 Netflix, Inc. +# 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 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -13,8 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -log4j.rootLogger=INFO, stdout - -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] - %m%n \ No newline at end of file +io.rsocket.frame.ByteBufRepresentation \ No newline at end of file diff --git a/rsocket-core/src/test/resources/log4j.properties b/rsocket-core/src/test/resources/log4j.properties deleted file mode 100644 index e39a6e823..000000000 --- a/rsocket-core/src/test/resources/log4j.properties +++ /dev/null @@ -1,34 +0,0 @@ -# -# Copyright 2016 Netflix, Inc. -#

-# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -#

-# http://www.apache.org/licenses/LICENSE-2.0 -#

-# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on -# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -# - - -# -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -log4j.rootLogger=INFO, stdout - -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] (%F:%L) - %m%n -log4j.logger.io.rsocket.FrameLogger=Debug \ No newline at end of file diff --git a/rsocket-core/src/test/resources/logback-test.xml b/rsocket-core/src/test/resources/logback-test.xml new file mode 100644 index 000000000..9081698fb --- /dev/null +++ b/rsocket-core/src/test/resources/logback-test.xml @@ -0,0 +1,32 @@ + + + + + + + + %date{HH:mm:ss.SSS} %-10thread %-42logger %msg%n + + + + + + + + + + diff --git a/rsocket-examples/build.gradle b/rsocket-examples/build.gradle index f82cc5d59..01e80cfa1 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,30 +15,25 @@ */ plugins { - id "me.champeau.gradle.jmh" version "0.3.1" -} - -jmh { - jmhVersion = '1.15' - jvmArgs = '-XX:+UseG1GC -Xmx4g -Xms4g -XX:+UnlockCommercialFeatures -XX:+FlightRecorder' - profilers = ['gc'] - zip64 = true - warmupBatchSize = 10 - iterations = 500 - duplicateClassesStrategy = 'warn' + id 'java' } dependencies { - compile project(':rsocket-core') - compile project(':rsocket-spectator') - compile project(':rsocket-transport-netty') - compile project(':rsocket-transport-local') - - compile 'org.slf4j:slf4j-log4j12:1.7.21' + implementation project(':rsocket-core') + implementation project(':rsocket-transport-local') + implementation project(':rsocket-transport-netty') + runtimeOnly 'ch.qos.logback:logback-classic' - jmh group: 'org.openjdk.jmh', name: 'jmh-generator-annprocess', version: '1.15' + testImplementation project(':rsocket-test') + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'org.mockito:mockito-core' + testImplementation 'org.assertj:assertj-core' + testImplementation 'io.projectreactor:reactor-test' - compile 'org.slf4j:slf4j-log4j12:1.7.21' - - testCompile project(':rsocket-test') + // TODO: Remove after JUnit5 migration + testCompileOnly 'junit:junit' + testImplementation 'org.hamcrest:hamcrest-library' + testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' } + +description = 'Example usage of the RSocket library' diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/channel/ChannelEchoClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/channel/ChannelEchoClient.java index 0dcbdf797..b532c0140 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/channel/ChannelEchoClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/channel/ChannelEchoClient.java @@ -1,70 +1,63 @@ /* - * Copyright 2016 Netflix, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket.examples.transport.tcp.channel; -import io.rsocket.AbstractRSocket; -import io.rsocket.ConnectionSetupPayload; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.TcpServerTransport; -import io.rsocket.util.PayloadImpl; -import java.nio.charset.StandardCharsets; +import io.rsocket.util.DefaultPayload; import java.time.Duration; -import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; public final class ChannelEchoClient { + private static final Logger logger = LoggerFactory.getLogger(ChannelEchoClient.class); + public static void main(String[] args) { - RSocketFactory.receive() - .acceptor(new SocketAcceptorImpl()) - .transport(TcpServerTransport.create("localhost", 7000)) - .start() + + SocketAcceptor echoAcceptor = + SocketAcceptor.forRequestChannel( + payloads -> + Flux.from(payloads) + .map(Payload::getDataUtf8) + .map(s -> "Echo: " + s) + .map(DefaultPayload::create)); + + RSocketServer.create(echoAcceptor) + .bind(TcpServerTransport.create("localhost", 7000)) .subscribe(); RSocket socket = - RSocketFactory.connect() - .transport(TcpClientTransport.create("localhost", 7000)) - .start() - .block(); + RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000)).block(); socket - .requestChannel(Flux.interval(Duration.ofMillis(1000)).map(i -> new PayloadImpl("Hello"))) - .map(payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString()) - .doOnNext(System.out::println) + .requestChannel( + Flux.interval(Duration.ofMillis(1000)).map(i -> DefaultPayload.create("Hello"))) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) .take(10) - .thenEmpty(socket.close()) + .doFinally(signalType -> socket.dispose()) + .then() .block(); } - - private static class SocketAcceptorImpl implements SocketAcceptor { - @Override - public Mono accept(ConnectionSetupPayload setupPayload, RSocket reactiveSocket) { - return Mono.just( - new AbstractRSocket() { - @Override - public Flux requestChannel(Publisher payloads) { - return Flux.from(payloads) - .map(payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString()) - .map(s -> "Echo: " + s) - .map(PayloadImpl::new); - } - }); - } - } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/duplex/DuplexClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/duplex/DuplexClient.java deleted file mode 100644 index b286da097..000000000 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/duplex/DuplexClient.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2017 Netflix, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.duplex; - -import io.rsocket.AbstractRSocket; -import io.rsocket.Payload; -import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; -import io.rsocket.transport.netty.client.TcpClientTransport; -import io.rsocket.transport.netty.server.TcpServerTransport; -import io.rsocket.util.PayloadImpl; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -public final class DuplexClient { - - public static void main(String[] args) { - RSocketFactory.receive() - .acceptor( - (setup, reactiveSocket) -> { - reactiveSocket - .requestStream(new PayloadImpl("Hello-Bidi")) - .map(payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString()) - .log() - .subscribe(); - - return Mono.just(new AbstractRSocket() {}); - }) - .transport(TcpServerTransport.create("localhost", 7000)) - .start() - .subscribe(); - - RSocket socket = - RSocketFactory.connect() - .acceptor( - rSocket -> - new AbstractRSocket() { - @Override - public Flux requestStream(Payload payload) { - return Flux.interval(Duration.ofSeconds(1)) - .map(aLong -> new PayloadImpl("Bi-di Response => " + aLong)); - } - }) - .transport(TcpClientTransport.create("localhost", 7000)) - .start() - .block(); - - socket.onClose().block(); - } -} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java new file mode 100644 index 000000000..cf68dcdde --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java @@ -0,0 +1,237 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.examples.transport.tcp.fnf; + +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadLocalRandom; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.UnicastProcessor; +import reactor.util.concurrent.Queues; + +/** + * An example of long-running tasks processing (a.k.a Kafka style) where a client submits tasks over + * request `FireAndForget` and then receives results over the same method but on it is own side. + * + *

This example shows a case when the client may disappear, however, another a client can connect + * again and receive undelivered completed tasks remaining for the previous one. + */ +public class TaskProcessingWithServerSideNotificationsExample { + + public static void main(String[] args) throws InterruptedException { + UnicastProcessor tasksProcessor = + UnicastProcessor.create(Queues.unboundedMultiproducer().get()); + ConcurrentMap> idToCompletedTasksMap = new ConcurrentHashMap<>(); + ConcurrentMap idToRSocketMap = new ConcurrentHashMap<>(); + BackgroundWorker backgroundWorker = + new BackgroundWorker(tasksProcessor, idToCompletedTasksMap, idToRSocketMap); + + RSocketServer.create(new TasksAcceptor(tasksProcessor, idToCompletedTasksMap, idToRSocketMap)) + .bindNow(TcpServerTransport.create(9991)); + + Logger logger = LoggerFactory.getLogger("RSocket.Client.ID[Test]"); + + Mono rSocketMono = + RSocketConnector.create() + .setupPayload(DefaultPayload.create("Test")) + .acceptor( + SocketAcceptor.forFireAndForget( + p -> { + logger.info("Received Processed Task[{}]", p.getDataUtf8()); + p.release(); + return Mono.empty(); + })) + .connect(TcpClientTransport.create(9991)); + + RSocket rSocketRequester1 = rSocketMono.block(); + + for (int i = 0; i < 10; i++) { + rSocketRequester1.fireAndForget(DefaultPayload.create("task" + i)).block(); + } + + Thread.sleep(4000); + + rSocketRequester1.dispose(); + logger.info("Disposed"); + + Thread.sleep(4000); + + RSocket rSocketRequester2 = rSocketMono.block(); + + logger.info("Reconnected"); + + Thread.sleep(10000); + } + + static class BackgroundWorker extends BaseSubscriber { + final ConcurrentMap> idToCompletedTasksMap; + final ConcurrentMap idToRSocketMap; + + BackgroundWorker( + Flux taskProducer, + ConcurrentMap> idToCompletedTasksMap, + ConcurrentMap idToRSocketMap) { + + this.idToCompletedTasksMap = idToCompletedTasksMap; + this.idToRSocketMap = idToRSocketMap; + + // mimic a long running task processing + taskProducer + .concatMap( + t -> + Mono.delay(Duration.ofMillis(ThreadLocalRandom.current().nextInt(200, 2000))) + .thenReturn(t)) + .subscribe(this); + } + + @Override + protected void hookOnNext(Task task) { + BlockingQueue completedTasksQueue = + idToCompletedTasksMap.computeIfAbsent(task.id, __ -> new LinkedBlockingQueue<>()); + + completedTasksQueue.offer(task); + RSocket rSocket = idToRSocketMap.get(task.id); + if (rSocket != null) { + rSocket + .fireAndForget(DefaultPayload.create(task.content)) + .subscribe(null, e -> {}, () -> completedTasksQueue.remove(task)); + } + } + } + + static class TasksAcceptor implements SocketAcceptor { + + static final Logger logger = LoggerFactory.getLogger(TasksAcceptor.class); + + final UnicastProcessor tasksToProcess; + final ConcurrentMap> idToCompletedTasksMap; + final ConcurrentMap idToRSocketMap; + + TasksAcceptor( + UnicastProcessor tasksToProcess, + ConcurrentMap> idToCompletedTasksMap, + ConcurrentMap idToRSocketMap) { + this.tasksToProcess = tasksToProcess; + this.idToCompletedTasksMap = idToCompletedTasksMap; + this.idToRSocketMap = idToRSocketMap; + } + + @Override + public Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket) { + String id = setup.getDataUtf8(); + logger.info("Accepting a new client connection with ID {}", id); + // sendingRSocket represents here an RSocket requester to a remote peer + + if (this.idToRSocketMap.compute( + id, (__, old) -> old == null || old.isDisposed() ? sendingSocket : old) + == sendingSocket) { + return Mono.just( + new RSocketTaskHandler(idToRSocketMap, tasksToProcess, id, sendingSocket)) + .doOnSuccess(__ -> checkTasksToDeliver(sendingSocket, id)); + } + + return Mono.error( + new IllegalStateException("There is already a client connected with the same ID")); + } + + private void checkTasksToDeliver(RSocket sendingSocket, String id) { + logger.info("Accepted a new client connection with ID {}. Checking for remaining tasks", id); + BlockingQueue tasksToDeliver = this.idToCompletedTasksMap.get(id); + + if (tasksToDeliver == null || tasksToDeliver.isEmpty()) { + // means nothing yet to send + return; + } + + logger.info("Found remaining tasks to deliver for client {}", id); + + for (; ; ) { + Task task = tasksToDeliver.poll(); + + if (task == null) { + return; + } + + sendingSocket + .fireAndForget(DefaultPayload.create(task.content)) + .subscribe( + null, + e -> { + // offers back a task if it has not been delivered + tasksToDeliver.offer(task); + }); + } + } + + private static class RSocketTaskHandler implements RSocket { + + private final String id; + private final RSocket sendingSocket; + private ConcurrentMap idToRSocketMap; + private UnicastProcessor tasksToProcess; + + public RSocketTaskHandler( + ConcurrentMap idToRSocketMap, + UnicastProcessor tasksToProcess, + String id, + RSocket sendingSocket) { + this.id = id; + this.sendingSocket = sendingSocket; + this.idToRSocketMap = idToRSocketMap; + this.tasksToProcess = tasksToProcess; + } + + @Override + public Mono fireAndForget(Payload payload) { + logger.info("Received a Task[{}] from Client.ID[{}]", payload.getDataUtf8(), id); + tasksToProcess.onNext(new Task(id, payload.getDataUtf8())); + payload.release(); + return Mono.empty(); + } + + @Override + public void dispose() { + idToRSocketMap.remove(id, sendingSocket); + } + } + } + + static class Task { + final String id; + final String content; + + Task(String id, String content) { + this.id = id; + this.content = content; + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java new file mode 100644 index 000000000..49f683204 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java @@ -0,0 +1,217 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.examples.transport.tcp.lease; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.lease.Lease; +import io.rsocket.lease.LeaseStats; +import io.rsocket.lease.Leases; +import io.rsocket.lease.MissingLeaseException; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.ByteBufPayload; +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.function.Consumer; +import java.util.function.Function; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.ReplayProcessor; +import reactor.util.retry.Retry; + +public class LeaseExample { + + private static final Logger logger = LoggerFactory.getLogger(LeaseExample.class); + + private static final String SERVER_TAG = "server"; + private static final String CLIENT_TAG = "client"; + + public static void main(String[] args) { + // Queue for incoming messages represented as Flux + // Imagine that every fireAndForget that is pushed is processed by a worker + + int queueCapacity = 50; + BlockingQueue messagesQueue = new ArrayBlockingQueue<>(queueCapacity); + + // emulating a worker that process data from the queue + Thread workerThread = + new Thread( + () -> { + try { + while (!Thread.currentThread().isInterrupted()) { + String message = messagesQueue.take(); + logger.info("Process message {}", message); + Thread.sleep(500); // emulating processing + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + workerThread.start(); + + CloseableChannel server = + RSocketServer.create( + (setup, sendingSocket) -> + Mono.just( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + // add element. if overflows errors and terminates execution + // specifically to show that lease can limit rate of fnf requests in + // that example + try { + if (!messagesQueue.offer(payload.getDataUtf8())) { + logger.error("Queue has been overflowed. Terminating execution"); + sendingSocket.dispose(); + workerThread.interrupt(); + } + } finally { + payload.release(); + } + return Mono.empty(); + } + })) + .lease(() -> Leases.create().sender(new LeaseCalculator(SERVER_TAG, messagesQueue))) + .bindNow(TcpServerTransport.create("localhost", 7000)); + + LeaseReceiver receiver = new LeaseReceiver(CLIENT_TAG); + RSocket clientRSocket = + RSocketConnector.create() + .lease(() -> Leases.create().receiver(receiver)) + .connect(TcpClientTransport.create(server.address())) + .block(); + + Objects.requireNonNull(clientRSocket); + + // generate stream of fnfs + Flux.generate( + () -> 0L, + (state, sink) -> { + sink.next(state); + return state + 1; + }) + // here we wait for the first lease for the responder side and start execution + // on if there is allowance + .delaySubscription(receiver.notifyWhenNewLease().then()) + .concatMap( + tick -> { + logger.info("Requesting FireAndForget({})", tick); + return Mono.defer(() -> clientRSocket.fireAndForget(ByteBufPayload.create("" + tick))) + .retryWhen( + Retry.indefinitely() + // ensures that error is the result of missed lease + .filter(t -> t instanceof MissingLeaseException) + .doBeforeRetryAsync( + rs -> { + // here we create a mechanism to delay the retry until + // the new lease allowance comes in. + logger.info("Ran out of leases {}", rs); + return receiver.notifyWhenNewLease().then(); + })); + }) + .blockLast(); + + clientRSocket.onClose().block(); + server.dispose(); + } + + /** + * This is a class responsible for making decision on whether Responder is ready to receive new + * FireAndForget or not base in the number of messages enqueued.
+ * In the nutshell this is responder-side rate-limiter logic which is created for every new + * connection.
+ * In real-world projects this class has to issue leases based on real metrics + */ + private static class LeaseCalculator implements Function, Flux> { + final String tag; + final BlockingQueue queue; + + public LeaseCalculator(String tag, BlockingQueue queue) { + this.tag = tag; + this.queue = queue; + } + + @Override + public Flux apply(Optional leaseStats) { + logger.info("{} stats are {}", tag, leaseStats.isPresent() ? "present" : "absent"); + Duration ttlDuration = Duration.ofSeconds(5); + // The interval function is used only for the demo purpose and should not be + // considered as the way to issue leases. + // For advanced RateLimiting with Leasing + // consider adopting https://github.com/Netflix/concurrency-limits#server-limiter + return Flux.interval(Duration.ZERO, ttlDuration.dividedBy(2)) + .handle( + (__, sink) -> { + // put queue.remainingCapacity() + 1 here if you want to observe that app is + // terminated because of the queue overflowing + int requests = queue.remainingCapacity(); + + // reissue new lease only if queue has remaining capacity to + // accept more requests + if (requests > 0) { + long ttl = ttlDuration.toMillis(); + sink.next(Lease.create((int) ttl, requests)); + } + }); + } + } + + /** + * Requester-side Lease listener.
+ * In the nutshell this class implements mechanism to listen (and do appropriate actions as + * needed) to incoming leases issued by the Responder + */ + private static class LeaseReceiver implements Consumer> { + final String tag; + final ReplayProcessor lastLeaseReplay = ReplayProcessor.cacheLast(); + + public LeaseReceiver(String tag) { + this.tag = tag; + } + + @Override + public void accept(Flux receivedLeases) { + receivedLeases.subscribe( + l -> { + logger.info( + "{} received leases - ttl: {}, requests: {}", + tag, + l.getTimeToLiveMillis(), + l.getAllowedRequests()); + lastLeaseReplay.onNext(l); + }); + } + + /** + * This method allows to listen to new incoming leases and delay some action (e.g . retry) until + * new valid lease has come in + */ + public Mono notifyWhenNewLease() { + return lastLeaseReplay.filter(l -> l.isValid()).next(); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/plugins/LimitRateInterceptorExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/plugins/LimitRateInterceptorExample.java new file mode 100644 index 000000000..67a85b67f --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/plugins/LimitRateInterceptorExample.java @@ -0,0 +1,84 @@ +package io.rsocket.examples.transport.tcp.plugins; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.plugins.LimitRateInterceptor; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +public class LimitRateInterceptorExample { + + private static final Logger logger = LoggerFactory.getLogger(LimitRateInterceptorExample.class); + + public static void main(String[] args) { + RSocketServer.create( + SocketAcceptor.with( + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + return Flux.interval(Duration.ofMillis(100)) + .doOnRequest( + e -> logger.debug("Server publisher receives request for " + e)) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + .doOnRequest( + e -> logger.debug("Server publisher receives request for " + e)); + } + })) + .interceptors(registry -> registry.forResponder(LimitRateInterceptor.forResponder(64))) + .bind(TcpServerTransport.create("localhost", 7000)) + .subscribe(); + + RSocket socket = + RSocketConnector.create() + .interceptors(registry -> registry.forRequester(LimitRateInterceptor.forRequester(64))) + .connect(TcpClientTransport.create("localhost", 7000)) + .block(); + + logger.debug( + "\n\nStart of requestStream interaction\n" + "----------------------------------\n"); + + socket + .requestStream(DefaultPayload.create("Hello")) + .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); + + logger.debug( + "\n\nStart of requestChannel interaction\n" + "-----------------------------------\n"); + + socket + .requestChannel( + Flux.generate( + () -> 1L, + (s, sink) -> { + sink.next(DefaultPayload.create("Next " + s)); + return ++s; + }) + .doOnRequest(e -> logger.debug("Client publisher receives request for " + e))) + .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .doFinally(signalType -> socket.dispose()) + .then() + .block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/requestresponse/HelloWorldClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/requestresponse/HelloWorldClient.java index 0e965e362..85faeee82 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/requestresponse/HelloWorldClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/requestresponse/HelloWorldClient.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,67 +16,55 @@ package io.rsocket.examples.transport.tcp.requestresponse; -import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.TcpServerTransport; -import io.rsocket.util.PayloadImpl; -import java.nio.charset.StandardCharsets; +import io.rsocket.util.DefaultPayload; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; public final class HelloWorldClient { + private static final Logger logger = LoggerFactory.getLogger(HelloWorldClient.class); + public static void main(String[] args) { - RSocketFactory.receive() - .acceptor( - (setupPayload, reactiveSocket) -> - Mono.just( - new AbstractRSocket() { - boolean fail = true; - @Override - public Mono requestResponse(Payload p) { - if (fail) { - fail = false; - return Mono.error(new Throwable()); - } else { - return Mono.just(p); - } - } - })) - .transport(TcpServerTransport.create("localhost", 7000)) - .start() - .subscribe(); + RSocket rsocket = + new RSocket() { + boolean fail = true; - RSocket socket = - RSocketFactory.connect() - .transport(TcpClientTransport.create("localhost", 7000)) - .start() - .block(); + @Override + public Mono requestResponse(Payload p) { + if (fail) { + fail = false; + return Mono.error(new Throwable("Simulated error")); + } else { + return Mono.just(p); + } + } + }; - socket - .requestResponse(new PayloadImpl("Hello")) - .map(payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString()) - .onErrorReturn("error") - .doOnNext(System.out::println) - .block(); + RSocketServer.create(SocketAcceptor.with(rsocket)) + .bind(TcpServerTransport.create("localhost", 7000)) + .subscribe(); - socket - .requestResponse(new PayloadImpl("Hello")) - .map(payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString()) - .onErrorReturn("error") - .doOnNext(System.out::println) - .block(); + RSocket socket = + RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000)).block(); - socket - .requestResponse(new PayloadImpl("Hello")) - .map(payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString()) - .onErrorReturn("error") - .doOnNext(System.out::println) - .block(); + for (int i = 0; i < 3; i++) { + socket + .requestResponse(DefaultPayload.create("Hello")) + .map(Payload::getDataUtf8) + .onErrorReturn("error") + .doOnNext(logger::debug) + .block(); + } - socket.close().block(); + socket.dispose(); } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/Files.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/Files.java new file mode 100644 index 000000000..6724ca93f --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/Files.java @@ -0,0 +1,141 @@ +package io.rsocket.examples.transport.tcp.resume; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.rsocket.Payload; +import java.io.BufferedInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.SynchronousSink; + +class Files { + private static final Logger logger = LoggerFactory.getLogger(Files.class); + + public static Flux fileSource(String fileName, int chunkSizeBytes) { + return Flux.generate( + () -> new FileState(fileName, chunkSizeBytes), FileState::consumeNext, FileState::dispose); + } + + public static Subscriber fileSink(String fileName, int windowSize) { + return new Subscriber() { + Subscription s; + int requests = windowSize; + OutputStream outputStream; + int receivedBytes; + int receivedCount; + + @Override + public void onSubscribe(Subscription s) { + this.s = s; + this.s.request(requests); + } + + @Override + public void onNext(Payload payload) { + ByteBuf data = payload.data(); + receivedBytes += data.readableBytes(); + receivedCount += 1; + logger.debug("Received file chunk: " + receivedCount + ". Total size: " + receivedBytes); + if (outputStream == null) { + outputStream = open(fileName); + } + write(outputStream, data); + payload.release(); + + requests--; + if (requests == windowSize / 2) { + requests += windowSize; + s.request(windowSize); + } + } + + private void write(OutputStream outputStream, ByteBuf byteBuf) { + try { + byteBuf.readBytes(outputStream, byteBuf.readableBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void onError(Throwable t) { + close(outputStream); + } + + @Override + public void onComplete() { + close(outputStream); + } + + private OutputStream open(String filename) { + try { + /*do not buffer for demo purposes*/ + return new FileOutputStream(filename); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + + private void close(OutputStream stream) { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + } + } + } + }; + } + + private static class FileState { + private final String fileName; + private final int chunkSizeBytes; + private BufferedInputStream inputStream; + private byte[] chunkBytes; + + public FileState(String fileName, int chunkSizeBytes) { + this.fileName = fileName; + this.chunkSizeBytes = chunkSizeBytes; + } + + public FileState consumeNext(SynchronousSink sink) { + if (inputStream == null) { + InputStream in = getClass().getClassLoader().getResourceAsStream(fileName); + if (in == null) { + sink.error(new FileNotFoundException(fileName)); + return this; + } + this.inputStream = new BufferedInputStream(in); + this.chunkBytes = new byte[chunkSizeBytes]; + } + try { + int consumedBytes = inputStream.read(chunkBytes); + if (consumedBytes == -1) { + sink.complete(); + } else { + sink.next(Unpooled.copiedBuffer(chunkBytes, 0, consumedBytes)); + } + } catch (IOException e) { + sink.error(e); + } + return this; + } + + public void dispose() { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + } + } + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java new file mode 100644 index 000000000..93b54e146 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java @@ -0,0 +1,118 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.examples.transport.tcp.resume; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.core.Resume; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.util.retry.Retry; + +public class ResumeFileTransfer { + + /*amount of file chunks requested by subscriber: n, refilled on n/2 of received items*/ + private static final int PREFETCH_WINDOW_SIZE = 4; + private static final Logger logger = LoggerFactory.getLogger(ResumeFileTransfer.class); + + public static void main(String[] args) { + + Resume resume = + new Resume() + .sessionDuration(Duration.ofMinutes(5)) + .retry( + Retry.fixedDelay(Long.MAX_VALUE, Duration.ofSeconds(1)) + .doBeforeRetry(s -> logger.debug("Disconnected. Trying to resume..."))); + + RequestCodec codec = new RequestCodec(); + + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> { + Request request = codec.decode(payload); + payload.release(); + String fileName = request.getFileName(); + int chunkSize = request.getChunkSize(); + + Flux ticks = Flux.interval(Duration.ofMillis(500)).onBackpressureDrop(); + + return Files.fileSource(fileName, chunkSize) + .map(DefaultPayload::create) + .zipWith(ticks, (p, tick) -> p); + })) + .resume(resume) + .bind(TcpServerTransport.create("localhost", 8000)) + .block(); + + RSocket client = + RSocketConnector.create() + .resume(resume) + .connect(TcpClientTransport.create("localhost", 8001)) + .block(); + + client + .requestStream(codec.encode(new Request(16, "lorem.txt"))) + .doFinally(s -> server.dispose()) + .subscribe(Files.fileSink("rsocket-examples/out/lorem_output.txt", PREFETCH_WINDOW_SIZE)); + + server.onClose().block(); + } + + private static class RequestCodec { + + public Payload encode(Request request) { + String encoded = request.getChunkSize() + ":" + request.getFileName(); + return DefaultPayload.create(encoded); + } + + public Request decode(Payload payload) { + String encoded = payload.getDataUtf8(); + String[] chunkSizeAndFileName = encoded.split(":"); + int chunkSize = Integer.parseInt(chunkSizeAndFileName[0]); + String fileName = chunkSizeAndFileName[1]; + return new Request(chunkSize, fileName); + } + } + + private static class Request { + private final int chunkSize; + private final String fileName; + + public Request(int chunkSize, String fileName) { + this.chunkSize = chunkSize; + this.fileName = fileName; + } + + public int getChunkSize() { + return chunkSize; + } + + public String getFileName() { + return fileName; + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/readme.md b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/readme.md new file mode 100644 index 000000000..55e761fe8 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/readme.md @@ -0,0 +1,29 @@ +1. Start socat. It is used for emulation of transport disconnects + +`socat -d TCP-LISTEN:8001,fork,reuseaddr TCP:localhost:8000` + +2. start `ResumeFileTransfer.main` + +3. terminate/start socat periodically for session resumption + +`ResumeFileTransfer` output is as follows + +``` +Received file chunk: 7. Total size: 112 +Received file chunk: 8. Total size: 128 +Received file chunk: 9. Total size: 144 +Received file chunk: 10. Total size: 160 +Disconnected. Trying to resume connection... +Disconnected. Trying to resume connection... +Disconnected. Trying to resume connection... +Disconnected. Trying to resume connection... +Disconnected. Trying to resume connection... +Received file chunk: 11. Total size: 176 +Received file chunk: 12. Total size: 192 +Received file chunk: 13. Total size: 208 +Received file chunk: 14. Total size: 224 +Received file chunk: 15. Total size: 240 +Received file chunk: 16. Total size: 256 +``` + +It transfers file from `resources/lorem.txt` to `build/out/lorem_output.txt` in chunks of 16 bytes every 500 millis diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java new file mode 100644 index 000000000..8116ad4ae --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.examples.transport.tcp.stream; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +public final class ClientStreamingToServer { + + private static final Logger logger = LoggerFactory.getLogger(ClientStreamingToServer.class); + + public static void main(String[] args) { + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> + Flux.interval(Duration.ofMillis(100)) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)))) + .bind(TcpServerTransport.create("localhost", 7000)) + .subscribe(); + + RSocket socket = + RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000)).block(); + + socket + .requestStream(DefaultPayload.create("Hello")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .doFinally(signalType -> socket.dispose()) + .then() + .block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ServerStreamingToClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ServerStreamingToClient.java new file mode 100644 index 000000000..f5b1e00e5 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ServerStreamingToClient.java @@ -0,0 +1,61 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.examples.transport.tcp.stream; + +import static io.rsocket.SocketAcceptor.forRequestStream; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public final class ServerStreamingToClient { + + public static void main(String[] args) { + + RSocketServer.create( + (setup, rsocket) -> { + rsocket + .requestStream(DefaultPayload.create("Hello-Bidi")) + .map(Payload::getDataUtf8) + .log() + .subscribe(); + + return Mono.just(new RSocket() {}); + }) + .bind(TcpServerTransport.create("localhost", 7000)) + .subscribe(); + + RSocket rsocket = + RSocketConnector.create() + .acceptor( + forRequestStream( + payload -> + Flux.interval(Duration.ofSeconds(1)) + .map(aLong -> DefaultPayload.create("Bi-di Response => " + aLong)))) + .connect(TcpClientTransport.create("localhost", 7000)) + .block(); + + rsocket.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java deleted file mode 100644 index e12742f4e..000000000 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.stream; - -import io.rsocket.*; -import io.rsocket.transport.netty.client.TcpClientTransport; -import io.rsocket.transport.netty.server.TcpServerTransport; -import io.rsocket.util.PayloadImpl; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -public final class StreamingClient { - - public static void main(String[] args) { - RSocketFactory.receive() - .acceptor(new SocketAcceptorImpl()) - .transport(TcpServerTransport.create("localhost", 7000)) - .start() - .subscribe(); - - RSocket socket = - RSocketFactory.connect() - .transport(TcpClientTransport.create("localhost", 7000)) - .start() - .block(); - - socket - .requestStream(new PayloadImpl("Hello")) - .map(payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString()) - .doOnNext(System.out::println) - .take(10) - .thenEmpty(socket.close()) - .block(); - } - - private static class SocketAcceptorImpl implements SocketAcceptor { - @Override - public Mono accept(ConnectionSetupPayload setupPayload, RSocket reactiveSocket) { - return Mono.just( - new AbstractRSocket() { - @Override - public Flux requestStream(Payload payload) { - return Flux.interval(Duration.ofMillis(100)) - .map(aLong -> new PayloadImpl("Interval: " + aLong)); - } - }); - } - } -} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketHeadersSample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketHeadersSample.java new file mode 100644 index 000000000..72e003d2a --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketHeadersSample.java @@ -0,0 +1,99 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.examples.transport.ws; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.WebsocketDuplexConnection; +import io.rsocket.transport.netty.client.WebsocketClientTransport; +import io.rsocket.util.ByteBufPayload; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; +import reactor.netty.DisposableServer; +import reactor.netty.http.server.HttpServer; + +public class WebSocketHeadersSample { + + private static final Logger logger = LoggerFactory.getLogger(WebSocketHeadersSample.class); + + public static void main(String[] args) { + + ServerTransport.ConnectionAcceptor connectionAcceptor = + RSocketServer.create(SocketAcceptor.forRequestResponse(Mono::just)) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .asConnectionAcceptor(); + + DisposableServer server = + HttpServer.create() + .host("localhost") + .port(0) + .route( + routes -> + routes.get( + "/", + (req, res) -> { + if (req.requestHeaders().containsValue("Authorization", "test", true)) { + return res.sendWebsocket( + (in, out) -> + connectionAcceptor + .apply(new WebsocketDuplexConnection((Connection) in)) + .then(out.neverComplete())); + } + res.status(HttpResponseStatus.UNAUTHORIZED); + return res.send(); + })) + .bindNow(); + + logger.debug( + "\n\nStart of Authorized WebSocket Connection\n----------------------------------\n"); + + WebsocketClientTransport transport = + WebsocketClientTransport.create(server.host(), server.port()) + .header("Authorization", "test"); + + RSocket clientRSocket = + RSocketConnector.create() + .keepAlive(Duration.ofMinutes(10), Duration.ofMinutes(10)) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(transport) + .block(); + + Flux.range(1, 100) + .concatMap(i -> clientRSocket.requestResponse(ByteBufPayload.create("Hello " + i))) + .doOnNext(payload -> logger.debug("Processed " + payload.getDataUtf8())) + .blockLast(); + clientRSocket.dispose(); + + logger.debug( + "\n\nStart of Unauthorized WebSocket Upgrade\n----------------------------------\n"); + + RSocketConnector.create() + .keepAlive(Duration.ofMinutes(10), Duration.ofMinutes(10)) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(WebsocketClientTransport.create(server.host(), server.port())) + .block(); + } +} diff --git a/rsocket-examples/src/main/resources/logback.xml b/rsocket-examples/src/main/resources/logback.xml new file mode 100644 index 000000000..780a70c99 --- /dev/null +++ b/rsocket-examples/src/main/resources/logback.xml @@ -0,0 +1,34 @@ + + + + + + + + %d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] %c{1} - %m%n + + + + + + + + + + + + diff --git a/rsocket-examples/src/main/resources/lorem.txt b/rsocket-examples/src/main/resources/lorem.txt new file mode 100644 index 000000000..e035ea86d --- /dev/null +++ b/rsocket-examples/src/main/resources/lorem.txt @@ -0,0 +1,32 @@ +Alteration literature to or an sympathize mr imprudence. Of is ferrars subject as enjoyed or tedious cottage. +Procuring as in resembled by in agreeable. Next long no gave mr eyes. Admiration advantages no he celebrated so pianoforte unreserved. +Not its herself forming charmed amiable. Him why feebly expect future now. + +Situation admitting promotion at or to perceived be. Mr acuteness we as estimable enjoyment up. +An held late as felt know. Learn do allow solid to grave. Middleton suspicion age her attention. +Chiefly several bed its wishing. Is so moments on chamber pressed to. Doubtful yet way properly answered humanity its desirous. + Minuter believe service arrived civilly add all. Acuteness allowance an at eagerness favourite in extensive exquisite ye. + + Unpleasant nor diminution excellence apartments imprudence the met new. Draw part them he an to he roof only. + Music leave say doors him. Tore bred form if sigh case as do. Staying he no looking if do opinion. + Sentiments way understood end partiality and his. + + Ladyship it daughter securing procured or am moreover mr. Put sir she exercise vicinity cheerful wondered. + Continual say suspicion provision you neglected sir curiosity unwilling. Simplicity end themselves increasing led day sympathize yet. + General windows effects not are drawing man garrets. Common indeed garden you his ladies out yet. Preference imprudence contrasted to remarkably in on. + Taken now you him trees tears any. Her object giving end sister except oppose. + + No comfort do written conduct at prevent manners on. Celebrated contrasted discretion him sympathize her collecting occasional. + Do answered bachelor occasion in of offended no concerns. Supply worthy warmth branch of no ye. Voice tried known to as my to. + Though wished merits or be. Alone visit use these smart rooms ham. No waiting in on enjoyed placing it inquiry. + + So insisted received is occasion advanced honoured. Among ready to which up. Attacks smiling and may out assured moments man nothing outward. + Thrown any behind afford either the set depend one temper. Instrument melancholy in acceptance collecting frequently be if. + Zealously now pronounce existence add you instantly say offending. Merry their far had widen was. Concerns no in expenses raillery formerly. + + As am hastily invited settled at limited civilly fortune me. Really spring in extent an by. Judge but built gay party world. + Of so am he remember although required. Bachelor unpacked be advanced at. Confined in declared marianne is vicinity. + + In alteration insipidity impression by travelling reasonable up motionless. Of regard warmth by unable sudden garden ladies. + No kept hung am size spot no. Likewise led and dissuade rejoiced welcomed husbands boy. Do listening on he suspected resembled. + Water would still if to. Position boy required law moderate was may. \ No newline at end of file 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 5449f6df8..e2471f2fc 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -19,87 +19,103 @@ import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.plugins.DuplexConnectionInterceptor; import io.rsocket.plugins.RSocketInterceptor; +import io.rsocket.plugins.SocketAcceptorInterceptor; import io.rsocket.test.TestSubscriber; import io.rsocket.transport.netty.client.TcpClientTransport; -import io.rsocket.transport.netty.server.NettyContextCloseable; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; -import io.rsocket.util.PayloadImpl; +import io.rsocket.util.DefaultPayload; import io.rsocket.util.RSocketProxy; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class IntegrationTest { - private NettyContextCloseable server; - private RSocket client; - private AtomicInteger requestCount; - private CountDownLatch disconnectionCounter; - public static volatile boolean calledClient = false; - public static volatile boolean calledServer = false; - public static volatile boolean calledFrame = false; + private static final RSocketInterceptor requesterInterceptor; + private static final RSocketInterceptor responderInterceptor; + private static final SocketAcceptorInterceptor clientAcceptorInterceptor; + private static final SocketAcceptorInterceptor serverAcceptorInterceptor; + private static final DuplexConnectionInterceptor connectionInterceptor; - private static final RSocketInterceptor clientPlugin; - private static final RSocketInterceptor serverPlugin; - private static final DuplexConnectionInterceptor connectionPlugin; + private static volatile boolean calledRequester = false; + private static volatile boolean calledResponder = false; + private static volatile boolean calledClientAcceptor = false; + private static volatile boolean calledServerAcceptor = false; + private static volatile boolean calledFrame = false; static { - clientPlugin = + requesterInterceptor = reactiveSocket -> new RSocketProxy(reactiveSocket) { @Override public Mono requestResponse(Payload payload) { - calledClient = true; + calledRequester = true; return reactiveSocket.requestResponse(payload); } }; - serverPlugin = + responderInterceptor = reactiveSocket -> new RSocketProxy(reactiveSocket) { @Override public Mono requestResponse(Payload payload) { - calledServer = true; + calledResponder = true; return reactiveSocket.requestResponse(payload); } }; - connectionPlugin = + clientAcceptorInterceptor = + acceptor -> + (setup, sendingSocket) -> { + calledClientAcceptor = true; + return acceptor.accept(setup, sendingSocket); + }; + + serverAcceptorInterceptor = + acceptor -> + (setup, sendingSocket) -> { + calledServerAcceptor = true; + return acceptor.accept(setup, sendingSocket); + }; + + connectionInterceptor = (type, connection) -> { calledFrame = true; return connection; }; } + private CloseableChannel server; + private RSocket client; + private AtomicInteger requestCount; + private CountDownLatch disconnectionCounter; + private AtomicInteger errorCount; + @Before public void startup() { + errorCount = new AtomicInteger(); requestCount = new AtomicInteger(); disconnectionCounter = new CountDownLatch(1); - TcpServerTransport serverTransport = TcpServerTransport.create(0); - server = - RSocketFactory.receive() - .addServerPlugin(serverPlugin) - .addConnectionPlugin(connectionPlugin) - .acceptor( + RSocketServer.create( (setup, sendingSocket) -> { sendingSocket .onClose() @@ -107,60 +123,84 @@ public void startup() { .subscribe(); return Mono.just( - new AbstractRSocket() { + new RSocket() { @Override public Mono requestResponse(Payload payload) { - return Mono.just(new PayloadImpl("RESPONSE", "METADATA")) + return Mono.just(DefaultPayload.create("RESPONSE", "METADATA")) .doOnSubscribe(s -> requestCount.incrementAndGet()); } @Override public Flux requestStream(Payload payload) { - return Flux.range(1, 10_000).map(i -> new PayloadImpl("data -> " + i)); + return Flux.range(1, 10_000) + .map(i -> DefaultPayload.create("data -> " + i)); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads); } }); }) - .transport(serverTransport) - .start() - // TODO fix the Types through RSocketFactory.Start - .cast(NettyContextCloseable.class) + .interceptors( + registry -> + registry + .forResponder(responderInterceptor) + .forSocketAcceptor(serverAcceptorInterceptor) + .forConnection(connectionInterceptor)) + .bind(TcpServerTransport.create("localhost", 0)) .block(); client = - RSocketFactory.connect() - .addClientPlugin(clientPlugin) - .addConnectionPlugin(connectionPlugin) - .transport(TcpClientTransport.create(server.address())) - .start() + RSocketConnector.create() + .interceptors( + registry -> + registry + .forRequester(requesterInterceptor) + .forSocketAcceptor(clientAcceptorInterceptor) + .forConnection(connectionInterceptor)) + .connect(TcpClientTransport.create(server.address())) .block(); } @After public void teardown() { - server.close().block(); + server.dispose(); } @Test(timeout = 5_000L) public void testRequest() { - client.requestResponse(new PayloadImpl("REQUEST", "META")).block(); + client.requestResponse(DefaultPayload.create("REQUEST", "META")).block(); assertThat("Server did not see the request.", requestCount.get(), is(1)); - assertTrue(calledClient); - assertTrue(calledServer); + assertTrue(calledRequester); + assertTrue(calledResponder); + assertTrue(calledClientAcceptor); + assertTrue(calledServerAcceptor); assertTrue(calledFrame); } - @Test - public void testStream() throws Exception { - Subscriber subscriber = TestSubscriber.createCancelling(); - client.requestStream(new PayloadImpl("start")).subscribe(subscriber); + @Test(timeout = 5_000L) + public void testStream() { + Subscriber subscriber = TestSubscriber.createCancelling(); + client.requestStream(DefaultPayload.create("start")).subscribe(subscriber); verify(subscriber).onSubscribe(any()); verifyNoMoreInteractions(subscriber); } @Test(timeout = 5_000L) - public void testClose() throws ExecutionException, InterruptedException, TimeoutException { - client.close().block(); + public void testClose() throws InterruptedException { + client.dispose(); disconnectionCounter.await(); } + + @Test // (timeout = 5_000L) + public void testCallRequestWithErrorAndThenRequest() { + try { + client.requestChannel(Mono.error(new Throwable())).blockLast(); + } catch (Throwable t) { + } + + testRequest(); + } } diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java new file mode 100644 index 000000000..48e5baaa7 --- /dev/null +++ b/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java @@ -0,0 +1,104 @@ +package io.rsocket.integration; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.test.SlowTest; +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.function.Supplier; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +public class InteractionsLoadTest { + + @Test + @SlowTest + public void channel() { + CloseableChannel server = + RSocketServer.create(SocketAcceptor.with(new EchoRSocket())) + .bind(TcpServerTransport.create("localhost", 0)) + .block(Duration.ofSeconds(10)); + + RSocket clientRSocket = + RSocketConnector.connectWith(TcpClientTransport.create(server.address())) + .block(Duration.ofSeconds(10)); + + int concurrency = 16; + Flux.range(1, concurrency) + .flatMap( + v -> + clientRSocket + .requestChannel( + input().onBackpressureDrop().map(iv -> DefaultPayload.create("foo"))) + .limitRate(10000), + concurrency) + .timeout(Duration.ofSeconds(5)) + .doOnNext( + p -> { + String data = p.getDataUtf8(); + if (!data.equals("bar")) { + throw new IllegalStateException("Channel Client Bad message: " + data); + } + }) + .window(Duration.ofSeconds(1)) + .flatMap(Flux::count) + .doOnNext(d -> System.out.println("Got: " + d)) + .take(Duration.ofMinutes(1)) + .doOnTerminate(server::dispose) + .subscribe(); + + server.onClose().block(); + } + + private static Flux input() { + Flux interval = Flux.interval(Duration.ofMillis(1)).onBackpressureDrop(); + for (int i = 0; i < 10; i++) { + interval = interval.mergeWith(interval); + } + return interval; + } + + private static class EchoRSocket implements RSocket { + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + .map( + p -> { + String data = p.getDataUtf8(); + if (!data.equals("foo")) { + throw new IllegalStateException("Channel Server Bad message: " + data); + } + return DefaultPayload.create("bar"); + }); + } + + @Override + public Flux requestStream(Payload payload) { + return Flux.just(payload) + .map( + p -> { + String data = p.getDataUtf8(); + return data; + }) + .doOnNext( + (data) -> { + if (!data.equals("foo")) { + throw new IllegalStateException("Stream Server Bad message: " + data); + } + }) + .flatMap( + data -> { + Supplier p = () -> DefaultPayload.create("bar"); + return Flux.range(1, 100).map(v -> p.get()); + }); + } + } +} 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 9dcf45e07..de27bcb9b 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java @@ -1,17 +1,17 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; @@ -19,17 +19,16 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import io.rsocket.AbstractRSocket; -import io.rsocket.Closeable; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.transport.netty.client.TcpClientTransport; -import io.rsocket.transport.netty.server.NettyContextCloseable; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; -import io.rsocket.util.PayloadImpl; +import io.rsocket.util.DefaultPayload; +import io.rsocket.util.EmptyPayload; import io.rsocket.util.RSocketProxy; -import java.nio.charset.StandardCharsets; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import org.junit.After; @@ -41,41 +40,31 @@ import reactor.core.scheduler.Schedulers; public class TcpIntegrationTest { - private AbstractRSocket handler; + private RSocket handler; - private NettyContextCloseable server; + private CloseableChannel server; @Before public void startup() { - TcpServerTransport serverTransport = TcpServerTransport.create(0); - RSocketFactory.Start transport = - RSocketFactory.receive() - .acceptor((setup, sendingSocket) -> Mono.just(new RSocketProxy(handler))) - .transport(serverTransport); server = - transport - .start() - // TODO fix the Types through RSocketFactory.Start - .cast(NettyContextCloseable.class) + RSocketServer.create((setup, sendingSocket) -> Mono.just(new RSocketProxy(handler))) + .bind(TcpServerTransport.create("localhost", 0)) .block(); } private RSocket buildClient() { - return RSocketFactory.connect() - .transport(TcpClientTransport.create(server.address())) - .start() - .block(); + return RSocketConnector.connectWith(TcpClientTransport.create(server.address())).block(); } @After public void cleanup() { - server.close().block(); + server.dispose(); } - @Test(timeout = 5_000L) - public void testCompleteWithoutNext() throws InterruptedException { + @Test(timeout = 15_000L) + public void testCompleteWithoutNext() { handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { return Flux.empty(); @@ -83,49 +72,49 @@ public Flux requestStream(Payload payload) { }; RSocket client = buildClient(); Boolean hasElements = - client.requestStream(new PayloadImpl("REQUEST", "META")).log().hasElements().block(); + client.requestStream(DefaultPayload.create("REQUEST", "META")).log().hasElements().block(); assertFalse(hasElements); } - @Test(timeout = 5_000L) - public void testSingleStream() throws InterruptedException { + @Test(timeout = 15_000L) + public void testSingleStream() { handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { - return Flux.just(new PayloadImpl("RESPONSE", "METADATA")); + return Flux.just(DefaultPayload.create("RESPONSE", "METADATA")); } }; RSocket client = buildClient(); - Payload result = client.requestStream(new PayloadImpl("REQUEST", "META")).blockLast(); + Payload result = client.requestStream(DefaultPayload.create("REQUEST", "META")).blockLast(); - assertEquals("RESPONSE", StandardCharsets.UTF_8.decode(result.getData()).toString()); + assertEquals("RESPONSE", result.getDataUtf8()); } - @Test(timeout = 5_000L) - public void testZeroPayload() throws InterruptedException { + @Test(timeout = 15_000L) + public void testZeroPayload() { handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { - return Flux.just(PayloadImpl.EMPTY); + return Flux.just(EmptyPayload.INSTANCE); } }; RSocket client = buildClient(); - Payload result = client.requestStream(new PayloadImpl("REQUEST", "META")).blockFirst(); + Payload result = client.requestStream(DefaultPayload.create("REQUEST", "META")).blockFirst(); - assertEquals("", StandardCharsets.UTF_8.decode(result.getData()).toString()); + assertEquals("", result.getDataUtf8()); } - @Test(timeout = 5_000L) - public void testRequestResponseErrors() throws InterruptedException { + @Test(timeout = 15_000L) + public void testRequestResponseErrors() { handler = - new AbstractRSocket() { + new RSocket() { boolean first = true; @Override @@ -134,7 +123,7 @@ public Mono requestResponse(Payload payload) { first = false; return Mono.error(new RuntimeException("EX")); } else { - return Mono.just(new PayloadImpl("SUCCESS")); + return Mono.just(DefaultPayload.create("SUCCESS")); } } }; @@ -143,20 +132,20 @@ public Mono requestResponse(Payload payload) { Payload response1 = client - .requestResponse(new PayloadImpl("REQUEST", "META")) - .onErrorReturn(new PayloadImpl("ERROR")) + .requestResponse(DefaultPayload.create("REQUEST", "META")) + .onErrorReturn(DefaultPayload.create("ERROR")) .block(); Payload response2 = client - .requestResponse(new PayloadImpl("REQUEST", "META")) - .onErrorReturn(new PayloadImpl("ERROR")) + .requestResponse(DefaultPayload.create("REQUEST", "META")) + .onErrorReturn(DefaultPayload.create("ERROR")) .block(); - assertEquals("ERROR", StandardCharsets.UTF_8.decode(response1.getData()).toString()); - assertEquals("SUCCESS", StandardCharsets.UTF_8.decode(response2.getData()).toString()); + assertEquals("ERROR", response1.getDataUtf8()); + assertEquals("SUCCESS", response2.getDataUtf8()); } - @Test(timeout = 5_000L) + @Test(timeout = 15_000L) public void testTwoConcurrentStreams() throws InterruptedException { ConcurrentHashMap> map = new ConcurrentHashMap<>(); UnicastProcessor processor1 = UnicastProcessor.create(); @@ -165,18 +154,17 @@ public void testTwoConcurrentStreams() throws InterruptedException { map.put("REQUEST2", processor2); handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { - String s = StandardCharsets.UTF_8.decode(payload.getData()).toString(); - return map.get(s); + return map.get(payload.getDataUtf8()); } }; RSocket client = buildClient(); - Flux response1 = client.requestStream(new PayloadImpl("REQUEST1")); - Flux response2 = client.requestStream(new PayloadImpl("REQUEST2")); + Flux response1 = client.requestStream(DefaultPayload.create("REQUEST1")); + Flux response2 = client.requestStream(DefaultPayload.create("REQUEST2")); CountDownLatch nextCountdown = new CountDownLatch(2); CountDownLatch completeCountdown = new CountDownLatch(2); @@ -189,8 +177,8 @@ public Flux requestStream(Payload payload) { .subscribeOn(Schedulers.newSingle("2")) .subscribe(c -> nextCountdown.countDown(), t -> {}, completeCountdown::countDown); - processor1.onNext(new PayloadImpl("RESPONSE1A")); - processor2.onNext(new PayloadImpl("RESPONSE2A")); + processor1.onNext(DefaultPayload.create("RESPONSE1A")); + processor2.onNext(DefaultPayload.create("RESPONSE2A")); nextCountdown.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 new file mode 100644 index 000000000..7d34ba478 --- /dev/null +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java @@ -0,0 +1,124 @@ +/* + * 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; + +import io.rsocket.Closeable; +import io.rsocket.Payload; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.exceptions.ApplicationErrorException; +import io.rsocket.transport.local.LocalClientTransport; +import io.rsocket.transport.local.LocalServerTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; +import reactor.core.publisher.Flux; + +public class TestingStreaming { + LocalServerTransport serverTransport = LocalServerTransport.create("test"); + + @Test(expected = ApplicationErrorException.class) + public void testRangeButThrowException() { + Closeable server = null; + try { + server = + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> + Flux.range(1, 1000) + .doOnNext( + i -> { + if (i > 3) { + throw new RuntimeException("BOOM!"); + } + }) + .map(l -> DefaultPayload.create("l -> " + l)) + .cast(Payload.class))) + .bind(serverTransport) + .block(); + + Flux.range(1, 6).flatMap(i -> consumer("connection number -> " + i)).blockLast(); + System.out.println("here"); + + } finally { + server.dispose(); + } + } + + @Test + public void testRangeOfConsumers() { + Closeable server = null; + try { + server = + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> + Flux.range(1, 1000) + .map(l -> DefaultPayload.create("l -> " + l)) + .cast(Payload.class))) + .bind(serverTransport) + .block(); + + Flux.range(1, 6).flatMap(i -> consumer("connection number -> " + i)).blockLast(); + System.out.println("here"); + + } finally { + server.dispose(); + } + } + + private Flux consumer(String s) { + return RSocketConnector.connectWith(LocalClientTransport.create("test")) + .flatMapMany( + rSocket -> { + AtomicInteger count = new AtomicInteger(); + return Flux.range(1, 100) + .flatMap( + i -> rSocket.requestStream(DefaultPayload.create("i -> " + i)).take(100), 1); + }); + } + + @Test + public void testSingleConsumer() { + Closeable server = null; + try { + server = + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> + Flux.range(1, 10_000) + .map(l -> DefaultPayload.create("l -> " + l)) + .cast(Payload.class))) + .bind(serverTransport) + .block(); + + consumer("1").blockLast(); + + } finally { + server.dispose(); + } + } + + @Test + public void testFluxOnly() { + Flux longFlux = Flux.interval(Duration.ofMillis(1)).onBackpressureDrop(); + + Flux.range(1, 60).flatMap(i -> longFlux.take(1000)).blockLast(); + } +} diff --git a/rsocket-examples/src/test/java/io/rsocket/resume/DisconnectableClientTransport.java b/rsocket-examples/src/test/java/io/rsocket/resume/DisconnectableClientTransport.java new file mode 100644 index 000000000..5824918bc --- /dev/null +++ b/rsocket-examples/src/test/java/io/rsocket/resume/DisconnectableClientTransport.java @@ -0,0 +1,75 @@ +/* + * 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.resume; + +import io.rsocket.DuplexConnection; +import io.rsocket.transport.ClientTransport; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicReference; +import reactor.core.publisher.Mono; + +class DisconnectableClientTransport implements ClientTransport { + private final ClientTransport clientTransport; + private final AtomicReference curConnection = new AtomicReference<>(); + private long nextConnectPermitMillis; + + public DisconnectableClientTransport(ClientTransport clientTransport) { + this.clientTransport = clientTransport; + } + + @Override + public Mono connect() { + return Mono.defer( + () -> + now() < nextConnectPermitMillis + ? Mono.error(new ClosedChannelException()) + : clientTransport + .connect() + .map( + c -> { + if (curConnection.compareAndSet(null, c)) { + return c; + } else { + throw new IllegalStateException( + "Transport supports at most 1 connection"); + } + })); + } + + public void disconnect() { + disconnectFor(Duration.ZERO); + } + + public void disconnectPermanently() { + disconnectFor(Duration.ofDays(42)); + } + + public void disconnectFor(Duration cooldown) { + DuplexConnection cur = curConnection.getAndSet(null); + if (cur != null) { + nextConnectPermitMillis = now() + cooldown.toMillis(); + cur.dispose(); + } else { + throw new IllegalStateException("Trying to disconnect while not connected"); + } + } + + private static long now() { + return System.currentTimeMillis(); + } +} diff --git a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java new file mode 100644 index 000000000..b2dad0022 --- /dev/null +++ b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java @@ -0,0 +1,229 @@ +/* + * 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.resume; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.core.Resume; +import io.rsocket.exceptions.RejectedResumeException; +import io.rsocket.exceptions.UnsupportedSetupException; +import io.rsocket.test.SlowTest; +import io.rsocket.transport.ClientTransport; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.DefaultPayload; +import java.net.InetSocketAddress; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; +import reactor.util.retry.Retry; + +@SlowTest +public class ResumeIntegrationTest { + private static final String SERVER_HOST = "localhost"; + private static final int SERVER_PORT = 0; + + @Test + void timeoutOnPermanentDisconnect() { + CloseableChannel closeable = newServerRSocket().block(); + + DisconnectableClientTransport clientTransport = + new DisconnectableClientTransport(clientTransport(closeable.address())); + + int sessionDurationSeconds = 5; + RSocket rSocket = newClientRSocket(clientTransport, sessionDurationSeconds).block(); + + Mono.delay(Duration.ofSeconds(1)).subscribe(v -> clientTransport.disconnectPermanently()); + + StepVerifier.create( + rSocket.requestChannel(testRequest()).then().doFinally(s -> closeable.dispose())) + .expectError(ClosedChannelException.class) + .verify(Duration.ofSeconds(7)); + } + + @Test + public void reconnectOnDisconnect() { + CloseableChannel closeable = newServerRSocket().block(); + + DisconnectableClientTransport clientTransport = + new DisconnectableClientTransport(clientTransport(closeable.address())); + + int sessionDurationSeconds = 15; + RSocket rSocket = newClientRSocket(clientTransport, sessionDurationSeconds).block(); + + Flux.just(3, 20, 40, 75) + .flatMap(v -> Mono.delay(Duration.ofSeconds(v))) + .subscribe(v -> clientTransport.disconnectFor(Duration.ofSeconds(7))); + + AtomicInteger counter = new AtomicInteger(-1); + StepVerifier.create( + rSocket + .requestChannel(testRequest()) + .take(Duration.ofSeconds(600)) + .map(Payload::getDataUtf8) + .timeout(Duration.ofSeconds(12)) + .doOnNext(x -> throwOnNonContinuous(counter, x)) + .then() + .doFinally(s -> closeable.dispose())) + .expectComplete() + .verify(); + } + + @Test + public void reconnectOnMissingSession() { + + int serverSessionDuration = 2; + + CloseableChannel closeable = newServerRSocket(serverSessionDuration).block(); + + DisconnectableClientTransport clientTransport = + new DisconnectableClientTransport(clientTransport(closeable.address())); + int clientSessionDurationSeconds = 10; + + RSocket rSocket = newClientRSocket(clientTransport, clientSessionDurationSeconds).block(); + + Mono.delay(Duration.ofSeconds(1)) + .subscribe(v -> clientTransport.disconnectFor(Duration.ofSeconds(3))); + + StepVerifier.create( + rSocket.requestChannel(testRequest()).then().doFinally(s -> closeable.dispose())) + .expectError() + .verify(Duration.ofSeconds(5)); + + StepVerifier.create(rSocket.onClose()) + .expectErrorMatches( + err -> + err instanceof RejectedResumeException + && "unknown resume token".equals(err.getMessage())) + .verify(Duration.ofSeconds(5)); + } + + @Test + void serverMissingResume() { + CloseableChannel closeableChannel = + RSocketServer.create(SocketAcceptor.with(new TestResponderRSocket())) + .bind(serverTransport(SERVER_HOST, SERVER_PORT)) + .block(); + + RSocket rSocket = + RSocketConnector.create() + .resume(new Resume()) + .connect(clientTransport(closeableChannel.address())) + .block(); + + StepVerifier.create(rSocket.onClose().doFinally(s -> closeableChannel.dispose())) + .expectErrorMatches( + err -> + err instanceof UnsupportedSetupException + && "resume not supported".equals(err.getMessage())) + .verify(Duration.ofSeconds(5)); + + Assertions.assertThat(rSocket.isDisposed()).isTrue(); + } + + static ClientTransport clientTransport(InetSocketAddress address) { + return TcpClientTransport.create(address); + } + + static ServerTransport serverTransport(String host, int port) { + return TcpServerTransport.create(host, port); + } + + private static Flux testRequest() { + return Flux.interval(Duration.ofMillis(500)) + .map(v -> DefaultPayload.create("client_request")) + .onBackpressureDrop(); + } + + private void throwOnNonContinuous(AtomicInteger counter, String x) { + int curValue = Integer.parseInt(x); + int prevValue = counter.get(); + if (prevValue >= 0) { + int dif = curValue - prevValue; + if (dif != 1) { + throw new IllegalStateException( + String.format( + "Payload values are expected to be continuous numbers: %d %d", + prevValue, curValue)); + } + } + counter.set(curValue); + } + + private static Mono newClientRSocket( + DisconnectableClientTransport clientTransport, int sessionDurationSeconds) { + return RSocketConnector.create() + .resume( + new Resume() + .sessionDuration(Duration.ofSeconds(sessionDurationSeconds)) + .storeFactory(t -> new InMemoryResumableFramesStore("client", 500_000)) + .cleanupStoreOnKeepAlive() + .retry(Retry.fixedDelay(Long.MAX_VALUE, Duration.ofSeconds(1)))) + .keepAlive(Duration.ofSeconds(5), Duration.ofMinutes(5)) + .connect(clientTransport); + } + + private static Mono newServerRSocket() { + return newServerRSocket(15); + } + + private static Mono newServerRSocket(int sessionDurationSeconds) { + return RSocketServer.create(SocketAcceptor.with(new TestResponderRSocket())) + .resume( + new Resume() + .sessionDuration(Duration.ofSeconds(sessionDurationSeconds)) + .cleanupStoreOnKeepAlive() + .storeFactory(t -> new InMemoryResumableFramesStore("server", 500_000))) + .bind(serverTransport(SERVER_HOST, SERVER_PORT)); + } + + private static class TestResponderRSocket implements RSocket { + + AtomicInteger counter = new AtomicInteger(); + + @Override + public Flux requestChannel(Publisher payloads) { + return duplicate( + Flux.interval(Duration.ofMillis(1)) + .onBackpressureLatest() + .publishOn(Schedulers.elastic()), + 20) + .map(v -> DefaultPayload.create(String.valueOf(counter.getAndIncrement()))) + .takeUntilOther(Flux.from(payloads).then()); + } + + private Flux duplicate(Flux f, int n) { + Flux r = Flux.empty(); + for (int i = 0; i < n; i++) { + r = r.mergeWith(f); + } + return r; + } + } +} diff --git a/rsocket-examples/src/test/resources/log4j.properties b/rsocket-examples/src/test/resources/log4j.properties deleted file mode 100644 index 9d662b3c9..000000000 --- a/rsocket-examples/src/test/resources/log4j.properties +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright 2016 Netflix, Inc. -#

-# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -#

-# http://www.apache.org/licenses/LICENSE-2.0 -#

-# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on -# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -# -log4j.rootLogger=INFO, stdout - -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %5p [%t] (%F) - %m%n -log4j.logger.io.rsocket.FrameLogger=Debug \ No newline at end of file diff --git a/rsocket-examples/src/test/resources/logback-test.xml b/rsocket-examples/src/test/resources/logback-test.xml new file mode 100644 index 000000000..13e65b37d --- /dev/null +++ b/rsocket-examples/src/test/resources/logback-test.xml @@ -0,0 +1,33 @@ + + + + + + + + %d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] %c{1} - %m%n + + + + + + + + + + + diff --git a/rsocket-load-balancer/build.gradle b/rsocket-load-balancer/build.gradle index 8ce69b411..d70e5b2cc 100644 --- a/rsocket-load-balancer/build.gradle +++ b/rsocket-load-balancer/build.gradle @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,7 +14,26 @@ * limitations under the License. */ +plugins { + id 'java-library' + id 'maven-publish' + id 'signing' +} + dependencies { - compile project(':rsocket-core') - testCompile project(':rsocket-test') + api project(':rsocket-core') + + implementation 'org.slf4j:slf4j-api' + + testImplementation project(':rsocket-test') + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'org.mockito:mockito-core' + + // TODO: Remove after JUnit5 migration + testCompileOnly 'junit:junit' + testImplementation 'org.hamcrest:hamcrest-library' + testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' + testRuntimeOnly 'ch.qos.logback:logback-classic' } + +description = 'Transparent Load Balancer for RSocket' diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java index 1f4f5c536..79186ddc6 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,45 +13,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.client; -import io.rsocket.Availability; -import io.rsocket.Closeable; -import io.rsocket.Payload; -import io.rsocket.RSocket; +import io.rsocket.*; import io.rsocket.client.filter.RSocketSupplier; -import io.rsocket.exceptions.NoAvailableRSocketException; -import io.rsocket.exceptions.TimeoutException; -import io.rsocket.exceptions.TransportException; import io.rsocket.stat.Ewma; import io.rsocket.stat.FrugalQuantile; import io.rsocket.stat.Median; import io.rsocket.stat.Quantile; import io.rsocket.util.Clock; -import io.rsocket.util.RSocketProxy; import java.nio.channels.ClosedChannelException; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; +import java.util.Optional; import java.util.Random; -import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; -import reactor.core.publisher.FluxSource; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; -import reactor.core.publisher.MonoSource; -import reactor.core.publisher.Operators; +import reactor.util.retry.Retry; /** * An implementation of {@link Mono} that load balances across a pool of RSockets and emits one when @@ -62,8 +53,6 @@ public abstract class LoadBalancedRSocketMono extends Mono implements Availability, Closeable { - private static final Logger logger = LoggerFactory.getLogger(LoadBalancedRSocketMono.class); - public static final double DEFAULT_EXP_FACTOR = 4.0; public static final double DEFAULT_LOWER_QUANTILE = 0.2; public static final double DEFAULT_HIGHER_QUANTILE = 0.8; @@ -73,39 +62,36 @@ public abstract class LoadBalancedRSocketMono extends Mono public static final int DEFAULT_MAX_APERTURE = 100; public static final long DEFAULT_MAX_REFRESH_PERIOD_MS = TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES); - + private static final Logger logger = LoggerFactory.getLogger(LoadBalancedRSocketMono.class); private static final long APERTURE_REFRESH_PERIOD = Clock.unit().convert(15, TimeUnit.SECONDS); private static final int EFFORT = 5; private static final long DEFAULT_INITIAL_INTER_ARRIVAL_TIME = Clock.unit().convert(1L, TimeUnit.SECONDS); private static final int DEFAULT_INTER_ARRIVAL_FACTOR = 500; + private static final FailingRSocket FAILING_REACTIVE_SOCKET = new FailingRSocket(); + protected final Mono rSocketMono; private final double minPendings; private final double maxPendings; private final int minAperture; private final int maxAperture; private final long maxRefreshPeriod; - private final double expFactor; private final Quantile lowerQuantile; private final Quantile higherQuantile; - - private int pendingSockets; private final ArrayList activeSockets; - private final ArrayList activeFactories; - private final FactoriesRefresher factoryRefresher; - private final Mono selectSocket; - private final Ewma pendings; + private final MonoProcessor onClose = MonoProcessor.create(); + private final RSocketSupplierPool pool; + private final long weightedSocketRetries; + private final Duration weightedSocketBackOff; + private final Duration weightedSocketMaxBackOff; private volatile int targetAperture; private long lastApertureRefresh; private long refreshPeriod; + private int pendingSockets; private volatile long lastRefresh; - private final MonoProcessor onClose = MonoProcessor.create(); - protected final MonoProcessor started = MonoProcessor.create(); - protected final Mono rSocketMono; - /** * @param factories the source (factories) of RSocket * @param expFactor how aggressive is the algorithm toward outliers. A higher number means we send @@ -122,6 +108,11 @@ public abstract class LoadBalancedRSocketMono extends Mono * load. * @param maxRefreshPeriodMs the maximum time between two "refreshes" of the list of active * RSocket. This is at that time that the slowest RSocket is closed. (unit is millisecond) + * @param weightedSocketRetries the number of times a weighted socket will attempt to retry when + * it receives an error before reconnecting. The default is 5 times. + * @param weightedSocketBackOff the duration a a weighted socket will add to each retry attempt. + * @param weightedSocketMaxBackOff the max duration a weighted socket will delay before retrying + * to connect. The default is 5 seconds. */ private LoadBalancedRSocketMono( Publisher> factories, @@ -132,16 +123,19 @@ private LoadBalancedRSocketMono( double maxPendings, int minAperture, int maxAperture, - long maxRefreshPeriodMs) { + long maxRefreshPeriodMs, + long weightedSocketRetries, + Duration weightedSocketBackOff, + Duration weightedSocketMaxBackOff) { + this.weightedSocketRetries = weightedSocketRetries; + this.weightedSocketBackOff = weightedSocketBackOff; + this.weightedSocketMaxBackOff = weightedSocketMaxBackOff; this.expFactor = expFactor; this.lowerQuantile = new FrugalQuantile(lowQuantile); this.higherQuantile = new FrugalQuantile(highQuantile); this.activeSockets = new ArrayList<>(); - this.activeFactories = new ArrayList<>(); this.pendingSockets = 0; - this.factoryRefresher = new FactoriesRefresher(); - this.selectSocket = Mono.fromCallable(this::select); this.minPendings = minPendings; this.maxPendings = maxPendings; @@ -155,15 +149,12 @@ private LoadBalancedRSocketMono( this.lastApertureRefresh = Clock.now(); this.refreshPeriod = Clock.unit().convert(15L, TimeUnit.SECONDS); this.lastRefresh = Clock.now(); + this.pool = new RSocketSupplierPool(factories); + refreshSockets(); - factories.subscribe(factoryRefresher); + rSocketMono = Mono.fromSupplier(this::select); - rSocketMono = - Mono.create( - sink -> { - RSocket rSocket = select(); - sink.success(rSocket); - }); + onClose.doFinally(signalType -> pool.dispose()).subscribe(); } public static LoadBalancedRSocketMono create( @@ -180,6 +171,39 @@ public static LoadBalancedRSocketMono create( DEFAULT_MAX_REFRESH_PERIOD_MS); } + public static LoadBalancedRSocketMono create( + Publisher> factories, + double expFactor, + double lowQuantile, + double highQuantile, + double minPendings, + double maxPendings, + int minAperture, + int maxAperture, + long maxRefreshPeriodMs, + long weightedSocketRetries, + Duration weightedSocketBackOff, + Duration weightedSocketMaxBackOff) { + return new LoadBalancedRSocketMono( + factories, + expFactor, + lowQuantile, + highQuantile, + minPendings, + maxPendings, + minAperture, + maxAperture, + maxRefreshPeriodMs, + weightedSocketRetries, + weightedSocketBackOff, + weightedSocketMaxBackOff) { + @Override + public void subscribe(CoreSubscriber s) { + rSocketMono.subscribe(s); + } + }; + } + public static LoadBalancedRSocketMono create( Publisher> factories, double expFactor, @@ -199,72 +223,65 @@ public static LoadBalancedRSocketMono create( maxPendings, minAperture, maxAperture, - maxRefreshPeriodMs) { + maxRefreshPeriodMs, + 5, + Duration.ofMillis(500), + Duration.ofSeconds(5)) { @Override - public void subscribe(Subscriber s) { - started.thenMany(rSocketMono).subscribe(s); + public void subscribe(CoreSubscriber s) { + rSocketMono.subscribe(s); } }; } + /** + * Responsible for: - refreshing the aperture - asynchronously adding/removing reactive sockets to + * match targetAperture - periodically append a new connection + */ + private synchronized void refreshSockets() { + refreshAperture(); + int n = activeSockets.size(); + if (n < targetAperture && !pool.isPoolEmpty()) { + logger.debug( + "aperture {} is below target {}, adding {} sockets", + n, + targetAperture, + targetAperture - n); + addSockets(targetAperture - n); + } else if (targetAperture < activeSockets.size()) { + logger.debug("aperture {} is above target {}, quicking 1 socket", n, targetAperture); + quickSlowestRS(); + } + + long now = Clock.now(); + if (now - lastRefresh >= refreshPeriod) { + long prev = refreshPeriod; + refreshPeriod = (long) Math.min(refreshPeriod * 1.5, maxRefreshPeriod); + logger.debug("Bumping refresh period, {}->{}", prev / 1000, refreshPeriod / 1000); + lastRefresh = now; + addSockets(1); + } + } + private synchronized void addSockets(int numberOfNewSocket) { int n = numberOfNewSocket; - if (n > activeFactories.size()) { - n = activeFactories.size(); + int poolSize = pool.poolSize(); + if (n > poolSize) { + n = poolSize; logger.debug( "addSockets({}) restricted by the number of factories, i.e. addSockets({})", numberOfNewSocket, n); } - Random rng = ThreadLocalRandom.current(); - while (n > 0) { - int size = activeFactories.size(); - if (size == 1) { - RSocketSupplier factory = activeFactories.get(0); - if (factory.availability() > 0.0) { - activeFactories.remove(0); - pendingSockets++; - factory.get().subscribe(new SocketAdder(factory)); - } - break; - } - RSocketSupplier factory0 = null; - RSocketSupplier factory1 = null; - int i0 = 0; - int i1 = 0; - for (int i = 0; i < EFFORT; i++) { - i0 = rng.nextInt(size); - i1 = rng.nextInt(size - 1); - if (i1 >= i0) { - i1++; - } - factory0 = activeFactories.get(i0); - factory1 = activeFactories.get(i1); - if (factory0.availability() > 0.0 && factory1.availability() > 0.0) { - break; - } - } + for (int i = 0; i < n; i++) { + Optional optional = pool.get(); - if (factory0.availability() < factory1.availability()) { - n--; - pendingSockets++; - // cheaper to permute activeFactories.get(i1) with the last item and remove the last - // rather than doing a activeFactories.remove(i1) - if (i1 < size - 1) { - activeFactories.set(i1, activeFactories.get(size - 1)); - } - activeFactories.remove(size - 1); - factory1.get().subscribe(new SocketAdder(factory1)); + if (optional.isPresent()) { + RSocketSupplier supplier = optional.get(); + WeightedSocket socket = new WeightedSocket(supplier, lowerQuantile, higherQuantile); } else { - n--; - pendingSockets++; - // c.f. above - if (i0 < size - 1) { - activeFactories.set(i0, activeFactories.get(size - 1)); - } - activeFactories.remove(size - 1); - factory0.get().subscribe(new SocketAdder(factory0)); + break; } } } @@ -302,7 +319,7 @@ private void updateAperture(int newValue, long now) { int previous = targetAperture; targetAperture = newValue; targetAperture = Math.max(minAperture, targetAperture); - int maxAperture = Math.min(this.maxAperture, activeSockets.size() + activeFactories.size()); + int maxAperture = Math.min(this.maxAperture, activeSockets.size() + pool.poolSize()); targetAperture = Math.min(maxAperture, targetAperture); lastApertureRefresh = now; pendings.reset((minPendings + maxPendings) / 2); @@ -316,35 +333,6 @@ private void updateAperture(int newValue, long now) { } } - /** - * Responsible for: - refreshing the aperture - asynchronously adding/removing reactive sockets to - * match targetAperture - periodically append a new connection - */ - private synchronized void refreshSockets() { - refreshAperture(); - int n = pendingSockets + activeSockets.size(); - if (n < targetAperture && !activeFactories.isEmpty()) { - logger.debug( - "aperture {} is below target {}, adding {} sockets", - n, - targetAperture, - targetAperture - n); - addSockets(targetAperture - n); - } else if (targetAperture < activeSockets.size()) { - logger.debug("aperture {} is above target {}, quicking 1 socket", n, targetAperture); - quickSlowestRS(); - } - - long now = Clock.now(); - if (now - lastRefresh >= refreshPeriod) { - long prev = refreshPeriod; - refreshPeriod = (long) Math.min(refreshPeriod * 1.5, maxRefreshPeriod); - logger.debug("Bumping refresh period, {}->{}", prev / 1000, refreshPeriod / 1000); - lastRefresh = now; - addSockets(1); - } - } - private synchronized void quickSlowestRS() { if (activeSockets.size() <= 1) { return; @@ -368,21 +356,8 @@ private synchronized void quickSlowestRS() { } if (slowest != null) { - removeSocket(slowest, false); - } - } - - private synchronized void removeSocket(WeightedSocket socket, boolean refresh) { - try { - logger.debug("Removing socket: -> " + socket); - activeSockets.remove(socket); - activeFactories.add(socket.getFactory()); - socket.close().subscribe(); - if (refresh) { - refreshSockets(); - } - } catch (Exception e) { - logger.warn("Exception while closing a RSocket", e); + logger.debug("Disposing slowest WeightedSocket {}", slowest); + slowest.dispose(); } } @@ -400,10 +375,11 @@ public synchronized double availability() { } private synchronized RSocket select() { + refreshSockets(); + if (activeSockets.isEmpty()) { return FAILING_REACTIVE_SOCKET; } - refreshSockets(); int size = activeSockets.size(); if (size == 1) { @@ -425,7 +401,7 @@ private synchronized RSocket select() { if (rsc1.availability() > 0.0 && rsc2.availability() > 0.0) { break; } - if (i + 1 == EFFORT && !activeFactories.isEmpty()) { + if (i + 1 == EFFORT && !pool.isPoolEmpty()) { addSockets(1); } } @@ -472,7 +448,7 @@ public synchronized String toString() { return "LoadBalancer(a:" + activeSockets.size() + ", f: " - + activeFactories.size() + + pool.poolSize() + ", avgPendings=" + pendings.value() + ", targetAperture=" @@ -485,203 +461,33 @@ public synchronized String toString() { } @Override - public Mono onClose() { - return onClose; + public void dispose() { + synchronized (this) { + activeSockets.forEach(WeightedSocket::dispose); + activeSockets.clear(); + onClose.onComplete(); + } } @Override - public Mono close() { - return MonoSource.wrap( - subscriber -> { - subscriber.onSubscribe(Operators.emptySubscription()); - - synchronized (this) { - factoryRefresher.close(); - activeFactories.clear(); - AtomicInteger n = new AtomicInteger(activeSockets.size()); - - activeSockets.forEach( - rs -> { - rs.close() - .subscribe( - new Subscriber() { - @Override - public void onSubscribe(Subscription s) { - s.request(Long.MAX_VALUE); - } - - @Override - public void onNext(Void aVoid) {} - - @Override - public void onError(Throwable t) { - logger.warn("Exception while closing a RSocket", t); - onComplete(); - } - - @Override - public void onComplete() { - if (n.decrementAndGet() == 0) { - subscriber.onComplete(); - onClose.onComplete(); - } - } - }); - }); - } - }); - } - - /** - * This subscriber role is to subscribe to the list of server identifier, and update the factory - * list. - */ - private class FactoriesRefresher implements Subscriber> { - private Subscription subscription; - - @Override - public void onSubscribe(Subscription subscription) { - this.subscription = subscription; - subscription.request(Long.MAX_VALUE); - } - - @Override - public void onNext(Collection newFactories) { - synchronized (LoadBalancedRSocketMono.this) { - Set current = new HashSet<>(activeFactories.size() + activeSockets.size()); - current.addAll(activeFactories); - for (WeightedSocket socket : activeSockets) { - RSocketSupplier factory = socket.getFactory(); - current.add(factory); - } - - Set removed = new HashSet<>(current); - removed.removeAll(newFactories); - - Set added = new HashSet<>(newFactories); - added.removeAll(current); - - boolean changed = false; - Iterator it0 = activeSockets.iterator(); - while (it0.hasNext()) { - WeightedSocket socket = it0.next(); - if (removed.contains(socket.getFactory())) { - it0.remove(); - try { - changed = true; - socket.close(); - } catch (Exception e) { - logger.warn("Exception while closing a RSocket", e); - } - } - } - Iterator it1 = activeFactories.iterator(); - while (it1.hasNext()) { - RSocketSupplier factory = it1.next(); - if (removed.contains(factory)) { - it1.remove(); - changed = true; - } - } - - activeFactories.addAll(added); - - if (changed && logger.isDebugEnabled()) { - StringBuilder msgBuilder = new StringBuilder(); - msgBuilder.append("\nUpdated active factories (size: " + activeFactories.size() + ")\n"); - for (RSocketSupplier f : activeFactories) { - msgBuilder.append(" + ").append(f).append('\n'); - } - msgBuilder.append("Active sockets:\n"); - for (WeightedSocket socket : activeSockets) { - msgBuilder.append(" + ").append(socket).append('\n'); - } - logger.debug(msgBuilder.toString()); - } - } - refreshSockets(); - } - - @Override - public void onError(Throwable t) { - // TODO: retry - logger.error("Error refreshing RSocket factories. They would no longer be refreshed.", t); - } - - @Override - public void onComplete() { - // TODO: retry - logger.warn("RSocket factories source completed. They would no longer be refreshed."); - } - - void close() { - subscription.cancel(); - } + public boolean isDisposed() { + return onClose.isDisposed(); } - private class SocketAdder implements Subscriber { - private final RSocketSupplier factory; - - private int errors; - - private SocketAdder(RSocketSupplier factory) { - this.factory = factory; - } - - @Override - public void onSubscribe(Subscription s) { - s.request(1L); - } - - @Override - public void onNext(RSocket rs) { - synchronized (LoadBalancedRSocketMono.this) { - if (activeSockets.size() >= targetAperture) { - quickSlowestRS(); - } - - WeightedSocket weightedSocket = - new WeightedSocket(rs, factory, lowerQuantile, higherQuantile); - logger.debug("Adding new WeightedSocket {}", weightedSocket); - - activeSockets.add(weightedSocket); - started.onComplete(); - pendingSockets -= 1; - } - } - - @Override - public void onError(Throwable t) { - logger.warn("Exception while subscribing to the RSocket source", t); - synchronized (LoadBalancedRSocketMono.this) { - pendingSockets -= 1; - if (++errors < 5) { - activeFactories.add(factory); - } else { - logger.warn( - "Exception count greater than 5, not re-adding factory {}", factory.toString()); - } - } - } - - @Override - public void onComplete() {} + @Override + public Mono onClose() { + return onClose; } - private static final FailingRSocket FAILING_REACTIVE_SOCKET = new FailingRSocket(); - /** * (Null Object Pattern) This failing RSocket never succeed, it is useful for simplifying the code * when dealing with edge cases. */ private static class FailingRSocket implements RSocket { - @SuppressWarnings("ThrowableInstanceNeverThrown") - private static final NoAvailableRSocketException NO_AVAILABLE_RS_EXCEPTION = - new NoAvailableRSocketException(); - - private static final Mono errorVoid = Mono.error(NO_AVAILABLE_RS_EXCEPTION); - private static final Mono errorPayload = Mono.error(NO_AVAILABLE_RS_EXCEPTION); + private static final Mono errorVoid = Mono.error(NoAvailableRSocketException.INSTANCE); + private static final Mono errorPayload = + Mono.error(NoAvailableRSocketException.INSTANCE); @Override public Mono fireAndForget(Payload payload) { @@ -714,8 +520,11 @@ public double availability() { } @Override - public Mono close() { - return Mono.empty(); + public void dispose() {} + + @Override + public boolean isDisposed() { + return true; } @Override @@ -728,15 +537,13 @@ public Mono onClose() { * Wrapper of a RSocket, it computes statistics about the req/resp calls and update availability * accordingly. */ - private class WeightedSocket extends RSocketProxy implements LoadBalancerSocketMetrics { + private class WeightedSocket implements LoadBalancerSocketMetrics, RSocket { private static final double STARTUP_PENALTY = Long.MAX_VALUE >> 12; - - private RSocketSupplier factory; private final Quantile lowerQuantile; private final Quantile higherQuantile; private final long inactivityFactor; - + private final MonoProcessor rSocketMono; private volatile int pending; // instantaneous rate private long stamp; // last timestamp we sent a request private long stamp0; // last timestamp we sent a request or receive a response @@ -747,14 +554,15 @@ private class WeightedSocket extends RSocketProxy implements LoadBalancerSocketM private AtomicLong pendingStreams; // number of active streams + private volatile double availability = 0.0; + private final MonoProcessor onClose = MonoProcessor.create(); + WeightedSocket( - RSocket child, RSocketSupplier factory, Quantile lowerQuantile, Quantile higherQuantile, int inactivityFactor) { - super(child); - this.factory = factory; + this.rSocketMono = MonoProcessor.create(); this.lowerQuantile = lowerQuantile; this.higherQuantile = higherQuantile; this.inactivityFactor = inactivityFactor; @@ -766,53 +574,154 @@ private class WeightedSocket extends RSocketProxy implements LoadBalancerSocketM this.median = new Median(); this.interArrivalTime = new Ewma(1, TimeUnit.MINUTES, DEFAULT_INITIAL_INTER_ARRIVAL_TIME); this.pendingStreams = new AtomicLong(); - child.onClose().doFinally(signalType -> removeSocket(this, true)).subscribe(); + + logger.debug("Creating WeightedSocket {} from factory {}", WeightedSocket.this, factory); + + WeightedSocket.this + .onClose() + .doFinally( + s -> { + pool.accept(factory); + activeSockets.remove(WeightedSocket.this); + logger.debug( + "Removed {} from factory {} from activeSockets", WeightedSocket.this, factory); + }) + .subscribe(); + + factory + .get() + .retryWhen( + Retry.backoff(weightedSocketRetries, weightedSocketBackOff) + .maxBackoff(weightedSocketMaxBackOff)) + .doOnError( + throwable -> { + logger.error( + "error while connecting {} from factory {}", + WeightedSocket.this, + factory, + throwable); + WeightedSocket.this.dispose(); + }) + .subscribe( + rSocket -> { + // When RSocket is closed, close the WeightedSocket + rSocket + .onClose() + .doFinally( + signalType -> { + logger.info( + "RSocket {} from factory {} closed", WeightedSocket.this, factory); + WeightedSocket.this.dispose(); + }) + .subscribe(); + + // When the factory is closed, close the RSocket + factory + .onClose() + .doFinally( + signalType -> { + logger.info("Factory {} closed", factory); + rSocket.dispose(); + }) + .subscribe(); + + // When the WeightedSocket is closed, close the RSocket + WeightedSocket.this + .onClose() + .doFinally( + signalType -> { + logger.info( + "WeightedSocket {} from factory {} closed", + WeightedSocket.this, + factory); + rSocket.dispose(); + }) + .subscribe(); + + /*synchronized (LoadBalancedRSocketMono.this) { + if (activeSockets.size() >= targetAperture) { + quickSlowestRS(); + pendingSockets -= 1; + } + }*/ + rSocketMono.onNext(rSocket); + availability = 1.0; + if (!WeightedSocket.this + .isDisposed()) { // May be already disposed because of retryBackoff delay + activeSockets.add(WeightedSocket.this); + logger.debug( + "Added WeightedSocket {} from factory {} to activeSockets", + WeightedSocket.this, + factory); + } + }); } - WeightedSocket( - RSocket child, RSocketSupplier factory, Quantile lowerQuantile, Quantile higherQuantile) { - this(child, factory, lowerQuantile, higherQuantile, DEFAULT_INTER_ARRIVAL_FACTOR); + WeightedSocket(RSocketSupplier factory, Quantile lowerQuantile, Quantile higherQuantile) { + this(factory, lowerQuantile, higherQuantile, DEFAULT_INTER_ARRIVAL_FACTOR); } @Override public Mono requestResponse(Payload payload) { - return MonoSource.wrap( - subscriber -> - source.requestResponse(payload).subscribe(new LatencySubscriber<>(subscriber, this))); + return rSocketMono.flatMap( + source -> { + return Mono.from( + subscriber -> + source + .requestResponse(payload) + .subscribe(new LatencySubscriber<>(subscriber, this))); + }); } @Override public Flux requestStream(Payload payload) { - return FluxSource.wrap( - subscriber -> - source.requestStream(payload).subscribe(new CountingSubscriber<>(subscriber, this))); + + return rSocketMono.flatMapMany( + source -> { + return Flux.from( + subscriber -> + source + .requestStream(payload) + .subscribe(new CountingSubscriber<>(subscriber, this))); + }); } @Override public Mono fireAndForget(Payload payload) { - return MonoSource.wrap( - subscriber -> - source.fireAndForget(payload).subscribe(new CountingSubscriber<>(subscriber, this))); + + return rSocketMono.flatMap( + source -> { + return Mono.from( + subscriber -> + source + .fireAndForget(payload) + .subscribe(new CountingSubscriber<>(subscriber, this))); + }); } @Override public Mono metadataPush(Payload payload) { - return MonoSource.wrap( - subscriber -> - source.metadataPush(payload).subscribe(new CountingSubscriber<>(subscriber, this))); + return rSocketMono.flatMap( + source -> { + return Mono.from( + subscriber -> + source + .metadataPush(payload) + .subscribe(new CountingSubscriber<>(subscriber, this))); + }); } @Override public Flux requestChannel(Publisher payloads) { - return FluxSource.wrap( - subscriber -> - source - .requestChannel(payloads) - .subscribe(new CountingSubscriber<>(subscriber, this))); - } - RSocketSupplier getFactory() { - return factory; + return rSocketMono.flatMapMany( + source -> { + return Flux.from( + subscriber -> + source + .requestChannel(payloads) + .subscribe(new CountingSubscriber<>(subscriber, this))); + }); } synchronized double getPredictedLatency() { @@ -882,8 +791,23 @@ private synchronized void observe(double rtt) { } @Override - public Mono close() { - return source.close(); + public double availability() { + return availability; + } + + @Override + public void dispose() { + onClose.onComplete(); + } + + @Override + public boolean isDisposed() { + return onClose.isDisposed(); + } + + @Override + public Mono onClose() { + return onClose; } @Override @@ -903,8 +827,7 @@ public String toString() { + pending + " availability= " + availability() - + ")->" - + source; + + ")->"; } @Override @@ -984,7 +907,7 @@ public void onError(Throwable t) { child.onError(t); long now = decr(start); if (t instanceof TransportException || t instanceof ClosedChannelException) { - removeSocket(socket, true); + socket.dispose(); } else if (t instanceof TimeoutException) { observe(now - start); } @@ -1030,7 +953,8 @@ public void onError(Throwable t) { socket.pendingStreams.decrementAndGet(); child.onError(t); if (t instanceof TransportException || t instanceof ClosedChannelException) { - removeSocket(socket, true); + logger.debug("Disposing {} from activeSockets because of error {}", socket, t); + socket.dispose(); } } diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancerSocketMetrics.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancerSocketMetrics.java index a4ec8571e..00d1861af 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancerSocketMetrics.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancerSocketMetrics.java @@ -1,14 +1,17 @@ /* - * Copyright 2017 Netflix, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket.client; diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/NoAvailableRSocketException.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/NoAvailableRSocketException.java new file mode 100644 index 000000000..08fb98d2d --- /dev/null +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/NoAvailableRSocketException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.client; + +/** An exception that indicates that no RSocket was available. */ +public final class NoAvailableRSocketException extends Exception { + + /** + * The single instance of this type. Note that it is initialized without any stack trace. + */ + public static final NoAvailableRSocketException INSTANCE; + + private static final long serialVersionUID = -2785312562743351184L; + + static { + NoAvailableRSocketException exception = new NoAvailableRSocketException(); + exception.setStackTrace( + new StackTraceElement[] { + new StackTraceElement(exception.getClass().getName(), "", null, -1) + }); + + INSTANCE = exception; + } + + private NoAvailableRSocketException() {}; +} diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/RSocketSupplierPool.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/RSocketSupplierPool.java new file mode 100644 index 000000000..1683ee125 --- /dev/null +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/RSocketSupplierPool.java @@ -0,0 +1,196 @@ +package io.rsocket.client; + +import io.rsocket.Closeable; +import io.rsocket.client.filter.RSocketSupplier; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Consumer; +import java.util.function.Supplier; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; + +public class RSocketSupplierPool + implements Supplier>, Consumer, Closeable { + private static final Logger logger = LoggerFactory.getLogger(RSocketSupplierPool.class); + private static final int EFFORT = 5; + + private final ArrayList factoryPool; + private final ArrayList leasedSuppliers; + + private final MonoProcessor onClose; + + public RSocketSupplierPool(Publisher> publisher) { + this.onClose = MonoProcessor.create(); + this.factoryPool = new ArrayList<>(); + this.leasedSuppliers = new ArrayList<>(); + + Disposable disposable = + Flux.from(publisher) + .doOnNext(this::handleNewFactories) + .onErrorResume( + t -> { + logger.error("error streaming RSocketSuppliers", t); + return Mono.delay(Duration.ofSeconds(10)).then(Mono.error(t)); + }) + .subscribe(); + + onClose.doFinally(s -> disposable.dispose()).subscribe(); + } + + private synchronized void handleNewFactories(Collection newFactories) { + Set current = new HashSet<>(factoryPool.size() + leasedSuppliers.size()); + current.addAll(factoryPool); + current.addAll(leasedSuppliers); + + Set removed = new HashSet<>(current); + removed.removeAll(newFactories); + + Set added = new HashSet<>(newFactories); + added.removeAll(current); + + boolean changed = false; + Iterator it0 = leasedSuppliers.iterator(); + while (it0.hasNext()) { + RSocketSupplier supplier = it0.next(); + if (removed.contains(supplier)) { + it0.remove(); + try { + changed = true; + supplier.dispose(); + } catch (Exception e) { + logger.warn("Exception while closing a RSocket", e); + } + } + } + + Iterator it1 = factoryPool.iterator(); + while (it1.hasNext()) { + RSocketSupplier supplier = it1.next(); + if (removed.contains(supplier)) { + it1.remove(); + try { + changed = true; + supplier.dispose(); + } catch (Exception e) { + logger.warn("Exception while closing a RSocket", e); + } + } + } + + factoryPool.addAll(added); + if (!added.isEmpty()) { + changed = true; + } + + if (changed && logger.isDebugEnabled()) { + StringBuilder msgBuilder = new StringBuilder(); + msgBuilder + .append("\nUpdated active factories (size: ") + .append(factoryPool.size()) + .append(")\n"); + for (RSocketSupplier f : factoryPool) { + msgBuilder.append(" + ").append(f).append('\n'); + } + msgBuilder.append("Active sockets:\n"); + for (RSocketSupplier socket : leasedSuppliers) { + msgBuilder.append(" + ").append(socket).append('\n'); + } + logger.debug(msgBuilder.toString()); + } + } + + @Override + public synchronized void accept(RSocketSupplier rSocketSupplier) { + boolean contained = leasedSuppliers.remove(rSocketSupplier); + if (contained + && !rSocketSupplier + .isDisposed()) { // only added leasedSupplier back to factoryPool if it's still there + factoryPool.add(rSocketSupplier); + } + } + + @Override + public synchronized Optional get() { + Optional optional = Optional.empty(); + int poolSize = factoryPool.size(); + if (poolSize == 1) { + RSocketSupplier rSocketSupplier = factoryPool.get(0); + if (rSocketSupplier.availability() > 0.0) { + factoryPool.remove(0); + leasedSuppliers.add(rSocketSupplier); + logger.debug("Added {} to leasedSuppliers", rSocketSupplier); + optional = Optional.of(rSocketSupplier); + } + } else if (poolSize > 1) { + Random rng = ThreadLocalRandom.current(); + int size = factoryPool.size(); + RSocketSupplier factory0 = null; + RSocketSupplier factory1 = null; + int i0 = 0; + int i1 = 0; + for (int i = 0; i < EFFORT; i++) { + i0 = rng.nextInt(size); + i1 = rng.nextInt(size - 1); + if (i1 >= i0) { + i1++; + } + factory0 = factoryPool.get(i0); + factory1 = factoryPool.get(i1); + if (factory0.availability() > 0.0 && factory1.availability() > 0.0) { + break; + } + } + if (factory0.availability() > factory1.availability()) { + factoryPool.remove(i0); + leasedSuppliers.add(factory0); + logger.debug("Added {} to leasedSuppliers", factory0); + optional = Optional.of(factory0); + } else { + factoryPool.remove(i1); + leasedSuppliers.add(factory1); + logger.debug("Added {} to leasedSuppliers", factory1); + optional = Optional.of(factory1); + } + } + + return optional; + } + + @Override + public Mono onClose() { + return onClose; + } + + @Override + public void dispose() { + if (!onClose.isDisposed()) { + onClose.onComplete(); + + close(factoryPool); + close(leasedSuppliers); + } + } + + private void close(Collection suppliers) { + for (RSocketSupplier supplier : suppliers) { + try { + supplier.dispose(); + } catch (Throwable t) { + } + } + } + + public synchronized int poolSize() { + return factoryPool.size(); + } + + public synchronized boolean isPoolEmpty() { + return factoryPool.isEmpty(); + } +} diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientServerTest.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/TimeoutException.java similarity index 58% rename from rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientServerTest.java rename to rsocket-load-balancer/src/main/java/io/rsocket/client/TimeoutException.java index 9ddfccf1f..7bd6c7135 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientServerTest.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/TimeoutException.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.transport.local; -import io.rsocket.test.BaseClientServerTest; +package io.rsocket.client; -public class LocalClientServerTest extends BaseClientServerTest { +public final class TimeoutException extends Exception { - @Override - protected LocalClientSetupRule createClientServer() { - return new LocalClientSetupRule(); - } + public static final TimeoutException INSTANCE = new TimeoutException(); + + private static final long serialVersionUID = -3094321310317812063L; + + private TimeoutException() {} } diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/TransportException.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/TransportException.java new file mode 100644 index 000000000..6823e3db8 --- /dev/null +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/TransportException.java @@ -0,0 +1,26 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.client; + +public final class TransportException extends Throwable { + + private static final long serialVersionUID = -3339846338318701123L; + + public TransportException(Throwable t) { + super(t); + } +} diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/BackupRequestSocket.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/BackupRequestSocket.java index 478ec21c0..db7e18df4 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/BackupRequestSocket.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/BackupRequestSocket.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.client.filter; import io.rsocket.Payload; @@ -31,7 +32,6 @@ import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoSource; public class BackupRequestSocket implements RSocket { private final ScheduledExecutorService executor; @@ -59,7 +59,7 @@ public Mono fireAndForget(Payload payload) { @Override public Mono requestResponse(Payload payload) { - return MonoSource.wrap( + return Mono.from( subscriber -> { Subscriber oneSubscriber = new OneSubscriber<>(subscriber); Subscriber backupRequest = @@ -89,8 +89,13 @@ public double availability() { } @Override - public Mono close() { - return child.close(); + public void dispose() { + child.dispose(); + } + + @Override + public boolean isDisposed() { + return child.isDisposed(); } @Override @@ -159,9 +164,7 @@ public void onSubscribe(Subscription s) { if (q.estimation() > 0) { future = executor.schedule( - () -> { - action.get().subscribe(new BackupRequestSubscriber<>(oneSubscriber, s)); - }, + () -> action.get().subscribe(new BackupRequestSubscriber<>(oneSubscriber, s)), (long) q.estimation(), TimeUnit.MICROSECONDS); } diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/RSocketSupplier.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/RSocketSupplier.java index 29617188b..7a94bdf07 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/RSocketSupplier.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/RSocketSupplier.java @@ -1,17 +1,17 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package io.rsocket.client.filter; @@ -87,8 +87,13 @@ public Mono get() { } @Override - public Mono close() { - return Mono.empty().doFinally(s -> onClose.onComplete()).then(); + public void dispose() { + onClose.onComplete(); + } + + @Override + public boolean isDisposed() { + return onClose.isDisposed(); } @Override @@ -100,7 +105,7 @@ private class AvailabilityAwareRSocketProxy extends RSocketProxy { public AvailabilityAwareRSocketProxy(RSocket source) { super(source); - onClose.then(close()).subscribe(); + onClose.doFinally(signalType -> source.dispose()).subscribe(); } @Override diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/RSockets.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/RSockets.java index 76041bca1..d52cf87f1 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/RSockets.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/RSockets.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -72,7 +72,7 @@ public Mono metadataPush(Payload payload) { /** * Provides a mapping function to wrap a {@code RSocket} such that a call to {@link - * RSocket#close()} does not cancel all pending requests. Instead, it will wait for all pending + * RSocket#dispose()} does not cancel all pending requests. Instead, it will wait for all pending * requests to finish and then close the socket. * * @return Function to transform any socket into a safe closing socket. @@ -91,7 +91,7 @@ public Mono fireAndForget(Payload payload) { .doFinally( signalType -> { if (count.decrementAndGet() == 0 && closed.get()) { - source.close().subscribe(); + source.dispose(); } }); } @@ -104,7 +104,7 @@ public Mono requestResponse(Payload payload) { .doFinally( signalType -> { if (count.decrementAndGet() == 0 && closed.get()) { - source.close().subscribe(); + source.dispose(); } }); } @@ -117,7 +117,7 @@ public Flux requestStream(Payload payload) { .doFinally( signalType -> { if (count.decrementAndGet() == 0 && closed.get()) { - source.close().subscribe(); + source.dispose(); } }); } @@ -130,7 +130,7 @@ public Flux requestChannel(Publisher payloads) { .doFinally( signalType -> { if (count.decrementAndGet() == 0 && closed.get()) { - source.close().subscribe(); + source.dispose(); } }); } @@ -143,24 +143,18 @@ public Mono metadataPush(Payload payload) { .doFinally( signalType -> { if (count.decrementAndGet() == 0 && closed.get()) { - source.close().subscribe(); + source.dispose(); } }); } @Override - public Mono close() { - return Mono.defer( - () -> { - if (closed.compareAndSet(false, true)) { - if (count.get() == 0) { - return source.close(); - } else { - return source.onClose(); - } - } - return source.onClose(); - }); + public void dispose() { + if (closed.compareAndSet(false, true)) { + if (count.get() == 0) { + source.dispose(); + } + } } }; } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/TransportException.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/package-info.java similarity index 69% rename from rsocket-core/src/main/java/io/rsocket/exceptions/TransportException.java rename to rsocket-load-balancer/src/main/java/io/rsocket/client/filter/package-info.java index 2a828975e..55ce5646c 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/TransportException.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/package-info.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,10 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.exceptions; -public class TransportException extends Throwable { - public TransportException(Throwable t) { - super(t); - } -} +@NonNullApi +package io.rsocket.client.filter; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/TimedOutException.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/package-info.java similarity index 73% rename from rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/TimedOutException.java rename to rsocket-load-balancer/src/main/java/io/rsocket/client/package-info.java index a3a83d38d..ec21dee96 100644 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/TimedOutException.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/package-info.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.aeron.internal; -public class TimedOutException extends RuntimeException {} +@NonNullApi +package io.rsocket.client; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/stat/Ewma.java b/rsocket-load-balancer/src/main/java/io/rsocket/stat/Ewma.java index 06ab60276..4641f5411 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/stat/Ewma.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/stat/Ewma.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.stat; import io.rsocket.util.Clock; diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/stat/FrugalQuantile.java b/rsocket-load-balancer/src/main/java/io/rsocket/stat/FrugalQuantile.java index 3c83f422f..c152c4a80 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/stat/FrugalQuantile.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/stat/FrugalQuantile.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.stat; import java.util.Random; diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/stat/Median.java b/rsocket-load-balancer/src/main/java/io/rsocket/stat/Median.java index 8256af70f..d0a982a26 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/stat/Median.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/stat/Median.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.stat; /** This implementation gives better results because it considers more data-point. */ diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/stat/Quantile.java b/rsocket-load-balancer/src/main/java/io/rsocket/stat/Quantile.java index d3d0539a3..fa62aedc9 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/stat/Quantile.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/stat/Quantile.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -17,12 +17,12 @@ public interface Quantile { /** @return the estimation of the current value of the quantile */ - public double estimation(); + double estimation(); /** * Insert a data point `x` in the quantile estimator. * - * @param x + * @param x the data point to add. */ - public void insert(double x); + void insert(double x); } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/NoAvailableRSocketException.java b/rsocket-load-balancer/src/main/java/io/rsocket/stat/package-info.java similarity index 73% rename from rsocket-core/src/main/java/io/rsocket/exceptions/NoAvailableRSocketException.java rename to rsocket-load-balancer/src/main/java/io/rsocket/stat/package-info.java index 5d074004d..cfb071175 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/NoAvailableRSocketException.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/stat/package-info.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.exceptions; -public class NoAvailableRSocketException extends Exception {} +@NonNullApi +package io.rsocket.stat; + +import reactor.util.annotation.NonNullApi; 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 d1a98775d..4baa106c5 100644 --- a/rsocket-load-balancer/src/test/java/io/rsocket/client/LoadBalancedRSocketMonoTest.java +++ b/rsocket-load-balancer/src/test/java/io/rsocket/client/LoadBalancedRSocketMonoTest.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -19,19 +19,15 @@ import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.client.filter.RSocketSupplier; -import io.rsocket.util.PayloadImpl; -import java.net.InetSocketAddress; -import java.net.SocketAddress; import java.util.Arrays; +import java.util.Collections; import java.util.List; -import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -39,11 +35,8 @@ public class LoadBalancedRSocketMonoTest { @Test(timeout = 10_000L) public void testNeverSelectFailingFactories() throws InterruptedException { - InetSocketAddress local0 = InetSocketAddress.createUnresolved("localhost", 7000); - InetSocketAddress local1 = InetSocketAddress.createUnresolved("localhost", 7001); - TestingRSocket socket = new TestingRSocket(Function.identity()); - RSocketSupplier failing = failingClient(local0); + RSocketSupplier failing = failingClient(); RSocketSupplier succeeding = succeedingFactory(socket); List factories = Arrays.asList(failing, succeeding); @@ -52,9 +45,6 @@ public void testNeverSelectFailingFactories() throws InterruptedException { @Test(timeout = 10_000L) public void testNeverSelectFailingSocket() throws InterruptedException { - InetSocketAddress local0 = InetSocketAddress.createUnresolved("localhost", 7000); - InetSocketAddress local1 = InetSocketAddress.createUnresolved("localhost", 7001); - TestingRSocket socket = new TestingRSocket(Function.identity()); TestingRSocket failingSocket = new TestingRSocket(Function.identity()) { @@ -76,6 +66,33 @@ public double availability() { testBalancer(clients); } + @Test(timeout = 10_000L) + public void testRefreshesSocketsOnSelectBeforeReturningFailedAfterNewFactoriesDelivered() { + TestingRSocket socket = new TestingRSocket(Function.identity()); + + CompletableFuture laterSupplier = new CompletableFuture<>(); + Flux> factories = + Flux.create( + s -> { + s.next(Collections.emptyList()); + + laterSupplier.handle( + (RSocketSupplier result, Throwable t) -> { + s.next(Collections.singletonList(result)); + return null; + }); + }); + + LoadBalancedRSocketMono balancer = LoadBalancedRSocketMono.create(factories); + + Assert.assertEquals(0.0, balancer.availability(), 0); + + laterSupplier.complete(succeedingFactory(socket)); + balancer.rSocketMono.block(); + + Assert.assertEquals(1.0, balancer.availability(), 0); + } + private void testBalancer(List factories) throws InterruptedException { Publisher> src = s -> { @@ -92,49 +109,17 @@ private void testBalancer(List factories) throws InterruptedExc Flux.range(0, 100).flatMap(i -> balancer).blockLast(); } - private void makeAcall(RSocket balancer) throws InterruptedException { - CountDownLatch latch = new CountDownLatch(1); - - balancer - .requestResponse(PayloadImpl.EMPTY) - .subscribe( - new Subscriber() { - @Override - public void onSubscribe(Subscription s) { - s.request(1L); - } - - @Override - public void onNext(Payload payload) { - System.out.println("Successfully receiving a response"); - } - - @Override - public void onError(Throwable t) { - t.printStackTrace(); - Assert.assertTrue(false); - latch.countDown(); - } - - @Override - public void onComplete() { - latch.countDown(); - } - }); - - latch.await(); - } - private static RSocketSupplier succeedingFactory(RSocket socket) { RSocketSupplier mock = Mockito.mock(RSocketSupplier.class); Mockito.when(mock.availability()).thenReturn(1.0); Mockito.when(mock.get()).thenReturn(Mono.just(socket)); + Mockito.when(mock.onClose()).thenReturn(Mono.never()); return mock; } - private static RSocketSupplier failingClient(SocketAddress sa) { + private static RSocketSupplier failingClient() { RSocketSupplier mock = Mockito.mock(RSocketSupplier.class); Mockito.when(mock.availability()).thenReturn(0.0); 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 f61237613..887132f99 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 @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,19 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + 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.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.client.filter.RSocketSupplier; import io.rsocket.test.TestSubscriber; -import io.rsocket.util.PayloadImpl; +import io.rsocket.util.EmptyPayload; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -44,7 +45,7 @@ public void testError() throws InterruptedException { testRSocket( (latch, socket) -> { assertEquals(1.0, socket.availability(), 0.0); - Publisher payloadPublisher = socket.requestResponse(PayloadImpl.EMPTY); + Publisher payloadPublisher = socket.requestResponse(EmptyPayload.INSTANCE); Subscriber subscriber = TestSubscriber.create(); payloadPublisher.subscribe(subscriber); @@ -73,7 +74,7 @@ public void testWidowReset() throws InterruptedException { testRSocket( (latch, socket) -> { assertEquals(1.0, socket.availability(), 0.0); - Publisher payloadPublisher = socket.requestResponse(PayloadImpl.EMPTY); + Publisher payloadPublisher = socket.requestResponse(EmptyPayload.INSTANCE); Subscriber subscriber = TestSubscriber.create(); payloadPublisher.subscribe(subscriber); @@ -106,7 +107,7 @@ private void testRSocket(BiConsumer f) throws Interrupt new TestingRSocket( input -> { if (count.getAndIncrement() < 1) { - return PayloadImpl.EMPTY; + return EmptyPayload.INSTANCE; } else { throw new RuntimeException(); } 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 83b8c14b0..96982121b 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,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -24,15 +24,12 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; -import reactor.core.publisher.MonoSource; +import reactor.core.publisher.*; public class TestingRSocket implements RSocket { private final AtomicInteger count; - private final MonoProcessor closeSubject = MonoProcessor.create(); + private final MonoProcessor onClose = MonoProcessor.create(); private final BiFunction, Payload, Boolean> eachPayloadHandler; public TestingRSocket(Function responder) { @@ -60,7 +57,7 @@ public Mono fireAndForget(Payload payload) { @Override public Mono requestResponse(Payload payload) { - return MonoSource.wrap( + return Mono.from( subscriber -> subscriber.onSubscribe( new Subscription() { @@ -130,16 +127,17 @@ public double availability() { } @Override - public Mono close() { - return Mono.defer( - () -> { - closeSubject.onComplete(); - return closeSubject; - }); + public void dispose() { + onClose.onComplete(); + } + + @Override + public boolean isDisposed() { + return onClose.isDisposed(); } @Override public Mono onClose() { - return closeSubject; + return onClose; } } 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 fdfd8015a..9a5ac644b 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 @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -21,8 +21,7 @@ import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.client.filter.RSockets; -import io.rsocket.exceptions.TimeoutException; -import io.rsocket.util.PayloadImpl; +import io.rsocket.util.EmptyPayload; import java.time.Duration; import org.hamcrest.MatcherAssert; import org.junit.Test; @@ -32,15 +31,11 @@ public class TimeoutClientTest { @Test public void testTimeoutSocket() { - TestingRSocket socket = - new TestingRSocket( - (subscriber, payload) -> { - return false; - }); + TestingRSocket socket = new TestingRSocket((subscriber, payload) -> false); RSocket timeout = RSockets.timeout(Duration.ofMillis(50)).apply(socket); timeout - .requestResponse(PayloadImpl.EMPTY) + .requestResponse(EmptyPayload.INSTANCE) .subscribe( new Subscriber() { @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 1dec437e0..0aab4afd7 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 @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/rsocket-load-balancer/src/test/resources/log4j.properties b/rsocket-load-balancer/src/test/resources/log4j.properties deleted file mode 100644 index 6477d125f..000000000 --- a/rsocket-load-balancer/src/test/resources/log4j.properties +++ /dev/null @@ -1,33 +0,0 @@ -# -# Copyright 2016 Netflix, Inc. -#

-# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -#

-# http://www.apache.org/licenses/LICENSE-2.0 -#

-# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on -# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -# - - -# -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -log4j.rootLogger=INFO, stdout - -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] (%F:%L) - %m%n \ No newline at end of file diff --git a/rsocket-load-balancer/src/test/resources/logback-test.xml b/rsocket-load-balancer/src/test/resources/logback-test.xml new file mode 100644 index 000000000..13e65b37d --- /dev/null +++ b/rsocket-load-balancer/src/test/resources/logback-test.xml @@ -0,0 +1,33 @@ + + + + + + + + %d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] %c{1} - %m%n + + + + + + + + + + + diff --git a/rsocket-micrometer/build.gradle b/rsocket-micrometer/build.gradle new file mode 100644 index 000000000..fd89aeae0 --- /dev/null +++ b/rsocket-micrometer/build.gradle @@ -0,0 +1,39 @@ +/* + * 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. + */ + +plugins { + id 'java-library' + id 'maven-publish' + id 'signing' +} + +dependencies { + api project(':rsocket-core') + api 'io.micrometer:micrometer-core' + + implementation 'org.slf4j:slf4j-api' + + testImplementation project(':rsocket-test') + testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'org.mockito:mockito-core' + + testRuntimeOnly 'ch.qos.logback:logback-classic' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' +} + +description = 'Transparent Metrics exposure to Micrometer' diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java new file mode 100644 index 000000000..c8b22382a --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java @@ -0,0 +1,257 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer; + +import static io.rsocket.frame.FrameType.*; + +import io.micrometer.core.instrument.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.plugins.DuplexConnectionInterceptor.Type; +import java.util.Objects; +import java.util.function.Consumer; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * An implementation of {@link DuplexConnection} that intercepts frames and gathers Micrometer + * metrics about them. + * + *

The metric is called {@code rsocket.frame} and is tagged with {@code connection.type} ({@link + * Type}), {@code frame.type} ({@link FrameType}), and any additional configured tags. {@code + * rsocket.duplex.connection.close} and {@code rsocket.duplex.connection.dispose} metrics, tagged + * with {@code connection.type} ({@link Type}) and any additional configured tags are also + * collected. + * + * @see Micrometer + */ +final class MicrometerDuplexConnection implements DuplexConnection { + + private final Counter close; + + private final DuplexConnection delegate; + + private final Counter dispose; + + private final FrameCounters frameCounters; + + /** + * Creates a new {@link DuplexConnection}. + * + * @param connectionType the type of connection being monitored + * @param delegate the {@link DuplexConnection} to delegate to + * @param meterRegistry the {@link MeterRegistry} to use + * @param tags additional tags to attach to {@link Meter}s + * @throws NullPointerException if {@code connectionType}, {@code delegate}, or {@code + * meterRegistry} is {@code null} + */ + MicrometerDuplexConnection( + Type connectionType, DuplexConnection delegate, MeterRegistry meterRegistry, Tag... tags) { + + Objects.requireNonNull(connectionType, "connectionType must not be null"); + this.delegate = Objects.requireNonNull(delegate, "delegate must not be null"); + Objects.requireNonNull(meterRegistry, "meterRegistry must not be null"); + + this.close = + meterRegistry.counter( + "rsocket.duplex.connection.close", + Tags.of(tags).and("connection.type", connectionType.name())); + this.dispose = + meterRegistry.counter( + "rsocket.duplex.connection.dispose", + Tags.of(tags).and("connection.type", connectionType.name())); + this.frameCounters = new FrameCounters(connectionType, meterRegistry, tags); + } + + @Override + public ByteBufAllocator alloc() { + return delegate.alloc(); + } + + @Override + public void dispose() { + delegate.dispose(); + dispose.increment(); + } + + @Override + public Mono onClose() { + return delegate.onClose().doAfterTerminate(close::increment); + } + + @Override + public Flux receive() { + return delegate.receive().doOnNext(frameCounters); + } + + @Override + public Mono send(Publisher frames) { + Objects.requireNonNull(frames, "frames must not be null"); + + return delegate.send(Flux.from(frames).doOnNext(frameCounters)); + } + + private static final class FrameCounters implements Consumer { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private final Counter cancel; + + private final Counter complete; + + private final Counter error; + + private final Counter extension; + + private final Counter keepalive; + + private final Counter lease; + + private final Counter metadataPush; + + private final Counter next; + + private final Counter nextComplete; + + private final Counter payload; + + private final Counter requestChannel; + + private final Counter requestFireAndForget; + + private final Counter requestN; + + private final Counter requestResponse; + + private final Counter requestStream; + + private final Counter resume; + + private final Counter resumeOk; + + private final Counter setup; + + private final Counter unknown; + + private FrameCounters(Type connectionType, MeterRegistry meterRegistry, Tag... tags) { + this.cancel = counter(connectionType, meterRegistry, CANCEL, tags); + this.complete = counter(connectionType, meterRegistry, COMPLETE, tags); + this.error = counter(connectionType, meterRegistry, ERROR, tags); + this.extension = counter(connectionType, meterRegistry, EXT, tags); + this.keepalive = counter(connectionType, meterRegistry, KEEPALIVE, tags); + this.lease = counter(connectionType, meterRegistry, LEASE, tags); + this.metadataPush = counter(connectionType, meterRegistry, METADATA_PUSH, tags); + this.next = counter(connectionType, meterRegistry, NEXT, tags); + this.nextComplete = counter(connectionType, meterRegistry, NEXT_COMPLETE, tags); + this.payload = counter(connectionType, meterRegistry, PAYLOAD, tags); + this.requestChannel = counter(connectionType, meterRegistry, REQUEST_CHANNEL, tags); + this.requestFireAndForget = counter(connectionType, meterRegistry, REQUEST_FNF, tags); + this.requestN = counter(connectionType, meterRegistry, REQUEST_N, tags); + this.requestResponse = counter(connectionType, meterRegistry, REQUEST_RESPONSE, tags); + this.requestStream = counter(connectionType, meterRegistry, REQUEST_STREAM, tags); + this.resume = counter(connectionType, meterRegistry, RESUME, tags); + this.resumeOk = counter(connectionType, meterRegistry, RESUME_OK, tags); + this.setup = counter(connectionType, meterRegistry, SETUP, tags); + this.unknown = counter(connectionType, meterRegistry, "UNKNOWN", tags); + } + + private static Counter counter( + Type connectionType, MeterRegistry meterRegistry, FrameType frameType, Tag... tags) { + + return counter(connectionType, meterRegistry, frameType.name(), tags); + } + + private static Counter counter( + Type connectionType, MeterRegistry meterRegistry, String frameType, Tag... tags) { + + return meterRegistry.counter( + "rsocket.frame", + Tags.of(tags).and("connection.type", connectionType.name()).and("frame.type", frameType)); + } + + @Override + public void accept(ByteBuf frame) { + FrameType frameType = FrameHeaderCodec.frameType(frame); + + switch (frameType) { + case SETUP: + this.setup.increment(); + break; + case LEASE: + this.lease.increment(); + break; + case KEEPALIVE: + this.keepalive.increment(); + break; + case REQUEST_RESPONSE: + this.requestResponse.increment(); + break; + case REQUEST_FNF: + this.requestFireAndForget.increment(); + break; + case REQUEST_STREAM: + this.requestStream.increment(); + break; + case REQUEST_CHANNEL: + this.requestChannel.increment(); + break; + case REQUEST_N: + this.requestN.increment(); + break; + case CANCEL: + this.cancel.increment(); + break; + case PAYLOAD: + this.payload.increment(); + break; + case ERROR: + this.error.increment(); + break; + case METADATA_PUSH: + this.metadataPush.increment(); + break; + case RESUME: + this.resume.increment(); + break; + case RESUME_OK: + this.resumeOk.increment(); + break; + case NEXT: + this.next.increment(); + break; + case COMPLETE: + this.complete.increment(); + break; + case NEXT_COMPLETE: + this.nextComplete.increment(); + break; + case EXT: + this.extension.increment(); + break; + default: + this.logger.debug("Skipping count of unknown frame type: {}", frameType); + this.unknown.increment(); + } + } + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnectionInterceptor.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnectionInterceptor.java new file mode 100644 index 000000000..b94e969ec --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnectionInterceptor.java @@ -0,0 +1,64 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.rsocket.DuplexConnection; +import io.rsocket.frame.FrameType; +import io.rsocket.plugins.DuplexConnectionInterceptor; +import java.util.Objects; + +/** + * An implementation of {@link DuplexConnectionInterceptor} that intercepts frames and gathers + * Micrometer metrics about them. + * + *

The metric is called {@code rsocket.frame} and is tagged with {@code connection.type} ({@link + * Type}), {@code frame.type} ({@link FrameType}), and any additional configured tags. {@code + * rsocket.duplex.connection.close} and {@code rsocket.duplex.connection.dispose} metrics, tagged + * with {@code connection.type} ({@link Type}) and any additional configured tags are also + * collected. + * + * @see Micrometer + */ +public final class MicrometerDuplexConnectionInterceptor implements DuplexConnectionInterceptor { + + private final MeterRegistry meterRegistry; + + private final Tag[] tags; + + /** + * Creates a new {@link DuplexConnectionInterceptor}. + * + * @param meterRegistry the {@link MeterRegistry} to use to create {@link Meter}s. + * @param tags the additional tags to attach to each {@link Meter} + * @throws NullPointerException if {@code meterRegistry} is {@code null} + */ + public MicrometerDuplexConnectionInterceptor(MeterRegistry meterRegistry, Tag... tags) { + this.meterRegistry = Objects.requireNonNull(meterRegistry, "meterRegistry must not be null"); + this.tags = tags; + } + + @Override + public MicrometerDuplexConnection apply(Type connectionType, DuplexConnection delegate) { + Objects.requireNonNull(connectionType, "connectionType must not be null"); + Objects.requireNonNull(delegate, "delegate must not be null"); + + return new MicrometerDuplexConnection(connectionType, delegate, meterRegistry, tags); + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerRSocket.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerRSocket.java new file mode 100644 index 000000000..9e1abbc03 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerRSocket.java @@ -0,0 +1,206 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer; + +import static reactor.core.publisher.SignalType.CANCEL; +import static reactor.core.publisher.SignalType.ON_COMPLETE; +import static reactor.core.publisher.SignalType.ON_ERROR; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.Timer.Sample; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; + +/** + * An implementation of {@link RSocket} that intercepts interactions and gathers Micrometer metrics + * about them. + * + *

The metrics are called {@code rsocket.[ metadata.push | request.channel | request.fnf | + * request.response | request.stream ]} and is tagged with {@code signal.type} ({@link SignalType}) + * and any additional configured tags. + * + * @see Micrometer + */ +final class MicrometerRSocket implements RSocket { + + private final RSocket delegate; + + private final InteractionCounters metadataPush; + + private final InteractionCounters requestChannel; + + private final InteractionCounters requestFireAndForget; + + private final InteractionTimers requestResponse; + + private final InteractionCounters requestStream; + + /** + * Creates a new {@link RSocket}. + * + * @param delegate the {@link RSocket} to delegate to + * @param meterRegistry the {@link MeterRegistry} to use + * @param tags additional tags to attach to {@link Meter}s + * @throws NullPointerException if {@code delegate} or {@code meterRegistry} is {@code null} + */ + MicrometerRSocket(RSocket delegate, MeterRegistry meterRegistry, Tag... tags) { + this.delegate = Objects.requireNonNull(delegate, "delegate must not be null"); + Objects.requireNonNull(meterRegistry, "meterRegistry must not be null"); + + this.metadataPush = new InteractionCounters(meterRegistry, "metadata.push", tags); + this.requestChannel = new InteractionCounters(meterRegistry, "request.channel", tags); + this.requestFireAndForget = new InteractionCounters(meterRegistry, "request.fnf", tags); + this.requestResponse = new InteractionTimers(meterRegistry, "request.response", tags); + this.requestStream = new InteractionCounters(meterRegistry, "request.stream", tags); + } + + @Override + public void dispose() { + delegate.dispose(); + } + + @Override + public Mono fireAndForget(Payload payload) { + return delegate.fireAndForget(payload).doFinally(requestFireAndForget); + } + + @Override + public Mono metadataPush(Payload payload) { + return delegate.metadataPush(payload).doFinally(metadataPush); + } + + @Override + public Mono onClose() { + return delegate.onClose(); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return delegate.requestChannel(payloads).doFinally(requestChannel); + } + + @Override + public Mono requestResponse(Payload payload) { + return Mono.defer( + () -> { + Sample sample = requestResponse.start(); + + return delegate + .requestResponse(payload) + .doFinally(signalType -> requestResponse.accept(sample, signalType)); + }); + } + + @Override + public Flux requestStream(Payload payload) { + return delegate.requestStream(payload).doFinally(requestStream); + } + + private static final class InteractionCounters implements Consumer { + + private final Counter cancel; + + private final Counter onComplete; + + private final Counter onError; + + private InteractionCounters(MeterRegistry meterRegistry, String interactionModel, Tag... tags) { + this.cancel = counter(meterRegistry, interactionModel, CANCEL, tags); + this.onComplete = counter(meterRegistry, interactionModel, ON_COMPLETE, tags); + this.onError = counter(meterRegistry, interactionModel, ON_ERROR, tags); + } + + @Override + public void accept(SignalType signalType) { + switch (signalType) { + case CANCEL: + cancel.increment(); + break; + case ON_COMPLETE: + onComplete.increment(); + break; + case ON_ERROR: + onError.increment(); + break; + } + } + + private static Counter counter( + MeterRegistry meterRegistry, String interactionModel, SignalType signalType, Tag... tags) { + + return meterRegistry.counter( + "rsocket." + interactionModel, Tags.of(tags).and("signal.type", signalType.name())); + } + } + + private static final class InteractionTimers implements BiConsumer { + + private final Timer cancel; + + private final MeterRegistry meterRegistry; + + private final Timer onComplete; + + private final Timer onError; + + private InteractionTimers(MeterRegistry meterRegistry, String interactionModel, Tag... tags) { + this.meterRegistry = meterRegistry; + + this.cancel = timer(meterRegistry, interactionModel, CANCEL, tags); + this.onComplete = timer(meterRegistry, interactionModel, ON_COMPLETE, tags); + this.onError = timer(meterRegistry, interactionModel, ON_ERROR, tags); + } + + @Override + public void accept(Sample sample, SignalType signalType) { + switch (signalType) { + case CANCEL: + sample.stop(cancel); + break; + case ON_COMPLETE: + sample.stop(onComplete); + break; + case ON_ERROR: + sample.stop(onError); + break; + } + } + + Sample start() { + return Timer.start(meterRegistry); + } + + private static Timer timer( + MeterRegistry meterRegistry, String interactionModel, SignalType signalType, Tag... tags) { + + return meterRegistry.timer( + "rsocket." + interactionModel, Tags.of(tags).and("signal.type", signalType.name())); + } + } +} diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerRSocketInterceptor.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerRSocketInterceptor.java new file mode 100644 index 000000000..c405c8601 --- /dev/null +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerRSocketInterceptor.java @@ -0,0 +1,61 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.rsocket.RSocket; +import io.rsocket.plugins.RSocketInterceptor; +import java.util.Objects; +import reactor.core.publisher.SignalType; + +/** + * An implementation of {@link RSocketInterceptor} that intercepts interactions and gathers + * Micrometer metrics about them. + * + *

The metrics are called {@code rsocket.[ metadata.push | request.channel | request.fnf | + * request.response | request.stream ]} and is tagged with {@code signal.type} ({@link SignalType}) + * and any additional configured tags. + * + * @see Micrometer + */ +public final class MicrometerRSocketInterceptor implements RSocketInterceptor { + + private final MeterRegistry meterRegistry; + + private final Tag[] tags; + + /** + * Creates a new {@link RSocketInterceptor}. + * + * @param meterRegistry the {@link MeterRegistry} to use to create {@link Meter}s. + * @param tags the additional tags to attach to each {@link Meter} + * @throws NullPointerException if {@code meterRegistry} is {@code null} + */ + public MicrometerRSocketInterceptor(MeterRegistry meterRegistry, Tag... tags) { + this.meterRegistry = Objects.requireNonNull(meterRegistry, "meterRegistry must not be null"); + this.tags = tags; + } + + @Override + public MicrometerRSocket apply(RSocket delegate) { + Objects.requireNonNull(delegate, "delegate must not be null"); + + return new MicrometerRSocket(delegate, meterRegistry, tags); + } +} diff --git a/rsocket-spectator/build.gradle b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/package-info.java similarity index 62% rename from rsocket-spectator/build.gradle rename to rsocket-micrometer/src/main/java/io/rsocket/micrometer/package-info.java index 2f7d001ad..c95f2ce02 100644 --- a/rsocket-spectator/build.gradle +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/package-info.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,10 +14,12 @@ * limitations under the License. */ -dependencies { - compile project(':rsocket-core') - compile 'com.netflix.spectator:spectator-api:0.45.0' - compile 'org.hdrhistogram:HdrHistogram:latest.release' +/** + * Transparent metrics exposure for Micrometer. + * + * @see Micrometer + */ +@NonNullApi +package io.rsocket.micrometer; - testCompile project(':rsocket-test') -} +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerDuplexConnectionInterceptorTest.java b/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerDuplexConnectionInterceptorTest.java new file mode 100644 index 000000000..4ff072252 --- /dev/null +++ b/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerDuplexConnectionInterceptorTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer; + +import static io.rsocket.plugins.DuplexConnectionInterceptor.Type.CLIENT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.mockito.Mockito.RETURNS_SMART_NULLS; +import static org.mockito.Mockito.mock; + +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.rsocket.DuplexConnection; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +final class MicrometerDuplexConnectionInterceptorTest { + + private final DuplexConnection delegate = mock(DuplexConnection.class, RETURNS_SMART_NULLS); + + private final SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry(); + + @DisplayName("creates MicrometerDuplexConnection") + @Test + void apply() { + assertThat(new MicrometerDuplexConnectionInterceptor(meterRegistry).apply(CLIENT, delegate)) + .isInstanceOf(MicrometerDuplexConnection.class); + } + + @DisplayName("apply throws NullPointerException with null connectionType") + @Test + void applyNullConnectionType() { + assertThatNullPointerException() + .isThrownBy( + () -> new MicrometerDuplexConnectionInterceptor(meterRegistry).apply(null, delegate)) + .withMessage("connectionType must not be null"); + } + + @DisplayName("apply throws NullPointerException with null delegate") + @Test + void applyNullDelegate() { + assertThatNullPointerException() + .isThrownBy( + () -> new MicrometerDuplexConnectionInterceptor(meterRegistry).apply(CLIENT, null)) + .withMessage("delegate must not be null"); + } + + @DisplayName("constructor throws NullPointer exception with null meterRegistry") + @Test + void constructorNullMeterRegistry() { + assertThatNullPointerException() + .isThrownBy(() -> new MicrometerDuplexConnectionInterceptor(null)) + .withMessage("meterRegistry must not be null"); + } +} diff --git a/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerDuplexConnectionTest.java b/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerDuplexConnectionTest.java new file mode 100644 index 000000000..03abd2084 --- /dev/null +++ b/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerDuplexConnectionTest.java @@ -0,0 +1,213 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer; + +import static io.rsocket.frame.FrameType.*; +import static io.rsocket.plugins.DuplexConnectionInterceptor.Type.CLIENT; +import static io.rsocket.plugins.DuplexConnectionInterceptor.Type.SERVER; +import static io.rsocket.test.TestFrames.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.mockito.Mockito.*; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.netty.buffer.ByteBuf; +import io.rsocket.DuplexConnection; +import io.rsocket.frame.FrameType; +import io.rsocket.plugins.DuplexConnectionInterceptor.Type; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.test.StepVerifier; + +final class MicrometerDuplexConnectionTest { + + private final DuplexConnection delegate = mock(DuplexConnection.class, RETURNS_SMART_NULLS); + + private final SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry(); + + @DisplayName("constructor throws NullPointerException with null connectionType") + @Test + void constructorNullConnectionType() { + assertThatNullPointerException() + .isThrownBy(() -> new MicrometerDuplexConnection(null, delegate, meterRegistry)) + .withMessage("connectionType must not be null"); + } + + @DisplayName("constructor throws NullPointerException with null delegate") + @Test + void constructorNullDelegate() { + assertThatNullPointerException() + .isThrownBy(() -> new MicrometerDuplexConnection(CLIENT, null, meterRegistry)) + .withMessage("delegate must not be null"); + } + + @DisplayName("constructor throws NullPointerException with null meterRegistry") + @Test + void constructorNullMeterRegistry() { + + assertThatNullPointerException() + .isThrownBy(() -> new MicrometerDuplexConnection(CLIENT, delegate, null)) + .withMessage("meterRegistry must not be null"); + } + + @DisplayName("dispose gathers metrics") + @Test + void dispose() { + new MicrometerDuplexConnection( + CLIENT, delegate, meterRegistry, Tag.of("test-key", "test-value")) + .dispose(); + + assertThat( + meterRegistry + .get("rsocket.duplex.connection.dispose") + .tag("connection.type", CLIENT.name()) + .tag("test-key", "test-value") + .counter() + .count()) + .isEqualTo(1); + } + + @DisplayName("onClose gathers metrics") + @Test + void onClose() { + when(delegate.onClose()).thenReturn(Mono.empty()); + + new MicrometerDuplexConnection( + CLIENT, delegate, meterRegistry, Tag.of("test-key", "test-value")) + .onClose() + .subscribe(Operators.drainSubscriber()); + + assertThat( + meterRegistry + .get("rsocket.duplex.connection.close") + .tag("connection.type", CLIENT.name()) + .tag("test-key", "test-value") + .counter() + .count()) + .isEqualTo(1); + } + + @DisplayName("receive gathers metrics") + @Test + void receive() { + Flux frames = + Flux.just( + createTestCancelFrame(), + createTestErrorFrame(), + createTestKeepaliveFrame(), + createTestLeaseFrame(), + createTestMetadataPushFrame(), + createTestPayloadFrame(), + createTestRequestChannelFrame(), + createTestRequestFireAndForgetFrame(), + createTestRequestNFrame(), + createTestRequestResponseFrame(), + createTestRequestStreamFrame(), + createTestSetupFrame()); + + when(delegate.receive()).thenReturn(frames); + + new MicrometerDuplexConnection( + CLIENT, delegate, meterRegistry, Tag.of("test-key", "test-value")) + .receive() + .as(StepVerifier::create) + .expectNextCount(12) + .verifyComplete(); + + assertThat(findCounter(CLIENT, CANCEL).count()).isEqualTo(1); + assertThat(findCounter(CLIENT, COMPLETE).count()).isEqualTo(1); + assertThat(findCounter(CLIENT, ERROR).count()).isEqualTo(1); + assertThat(findCounter(CLIENT, KEEPALIVE).count()).isEqualTo(1); + assertThat(findCounter(CLIENT, LEASE).count()).isEqualTo(1); + assertThat(findCounter(CLIENT, METADATA_PUSH).count()).isEqualTo(1); + assertThat(findCounter(CLIENT, REQUEST_CHANNEL).count()).isEqualTo(1); + assertThat(findCounter(CLIENT, REQUEST_FNF).count()).isEqualTo(1); + assertThat(findCounter(CLIENT, REQUEST_N).count()).isEqualTo(1); + assertThat(findCounter(CLIENT, REQUEST_RESPONSE).count()).isEqualTo(1); + assertThat(findCounter(CLIENT, REQUEST_STREAM).count()).isEqualTo(1); + assertThat(findCounter(CLIENT, SETUP).count()).isEqualTo(1); + } + + @DisplayName("send gathers metrics") + @SuppressWarnings("unchecked") + @Test + void send() { + ArgumentCaptor> captor = ArgumentCaptor.forClass(Publisher.class); + when(delegate.send(captor.capture())).thenReturn(Mono.empty()); + + Flux frames = + Flux.just( + createTestCancelFrame(), + createTestErrorFrame(), + createTestKeepaliveFrame(), + createTestLeaseFrame(), + createTestMetadataPushFrame(), + createTestPayloadFrame(), + createTestRequestChannelFrame(), + createTestRequestFireAndForgetFrame(), + createTestRequestNFrame(), + createTestRequestResponseFrame(), + createTestRequestStreamFrame(), + createTestSetupFrame()); + + new MicrometerDuplexConnection( + SERVER, delegate, meterRegistry, Tag.of("test-key", "test-value")) + .send(frames) + .as(StepVerifier::create) + .verifyComplete(); + + StepVerifier.create(captor.getValue()).expectNextCount(12).verifyComplete(); + + assertThat(findCounter(SERVER, CANCEL).count()).isEqualTo(1); + assertThat(findCounter(SERVER, COMPLETE).count()).isEqualTo(1); + assertThat(findCounter(SERVER, ERROR).count()).isEqualTo(1); + assertThat(findCounter(SERVER, KEEPALIVE).count()).isEqualTo(1); + assertThat(findCounter(SERVER, LEASE).count()).isEqualTo(1); + assertThat(findCounter(SERVER, METADATA_PUSH).count()).isEqualTo(1); + assertThat(findCounter(SERVER, REQUEST_CHANNEL).count()).isEqualTo(1); + assertThat(findCounter(SERVER, REQUEST_FNF).count()).isEqualTo(1); + assertThat(findCounter(SERVER, REQUEST_N).count()).isEqualTo(1); + assertThat(findCounter(SERVER, REQUEST_RESPONSE).count()).isEqualTo(1); + assertThat(findCounter(SERVER, REQUEST_STREAM).count()).isEqualTo(1); + assertThat(findCounter(SERVER, SETUP).count()).isEqualTo(1); + } + + @DisplayName("send throws NullPointerException with null frames") + @Test + void sendNullFrames() { + assertThatNullPointerException() + .isThrownBy( + () -> new MicrometerDuplexConnection(CLIENT, delegate, meterRegistry).send(null)) + .withMessage("frames must not be null"); + } + + private Counter findCounter(Type connectionType, FrameType frameType) { + return meterRegistry + .get("rsocket.frame") + .tag("connection.type", connectionType.name()) + .tag("frame.type", frameType.name()) + .tag("test-key", "test-value") + .counter(); + } +} diff --git a/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerRSocketInterceptorTest.java b/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerRSocketInterceptorTest.java new file mode 100644 index 000000000..196ee1aa6 --- /dev/null +++ b/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerRSocketInterceptorTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.mockito.Mockito.RETURNS_SMART_NULLS; +import static org.mockito.Mockito.mock; + +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.rsocket.RSocket; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +final class MicrometerRSocketInterceptorTest { + + private final RSocket delegate = mock(RSocket.class, RETURNS_SMART_NULLS); + + private final SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry(); + + @DisplayName("creates MicrometerRSocket") + @Test + void apply() { + assertThat(new MicrometerRSocketInterceptor(meterRegistry).apply(delegate)) + .isInstanceOf(MicrometerRSocket.class); + } + + @DisplayName("apply throws NullPointerException with null delegate") + @Test + void applyNullDelegate() { + assertThatNullPointerException() + .isThrownBy(() -> new MicrometerRSocketInterceptor(meterRegistry).apply(null)) + .withMessage("delegate must not be null"); + } + + @DisplayName("constructor throws NullPointerException with null meterRegistry") + @Test + void constructorNullMeterRegistry() { + assertThatNullPointerException() + .isThrownBy(() -> new MicrometerRSocketInterceptor(null)) + .withMessage("meterRegistry must not be null"); + } +} diff --git a/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerRSocketTest.java b/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerRSocketTest.java new file mode 100644 index 000000000..7317c5c59 --- /dev/null +++ b/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerRSocketTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.micrometer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.mockito.Mockito.RETURNS_SMART_NULLS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.util.DefaultPayload; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; +import reactor.test.StepVerifier; + +final class MicrometerRSocketTest { + + private final RSocket delegate = mock(RSocket.class, RETURNS_SMART_NULLS); + + private final SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry(); + + @DisplayName("constructor throws NullPointerException with null delegate") + @Test + void constructorNullDelegate() { + assertThatNullPointerException() + .isThrownBy(() -> new MicrometerRSocket(null, meterRegistry)) + .withMessage("delegate must not be null"); + } + + @DisplayName("constructor throws NullPointerException with null meterRegistry") + @Test + void constructorNullMeterRegistry() { + assertThatNullPointerException() + .isThrownBy(() -> new MicrometerRSocket(delegate, null)) + .withMessage("meterRegistry must not be null"); + } + + @DisplayName("fireAndForget gathers metrics") + @Test + void fireAndForget() { + Payload payload = DefaultPayload.create("test-metadata", "test-data"); + when(delegate.fireAndForget(payload)).thenReturn(Mono.empty()); + + new MicrometerRSocket(delegate, meterRegistry, Tag.of("test-key", "test-value")) + .fireAndForget(payload) + .as(StepVerifier::create) + .verifyComplete(); + + assertThat(findCounter("request.fnf", SignalType.ON_COMPLETE).count()).isEqualTo(1); + } + + @DisplayName("metadataPush gathers metrics") + @Test + void metadataPush() { + Payload payload = DefaultPayload.create("test-metadata", "test-data"); + when(delegate.metadataPush(payload)).thenReturn(Mono.empty()); + + new MicrometerRSocket(delegate, meterRegistry, Tag.of("test-key", "test-value")) + .metadataPush(payload) + .as(StepVerifier::create) + .verifyComplete(); + + assertThat(findCounter("metadata.push", SignalType.ON_COMPLETE).count()).isEqualTo(1); + } + + @DisplayName("requestChannel gathers metrics") + @Test + void requestChannel() { + Mono payload = Mono.just(DefaultPayload.create("test-metadata", "test-data")); + when(delegate.requestChannel(payload)).thenReturn(Flux.empty()); + + new MicrometerRSocket(delegate, meterRegistry, Tag.of("test-key", "test-value")) + .requestChannel(payload) + .as(StepVerifier::create) + .verifyComplete(); + + assertThat(findCounter("request.channel", SignalType.ON_COMPLETE).count()).isEqualTo(1); + } + + @DisplayName("requestResponse gathers metrics") + @Test + void requestResponse() { + Payload payload = DefaultPayload.create("test-metadata", "test-data"); + when(delegate.requestResponse(payload)).thenReturn(Mono.empty()); + + new MicrometerRSocket(delegate, meterRegistry, Tag.of("test-key", "test-value")) + .requestResponse(payload) + .as(StepVerifier::create) + .verifyComplete(); + + assertThat(findTimer("request.response", SignalType.ON_COMPLETE).count()).isEqualTo(1); + } + + @DisplayName("requestStream gathers metrics") + @Test + void requestStream() { + Payload payload = DefaultPayload.create("test-metadata", "test-data"); + when(delegate.requestStream(payload)).thenReturn(Flux.empty()); + + new MicrometerRSocket(delegate, meterRegistry, Tag.of("test-key", "test-value")) + .requestStream(payload) + .as(StepVerifier::create) + .verifyComplete(); + + assertThat(findCounter("request.stream", SignalType.ON_COMPLETE).count()).isEqualTo(1); + } + + private Counter findCounter(String interactionModel, SignalType signalType) { + return meterRegistry + .get(String.format("rsocket.%s", interactionModel)) + .tag("signal.type", signalType.name()) + .tag("test-key", "test-value") + .counter(); + } + + private Timer findTimer(String interactionModel, SignalType signalType) { + return meterRegistry + .get(String.format("rsocket.%s", interactionModel)) + .tag("signal.type", signalType.name()) + .tag("test-key", "test-value") + .timer(); + } +} diff --git a/rsocket-micrometer/src/test/resources/logback-test.xml b/rsocket-micrometer/src/test/resources/logback-test.xml new file mode 100644 index 000000000..56e2f9c9b --- /dev/null +++ b/rsocket-micrometer/src/test/resources/logback-test.xml @@ -0,0 +1,32 @@ + + + + + + + + %date{HH:mm:ss.SSS} %-10thread %-42logger %msg%n + + + + + + + + + + diff --git a/rsocket-spectator/src/main/java/io/rsocket/spectator/SpectatorFrameInterceptor.java b/rsocket-spectator/src/main/java/io/rsocket/spectator/SpectatorFrameInterceptor.java deleted file mode 100644 index 3d93f4607..000000000 --- a/rsocket-spectator/src/main/java/io/rsocket/spectator/SpectatorFrameInterceptor.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.spectator; - -import com.netflix.spectator.api.Counter; -import com.netflix.spectator.api.Registry; -import io.rsocket.DuplexConnection; -import io.rsocket.Frame; -import io.rsocket.FrameType; -import io.rsocket.plugins.DuplexConnectionInterceptor; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** An implementation of {@link DuplexConnectionInterceptor} that uses Spectator */ -public class SpectatorFrameInterceptor implements DuplexConnectionInterceptor { - private final Registry registry; - - public SpectatorFrameInterceptor(Registry registry) { - this.registry = registry; - } - - @Override - public DuplexConnection apply(Type type, DuplexConnection connection) { - return new DuplexConnection() { - Counter cancelCounter = registry.counter(FrameType.CANCEL.name(), type.name()); - Counter requestChannelCounter = - registry.counter(FrameType.REQUEST_CHANNEL.name(), type.name()); - Counter completeCounter = registry.counter(FrameType.COMPLETE.name(), type.name()); - Counter errorCounter = registry.counter(FrameType.ERROR.name(), type.name()); - Counter extCounter = registry.counter(FrameType.EXT.name(), type.name()); - Counter fireAndForgetCounter = - registry.counter(FrameType.FIRE_AND_FORGET.name(), type.name()); - Counter keepAliveCounter = registry.counter(FrameType.KEEPALIVE.name(), type.name()); - Counter leaseCounter = registry.counter(FrameType.LEASE.name(), type.name()); - Counter metadataPushCounter = registry.counter(FrameType.METADATA_PUSH.name(), type.name()); - Counter nextCounter = registry.counter(FrameType.NEXT.name(), type.name()); - Counter nextCompleteCounter = registry.counter(FrameType.NEXT_COMPLETE.name(), type.name()); - Counter payloadCounter = registry.counter(FrameType.PAYLOAD.name(), type.name()); - Counter requestNCounter = registry.counter(FrameType.REQUEST_N.name(), type.name()); - Counter requestResponseCounter = - registry.counter(FrameType.REQUEST_RESPONSE.name(), type.name()); - Counter requestStreamCounter = registry.counter(FrameType.REQUEST_STREAM.name(), type.name()); - Counter resumeCounter = registry.counter(FrameType.RESUME.name(), type.name()); - Counter resumeOkCounter = registry.counter(FrameType.RESUME_OK.name(), type.name()); - Counter setupCounter = registry.counter(FrameType.SETUP.name(), type.name()); - Counter undefinedCounter = registry.counter(FrameType.UNDEFINED.name(), type.name()); - - @Override - public Mono send(Publisher frame) { - return connection.send(Flux.from(frame).doOnNext(this::count)); - } - - @Override - public Mono sendOne(Frame frame) { - return Mono.defer( - () -> { - count(frame); - return connection.sendOne(frame); - }); - } - - @Override - public Flux receive() { - return connection.receive().doOnNext(this::count); - } - - @Override - public Mono close() { - return connection.close(); - } - - @Override - public Mono onClose() { - return connection.onClose(); - } - - @Override - public double availability() { - return connection.availability(); - } - - private void count(Frame frame) { - switch (frame.getType()) { - case CANCEL: - cancelCounter.increment(); - break; - case REQUEST_CHANNEL: - requestChannelCounter.increment(); - break; - case COMPLETE: - completeCounter.increment(); - break; - case ERROR: - errorCounter.increment(); - break; - case EXT: - extCounter.increment(); - break; - case FIRE_AND_FORGET: - fireAndForgetCounter.increment(); - break; - case KEEPALIVE: - keepAliveCounter.increment(); - break; - case LEASE: - leaseCounter.increment(); - break; - case METADATA_PUSH: - metadataPushCounter.increment(); - break; - case NEXT: - nextCounter.increment(); - break; - case NEXT_COMPLETE: - nextCompleteCounter.increment(); - break; - case PAYLOAD: - payloadCounter.increment(); - break; - case REQUEST_N: - requestNCounter.increment(); - break; - case REQUEST_RESPONSE: - requestResponseCounter.increment(); - break; - case REQUEST_STREAM: - requestStreamCounter.increment(); - break; - case RESUME: - resumeCounter.increment(); - break; - case RESUME_OK: - resumeOkCounter.increment(); - break; - case SETUP: - setupCounter.increment(); - break; - case UNDEFINED: - default: - undefinedCounter.increment(); - break; - } - } - }; - } -} diff --git a/rsocket-spectator/src/main/java/io/rsocket/spectator/SpectatorRSocket.java b/rsocket-spectator/src/main/java/io/rsocket/spectator/SpectatorRSocket.java deleted file mode 100644 index 9cd16a7b3..000000000 --- a/rsocket-spectator/src/main/java/io/rsocket/spectator/SpectatorRSocket.java +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.spectator; - -import com.netflix.spectator.api.Counter; -import com.netflix.spectator.api.Registry; -import com.netflix.spectator.api.Timer; -import io.rsocket.Payload; -import io.rsocket.RSocket; -import java.lang.reflect.Array; -import java.util.concurrent.TimeUnit; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** Wraps a {@link RSocket} with counters */ -public class SpectatorRSocket implements RSocket { - private final RSocket delegate; - - private Counter fireAndForgetErrors; - private Counter fireAndForgetCanceled; - private Counter fireAndForgetTotal; - private Timer fireAndForgetTimer; - - private Counter requestResponseErrors; - private Counter requestResponseCanceled; - private Counter requestResponseTotal; - private Timer requestResponseTimer; - - private Counter requestStreamErrors; - private Counter requestStreamCanceled; - private Counter requestStreamTotal; - - private Counter requestChannelErrors; - private Counter requestChannelCanceled; - private Counter requestChannelTotal; - - private Counter metadataPushErrors; - private Counter metadataPushCanceled; - private Counter metadataPushTotal; - private Timer metadataPushTimer; - - public SpectatorRSocket(Registry registry, RSocket delegate, String... tags) { - this.delegate = delegate; - - this.fireAndForgetErrors = - registry.counter("reactiveSocketStats", concatenate(tags, "fireAndForget", "errors")); - this.fireAndForgetCanceled = - registry.counter("reactiveSocketStats", concatenate(tags, "fireAndForget", "canceled")); - this.fireAndForgetTotal = - registry.counter("reactiveSocketStats", concatenate(tags, "fireAndForget", "total")); - this.fireAndForgetTimer = - registry.timer("reactiveSocketStats", concatenate(tags, "fireAndForget", "timer")); - - this.requestResponseErrors = - registry.counter("reactiveSocketStats", concatenate(tags, "requestResponse", "errors")); - this.requestResponseCanceled = - registry.counter("reactiveSocketStats", concatenate(tags, "requestResponse", "canceled")); - this.requestResponseTotal = - registry.counter("reactiveSocketStats", concatenate(tags, "requestResponse", "total")); - this.requestResponseTimer = - registry.timer("reactiveSocketStats", concatenate(tags, "requestResponse", "timer")); - - this.requestStreamErrors = - registry.counter("reactiveSocketStats", concatenate(tags, "requestStream", "errors")); - this.requestStreamCanceled = - registry.counter("reactiveSocketStats", concatenate(tags, "requestStream", "canceled")); - this.requestStreamTotal = - registry.counter("reactiveSocketStats", concatenate(tags, "requestStream", "total")); - - this.requestChannelErrors = - registry.counter("reactiveSocketStats", concatenate(tags, "requestChannel", "errors")); - this.requestChannelCanceled = - registry.counter("reactiveSocketStats", concatenate(tags, "requestChannel", "canceled")); - this.requestChannelTotal = - registry.counter("reactiveSocketStats", concatenate(tags, "requestChannel", "total")); - - this.metadataPushErrors = - registry.counter("reactiveSocketStats", concatenate(tags, "metadataPush", "errors")); - this.metadataPushCanceled = - registry.counter("reactiveSocketStats", concatenate(tags, "metadataPush", "canceled")); - this.metadataPushTotal = - registry.counter("reactiveSocketStats", concatenate(tags, "metadataPush", "total")); - this.metadataPushTimer = - registry.timer("reactiveSocketStats", concatenate(tags, "metadataPush", "timer")); - } - - static T[] concatenate(T[] a, T... b) { - if (a == null || a.length == 0) { - return b; - } - - int aLen = a.length; - int bLen = b.length; - - @SuppressWarnings("unchecked") - T[] c = (T[]) Array.newInstance(a.getClass().getComponentType(), aLen + bLen); - System.arraycopy(a, 0, c, 0, aLen); - System.arraycopy(b, 0, c, aLen, bLen); - - return c; - } - - @Override - public Mono fireAndForget(Payload payload) { - return Mono.defer( - () -> { - long start = System.nanoTime(); - return delegate - .fireAndForget(payload) - .doFinally( - signalType -> { - fireAndForgetTimer.record(System.nanoTime() - start, TimeUnit.NANOSECONDS); - fireAndForgetTotal.increment(); - - switch (signalType) { - case CANCEL: - fireAndForgetCanceled.increment(); - break; - case ON_ERROR: - fireAndForgetErrors.increment(); - } - }); - }); - } - - @Override - public Mono requestResponse(Payload payload) { - return Mono.defer( - () -> { - long start = System.nanoTime(); - return delegate - .requestResponse(payload) - .doFinally( - signalType -> { - requestResponseTimer.record(System.nanoTime() - start, TimeUnit.NANOSECONDS); - requestResponseTotal.increment(); - - switch (signalType) { - case CANCEL: - requestResponseCanceled.increment(); - break; - case ON_ERROR: - requestResponseErrors.increment(); - } - }); - }); - } - - @Override - public Flux requestStream(Payload payload) { - return Flux.defer( - () -> - delegate - .requestStream(payload) - .doFinally( - signalType -> { - requestStreamTotal.increment(); - - switch (signalType) { - case CANCEL: - requestStreamCanceled.increment(); - break; - case ON_ERROR: - requestStreamErrors.increment(); - } - })); - } - - @Override - public Flux requestChannel(Publisher payloads) { - return Flux.defer( - () -> - delegate - .requestChannel(payloads) - .doFinally( - signalType -> { - requestChannelTotal.increment(); - - switch (signalType) { - case CANCEL: - requestChannelCanceled.increment(); - break; - case ON_ERROR: - requestChannelErrors.increment(); - } - })); - } - - @Override - public Mono metadataPush(Payload payload) { - return Mono.defer( - () -> { - long start = System.nanoTime(); - return delegate - .metadataPush(payload) - .doFinally( - signalType -> { - metadataPushTimer.record(System.nanoTime() - start, TimeUnit.NANOSECONDS); - metadataPushTotal.increment(); - - switch (signalType) { - case CANCEL: - metadataPushCanceled.increment(); - break; - case ON_ERROR: - metadataPushErrors.increment(); - } - }); - }); - } - - @Override - public Mono close() { - return delegate.close(); - } - - @Override - public Mono onClose() { - return delegate.onClose(); - } - - @Override - public double availability() { - return delegate.availability(); - } -} diff --git a/rsocket-spectator/src/main/java/io/rsocket/spectator/SpectatorRSocketInterceptor.java b/rsocket-spectator/src/main/java/io/rsocket/spectator/SpectatorRSocketInterceptor.java deleted file mode 100644 index ceede8338..000000000 --- a/rsocket-spectator/src/main/java/io/rsocket/spectator/SpectatorRSocketInterceptor.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.spectator; - -import com.netflix.spectator.api.Registry; -import io.rsocket.RSocket; -import io.rsocket.plugins.RSocketInterceptor; - -/** Interceptor that wraps a {@link RSocket} with a {@link SpectatorRSocket} */ -public class SpectatorRSocketInterceptor implements RSocketInterceptor { - private static final String[] EMPTY = new String[0]; - private final Registry registry; - private final String[] tags; - - public SpectatorRSocketInterceptor(Registry registry, String... tags) { - this.registry = registry; - this.tags = tags; - } - - public SpectatorRSocketInterceptor(Registry registry) { - this(registry, EMPTY); - } - - @Override - public RSocket apply(RSocket reactiveSocket) { - return new SpectatorRSocket(registry, reactiveSocket, tags); - } -} diff --git a/rsocket-tck-drivers/README.md b/rsocket-tck-drivers/README.md deleted file mode 100644 index 0eec47ce1..000000000 --- a/rsocket-tck-drivers/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# RSocket TCK Drivers - -This is meant to be used in conjunction with the TCK at [reactivesocket-tck](https://github.com/RSocket/reactivesocket-tck) - -## Basic Idea and Organization - -The philosophy behind the TCK is that it should allow any implementation of RSocket to verify itself against any other -implementation. In order to provide a truly polyglot solution, we determined that the best way to do so was to provide a central -TCK repository with only the code that generates the intermediate script, and then leave it up to implementers to create the -drivers for their own implementation. The script was created specifically to be easy to parse and implement drivers for, -and this Java driver is the first driver to be created as the Java implementation of RSockets is the most mature at -the time. - -The driver is organized with a simple structure. On both the client and server drivers, we have the main driver class that -do an intial parse of the script files. On the server side, this process basically constructs dynamic request handlers where -every time a request is received, the appropriate behavior is searched up and is passed to a ParseMarble object, which is run -on its own thread and is used to parse through a marble diagram and enact it's behavior. On the client side, the main driver -class splits up each test into it's own individual lines, and then runs each test synchronously in its own thread. Support -for concurrent behavior can easily be added later. - -On the client side, for the most part, each test thread just parses through each line of the test in order, synchronously and enacts its -behavior on our TestSubscriber, a special subscriber that we can use to verify that certain things have happened. `await` calls -should block the main test thread, and the test should fail if a single assert fails. - -Things get trickier with channel tests, because the client and server need to have the same behavior. In channel tests on both -sides, the driver creates a ParseChannel object, which parses through the contents of a channel tests and handles receiving -and sending data. We use the ParseMarble object to handle sending data. Here, we have one thread that continuously runs `parse()`, -and other threads that run `add()` and `request()`, which stages data to be added and requested. - - - -## Run Instructions - -You can build the project with `gradle build`. -You can run the client and server using the `run` script with `./run [options]`. The options are: - -`--server` : This is if you want to launch the server - -`--client` : This is if you want to launch the client - -`--host ` : This is for the client only, determines what host to connect to - -`--port

` : If launching as client, tells it to connect to port `p`, and if launching as server, tells what port to server on - -`--file ` : The path to the script file. Make sure to give the server and client the correct file formats - -`--debug` : This is if you want to look at the individual frames being sent and received by the client - -`--tests` : This allows you, when you're running the client, to specify the tests you want to run by name. Each test -should be comma separated. - -Examples: -`./run.sh --server --port 4567 --file src/test/resources/servertest.txt` should start up a server on port `4567` that -has its behavior determined by the file `servertest.txt`. - -`./run.sh --client --host localhost --port 4567 --file src/test/resources/clienttest.txt --debug --tests genericTest,badTest` should -start the client and have it connect to localhost on port `4567` and load the tests in `clienttest.txt` in debug mode, -and only run the tests named `genericTest` and `badTest`. - diff --git a/rsocket-tck-drivers/build.gradle b/rsocket-tck-drivers/build.gradle deleted file mode 100644 index 037faf2b5..000000000 --- a/rsocket-tck-drivers/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -apply plugin: 'application' -apply plugin: 'java' - -mainClassName = "io.rsocket.tckdrivers.main.Main" - -jar { - manifest { - attributes "Main-Class": "$mainClassName" - } - - from { - configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } - } -} - -dependencies { - compile project(':rsocket-core') - compile project(':rsocket-transport-netty') - compile project(':rsocket-test') - compile 'com.fasterxml.jackson.core:jackson-core:2.8.8' - compile 'com.fasterxml.jackson.core:jackson-databind:2.8.8' - compile 'org.apache.commons:commons-lang3:3.4' - compile 'io.airlift:airline:0.7' - compile 'io.reactivex.rxjava2:rxjava:2.1.0' -} diff --git a/rsocket-tck-drivers/run.sh b/rsocket-tck-drivers/run.sh deleted file mode 100755 index 54c55cadd..000000000 --- a/rsocket-tck-drivers/run.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -LATEST_VERSION=$(ls build/libs/reactivesocket-tck-drivers-*-SNAPSHOT.jar | sort -r | head -1) - -echo "running latest version $LATEST_VERSION" - -java -cp "$LATEST_VERSION" io.rsocket.tckdrivers.main.Main "$@" diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/client/JavaClientDriver.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/client/JavaClientDriver.java deleted file mode 100644 index 372dc969f..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/client/JavaClientDriver.java +++ /dev/null @@ -1,616 +0,0 @@ -/* - * Copyright 2016 Facebook, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package io.rsocket.tckdrivers.client; - -import io.rsocket.Payload; -import io.rsocket.RSocket; -import io.rsocket.tckdrivers.common.ConsoleUtils; -import io.rsocket.tckdrivers.common.EchoSubscription; -import io.rsocket.tckdrivers.common.MySubscriber; -import io.rsocket.tckdrivers.common.ParseChannel; -import io.rsocket.tckdrivers.common.ParseChannelThread; -import io.rsocket.tckdrivers.common.ParseMarble; -import io.rsocket.tckdrivers.common.Tuple; -import io.rsocket.util.PayloadImpl; -import java.io.BufferedReader; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -/** - * This class is the driver for the Java RSocket client. To use with class with the current Java - * impl of RSocket, one should supply both a test file as well as a function that can generate - * RSockets on demand. This driver will then parse through the test file, and for each test, it will - * run them on their own thread and print out the results. - */ -public class JavaClientDriver { - - private final BufferedReader reader; - private final Map> payloadSubscribers; - private final Map> fnfSubscribers; - private final Map idToType; - private final Supplier createClient; - private final List testList; - private final String AGENT = "[CLIENT]"; - private ConsoleUtils consoleUtils = new ConsoleUtils(AGENT); - - public JavaClientDriver(String path, Supplier createClient, List tests) - throws FileNotFoundException { - this.reader = new BufferedReader(new FileReader(path)); - this.payloadSubscribers = new HashMap<>(); - this.fnfSubscribers = new HashMap<>(); - this.idToType = new HashMap<>(); - this.createClient = createClient; - this.testList = tests; - } - - private enum TestResult { - PASS, - FAIL, - CHANNEL - } - - /** - * Splits the test file into individual tests, and then run each of them on their own thread. - * - * @throws IOException - */ - public boolean runTests() throws IOException { - List> tests = new ArrayList<>(); - List test = new ArrayList<>(); - String line = reader.readLine(); - while (line != null) { - switch (line) { - case "!": - tests.add(test); - test = new ArrayList<>(); - break; - default: - test.add(line); - break; - } - line = reader.readLine(); - } - tests.add(test); - tests = tests.subList(1, tests.size()); // remove the first list, which is empty - for (List t : tests) { - TestThread thread = new TestThread(t); - thread.start(); - thread.join(); - } - return ConsoleUtils.allPassed(); - } - - /** - * Parses through the commands for each test, and calls handlers that execute the commands. - * - * @param test the list of strings which makes up each test case - * @param name the name of the test - * @return an option with either true if the test passed, false if it failed, or empty if no - * subscribers were found - */ - private TestResult parse(List test, String name) throws Exception { - List id = new ArrayList<>(); - Iterator iter = test.iterator(); - boolean channelTest = false; // tells whether this is a test for channel or not - boolean hasPassed = true; - while (iter.hasNext()) { - String line = iter.next(); - String[] args = line.split("%%"); - switch (args[0]) { - case "subscribe": - handleSubscribe(args); - id.add(args[2]); - break; - case "channel": - channelTest = true; - handleChannel(args, iter, name, true); - break; - case "echochannel": - handleEchoChannel(args); - break; - case "await": - switch (args[1]) { - case "terminal": - hasPassed &= handleAwaitTerminal(args); - break; - case "atLeast": - hasPassed &= handleAwaitAtLeast(args); - break; - case "no_events": - hasPassed &= handleAwaitNoEvents(args); - break; - default: - break; - } - break; - - case "assert": - switch (args[1]) { - case "no_error": - hasPassed &= assertNoError(args); - break; - case "error": - hasPassed &= assertError(args); - break; - case "received": - hasPassed &= assertReceived(args); - break; - case "received_n": - hasPassed &= assertReceivedN(args); - break; - case "received_at_least": - hasPassed &= assertReceivedAtLeast(args); - break; - case "completed": - hasPassed &= assertCompleted(args); - break; - case "no_completed": - hasPassed &= assertNoCompleted(args); - break; - case "canceled": - hasPassed &= assertCancelled(args); - break; - } - break; - case "take": - handleTake(args); - break; - case "request": - handleRequest(args); - break; - case "cancel": - handleCancel(args); - break; - case "EOF": - handleEOF(); - break; - default: - // the default behavior is to just skip the line, so we can acommodate slight changes to the TCK - break; - } - } - // this check each of the subscribers to see that they all passed their assertions - if (id.size() > 0) { - for (String str : id) { - if (payloadSubscribers.get(str) != null) - hasPassed = hasPassed && payloadSubscribers.get(str).hasPassed(); - else hasPassed = hasPassed && fnfSubscribers.get(str).hasPassed(); - } - if (hasPassed) return TestResult.PASS; - else return TestResult.FAIL; - } else if (channelTest) return TestResult.CHANNEL; - else throw new Exception("There is no subscriber in this test"); - } - - /** - * This function takes in the arguments for the subscribe command, and subscribes an instance of - * MySubscriber with an initial request of 0 (which means don't immediately make a request) to an - * instance of the corresponding publisher - * - * @param args - */ - private void handleSubscribe(String[] args) { - switch (args[1]) { - case "rr": - MySubscriber rrsub = new MySubscriber<>(0L, AGENT); - payloadSubscribers.put(args[2], rrsub); - idToType.put(args[2], args[1]); - RSocket rrclient = createClient.get(); - consoleUtils.info("Sending RR with " + args[3] + " " + args[4]); - Publisher rrpub = rrclient.requestResponse(new PayloadImpl(args[3], args[4])); - rrpub.subscribe(rrsub); - break; - case "rs": - MySubscriber rssub = new MySubscriber<>(0L, AGENT); - payloadSubscribers.put(args[2], rssub); - idToType.put(args[2], args[1]); - RSocket rsclient = createClient.get(); - consoleUtils.info("Sending RS with " + args[3] + " " + args[4]); - Publisher rspub = rsclient.requestStream(new PayloadImpl(args[3], args[4])); - rspub.subscribe(rssub); - break; - case "fnf": - MySubscriber fnfsub = new MySubscriber<>(0L, AGENT); - fnfSubscribers.put(args[2], fnfsub); - idToType.put(args[2], args[1]); - RSocket fnfclient = createClient.get(); - consoleUtils.info("Sending fnf with " + args[3] + " " + args[4]); - Publisher fnfpub = fnfclient.fireAndForget(new PayloadImpl(args[3], args[4])); - fnfpub.subscribe(fnfsub); - break; - default: - break; - } - } - - /** - * This function takes in an iterator that is parsing through the test, and collects all the parts - * that make up the channel functionality. It then create a thread that runs the test, which we - * wait to finish before proceeding with the other tests. - * - * @param args - * @param iter - * @param name - */ - private void handleChannel(String[] args, Iterator iter, String name, boolean pass) { - List commands = new ArrayList<>(); - String line = iter.next(); - // channel script should be bounded by curly braces - while (!line.equals("}")) { - commands.add(line); - line = iter.next(); - } - // set the initial payload - Payload initialPayload = new PayloadImpl(args[1], args[2]); - - // this is the subscriber that will request data from the server, like all the other test subscribers - MySubscriber testsub = new MySubscriber<>(1L, AGENT); - CountDownLatch c = new CountDownLatch(1); - - // we now create the publisher that the server will subscribe to with its own subscriber - // we want to give that subscriber a subscription that the client will use to send data to the server - RSocket client = createClient.get(); - AtomicReference mypct = new AtomicReference<>(); - Publisher pub = - client.requestChannel( - new Publisher() { - @Override - public void subscribe(Subscriber s) { - ParseMarble pm = new ParseMarble(s, AGENT); - TestSubscription ts = new TestSubscription(pm, initialPayload, s); - s.onSubscribe(ts); - ParseChannel pc = new ParseChannel(commands, testsub, pm, name, pass, AGENT); - ParseChannelThread pct = new ParseChannelThread(pc); - pct.start(); - mypct.set(pct); - c.countDown(); - } - }); - pub.subscribe(testsub); - try { - c.await(); - } catch (InterruptedException e) { - consoleUtils.info("interrupted"); - } - mypct.get().join(); - } - - /** - * This handles echo tests. This sets up a channel connection with the EchoSubscription, which we - * pass to the MySubscriber. - * - * @param args - */ - private void handleEchoChannel(String[] args) { - Payload initPayload = new PayloadImpl(args[1], args[2]); - MySubscriber testsub = new MySubscriber<>(1L, AGENT); - RSocket client = createClient.get(); - Publisher pub = - client.requestChannel( - new Publisher() { - @Override - public void subscribe(Subscriber s) { - EchoSubscription echoSub = new EchoSubscription(s); - s.onSubscribe(echoSub); - testsub.setEcho(echoSub); - s.onNext(initPayload); - } - }); - pub.subscribe(testsub); - } - - private boolean handleAwaitTerminal(String[] args) { - consoleUtils.info("Awaiting at Terminal"); - String id = args[2]; - if (idToType.get(id) == null) { - consoleUtils.failure("Could not find subscriber with given id"); - return false; - } else { - if (idToType.get(id).equals("fnf")) { - MySubscriber sub = fnfSubscribers.get(id); - return sub.awaitTerminalEvent(); - } else { - MySubscriber sub = payloadSubscribers.get(id); - return sub.awaitTerminalEvent(); - } - } - } - - private boolean handleAwaitAtLeast(String[] args) { - consoleUtils.info("Awaiting at Terminal for at least " + args[3]); - try { - String id = args[2]; - MySubscriber sub = payloadSubscribers.get(id); - return sub.awaitAtLeast(Long.parseLong(args[3])); - } catch (InterruptedException e) { - consoleUtils.error("interrupted"); - return false; - } - } - - private boolean handleAwaitNoEvents(String[] args) { - try { - String id = args[2]; - MySubscriber sub = payloadSubscribers.get(id); - return sub.awaitNoEvents(Long.parseLong(args[3])); - } catch (InterruptedException e) { - consoleUtils.error("Interrupted"); - return false; - } - } - - private boolean assertNoError(String[] args) { - String id = args[2]; - if (idToType.get(id) == null) { - consoleUtils.error("Could not find subscriber with given id"); - return false; - } else { - if (idToType.get(id).equals("fnf")) { - MySubscriber sub = fnfSubscribers.get(id); - try { - sub.assertNoErrors(); - return true; - } catch (Throwable ex) { - return false; - } - } else { - MySubscriber sub = payloadSubscribers.get(id); - sub.assertNoErrors(); - try { - sub.assertNoErrors(); - return true; - } catch (Throwable ex) { - return false; - } - } - } - } - - private boolean assertError(String[] args) { - consoleUtils.info("Checking for error"); - String id = args[2]; - if (idToType.get(id) == null) { - consoleUtils.error("Could not find subscriber with given id"); - return false; - } else { - if (idToType.get(id).equals("fnf")) { - MySubscriber sub = fnfSubscribers.get(id); - return sub.myAssertError(new Throwable()); - } else { - MySubscriber sub = payloadSubscribers.get(id); - return sub.myAssertError(new Throwable()); - } - } - } - - private boolean assertReceived(String[] args) { - consoleUtils.info("Verify we received " + args[3]); - String id = args[2]; - MySubscriber sub = payloadSubscribers.get(id); - String[] values = args[3].split("&&"); - List> assertList = new ArrayList<>(); - for (String v : values) { - String[] vals = v.split(","); - assertList.add(new Tuple<>(vals[0], vals[1])); - } - return sub.assertValues(assertList); - } - - private boolean assertReceivedN(String[] args) { - String id = args[2]; - MySubscriber sub = payloadSubscribers.get(id); - try { - sub.assertValueCount(Integer.parseInt(args[3])); - } catch (Throwable ex) { - return false; - } - return true; - } - - private boolean assertReceivedAtLeast(String[] args) { - String id = args[2]; - MySubscriber sub = payloadSubscribers.get(id); - return sub.assertReceivedAtLeast(Integer.parseInt(args[3])); - } - - private boolean assertCompleted(String[] args) { - consoleUtils.info("Handling onComplete"); - String id = args[2]; - if (idToType.get(id) == null) { - consoleUtils.error("Could not find subscriber with given id"); - return false; - } else { - if (idToType.get(id).equals("fnf")) { - MySubscriber sub = fnfSubscribers.get(id); - try { - sub.assertComplete(); - } catch (Throwable ex) { - return false; - } - return true; - } else { - MySubscriber sub = payloadSubscribers.get(id); - try { - sub.assertComplete(); - } catch (Throwable ex) { - return false; - } - return true; - } - } - } - - private boolean assertNoCompleted(String[] args) { - consoleUtils.info("Handling NO onComplete"); - String id = args[2]; - if (idToType.get(id) == null) { - consoleUtils.error("Could not find subscriber with given id"); - return false; - } else { - if (idToType.get(id).equals("fnf")) { - MySubscriber sub = fnfSubscribers.get(id); - try { - sub.assertNotComplete(); - } catch (Throwable ex) { - return false; - } - return true; - } else { - MySubscriber sub = payloadSubscribers.get(id); - try { - sub.assertNotComplete(); - } catch (Throwable ex) { - return false; - } - return true; - } - } - } - - private boolean assertCancelled(String[] args) { - String id = args[2]; - MySubscriber sub = payloadSubscribers.get(id); - return sub.isCancelled(); - } - - private void handleRequest(String[] args) { - Long num = Long.parseLong(args[1]); - String id = args[2]; - if (idToType.get(id) == null) { - consoleUtils.error("Could not find subscriber with given id"); - } else { - if (idToType.get(id).equals("fnf")) { - MySubscriber sub = fnfSubscribers.get(id); - consoleUtils.info("ClientDriver: Sending request for " + num); - sub.request(num); - } else { - MySubscriber sub = payloadSubscribers.get(id); - consoleUtils.info("ClientDriver: Sending request for " + num); - sub.request(num); - } - } - } - - private void handleTake(String[] args) { - String id = args[2]; - Long num = Long.parseLong(args[1]); - MySubscriber sub = payloadSubscribers.get(id); - sub.take(num); - } - - private void handleCancel(String[] args) { - String id = args[1]; - MySubscriber sub = payloadSubscribers.get(id); - sub.cancel(); - } - - private void handleEOF() { - MySubscriber fnfsub = new MySubscriber<>(0L, AGENT); - RSocket fnfclient = createClient.get(); - Publisher fnfpub = fnfclient.fireAndForget(new PayloadImpl("shutdown", "shutdown")); - fnfpub.subscribe(fnfsub); - fnfsub.request(1); - } - - /** This thread class parses through a single test and prints whether it succeeded or not */ - private class TestThread implements Runnable { - private Thread t; - private List test; - private long startTime; - private long endTime; - private boolean isRun = true; - - public TestThread(List test) { - this.t = new Thread(this); - this.test = test; - } - - @Override - public void run() { - String name = ""; - name = test.get(0).split("%%")[1]; - if (testList.size() > 0 && !testList.contains(name)) { - isRun = false; - return; - } - try { - consoleUtils.teststart(name); - TestResult result = parse(test.subList(1, test.size()), name); - if (result == TestResult.PASS) consoleUtils.success(name); - else if (result == TestResult.FAIL) consoleUtils.failure(name); - } catch (Exception e) { - e.printStackTrace(); - consoleUtils.failure(name); - } - } - - public void start() { - startTime = System.nanoTime(); - t.start(); - } - - public void join() { - try { - t.join(); - endTime = System.nanoTime(); - if (isRun) consoleUtils.time((endTime - startTime) / 1000000.0 + " MILLISECONDS\n"); - } catch (Exception e) { - consoleUtils.error("join exception"); - } - } - } - - /** A subscription for channel, it handles request(n) by sort of faking an initial payload. */ - private class TestSubscription implements Subscription { - private boolean firstRequest = true; - private ParseMarble pm; - private Payload initPayload; - private Subscriber sub; - - public TestSubscription(ParseMarble pm, Payload initpayload, Subscriber sub) { - this.pm = pm; - this.initPayload = initpayload; - this.sub = sub; - } - - @Override - public void cancel() { - pm.cancel(); - } - - @Override - public void request(long n) { - consoleUtils.info("TestSubscription: request " + n); - long m = n; - if (firstRequest) { - sub.onNext(initPayload); - firstRequest = false; - m = m - 1; - } - if (m > 0) pm.request(m); - } - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/client/JavaTCPClient.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/client/JavaTCPClient.java deleted file mode 100644 index 40f2ece91..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/client/JavaTCPClient.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2016 Facebook, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package io.rsocket.tckdrivers.client; - -import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; -import io.rsocket.transport.netty.client.TcpClientTransport; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.List; - -/** A client that implements a method to create RSockets, and runs the tests. */ -public class JavaTCPClient { - - private static URI uri; - - public boolean run(String realfile, String host, int port, boolean debug2, List tests) - throws MalformedURLException, URISyntaxException { - // we pass in our reactive socket here to the test suite - String file = "rsocket-tck-drivers/src/test/resources/clienttest.txt"; - if (realfile != null) file = realfile; - try { - setURI(new URI("tcp://" + host + ":" + port + "/rs")); - RSocket client = createClient(); - JavaClientDriver jd = new JavaClientDriver(file, () -> client, tests); - return jd.runTests(); - } catch (Exception e) { - e.printStackTrace(); - } - return false; - } - - public static void setURI(URI uri2) { - uri = uri2; - } - - /** - * A function that creates a RSocket on a new TCP connection. - * - * @return a RSocket - */ - public static RSocket createClient() { - if ("tcp".equals(uri.getScheme())) { - return RSocketFactory.connect() - .transport(TcpClientTransport.create(uri.getHost(), uri.getPort())) - .start() - .block(); - } else { - throw new UnsupportedOperationException("uri unsupported: " + uri); - } - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/AddThread.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/AddThread.java deleted file mode 100644 index e1e22888e..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/AddThread.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2016 Facebook, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package io.rsocket.tckdrivers.common; - -import java.util.concurrent.CountDownLatch; - -/** - * A thread that is created to wait to be able to append a marble string. We wait for the previous - * thread to have finished adding before allowing this thread to append, and after adding, we call - * countDown() to allow whatever thread waiting on this one to begin adding - */ -public class AddThread implements Runnable { - - private String marble; - private ParseMarble parseMarble; - private Thread t; - private CountDownLatch prev, curr; - - public AddThread( - String marble, ParseMarble parseMarble, CountDownLatch prev, CountDownLatch curr) { - this.marble = marble; - this.parseMarble = parseMarble; - this.t = new Thread(this); - this.prev = prev; - this.curr = curr; - } - - @Override - public void run() { - try { - // await for the previous latch to have counted down, if it exists - if (prev != null) prev.await(); - parseMarble.add(marble); - curr.countDown(); // count down on the current to unblock the next append - } catch (InterruptedException e) { - System.out.println("Interrupted in AddThread"); - } - } - - public void start() { - t.start(); - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ConsoleUtils.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ConsoleUtils.java deleted file mode 100644 index a306723fc..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ConsoleUtils.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.tckdrivers.common; - -/** This class handles everything that gets printed to the console */ -public class ConsoleUtils { - - private static final String ANSI_RESET = ""; //"\u001B[0m"; - private static final String ANSI_RED = ""; //\u001B[31m"; - private static final String ANSI_GREEN = ""; //\u001B[32m"; - private static final String ANSI_CYAN = ""; //\u001B[36m"; - private static final String ANSI_BLUE = ""; //\u001B[34m"; - private static boolean allPassed = true; - private String agent; - - public ConsoleUtils(String s) { - agent = s + " "; - } - - /** Logs something at the info level */ - public void info(String s) { - System.out.println(agent + "INFO: " + s); - } - - /** Logs a successful event */ - public void success(String s) { - System.out.println(ANSI_GREEN + agent + "SUCCESS: " + s + ANSI_RESET); - } - - /** - * Logs a failure event, and sets the allPassed boolean in this class to false. This can be used - * to check if there have been any failures in any tests after all tests have been run by the - * driver. - */ - public void failure(String s) { - allPassed = false; - System.out.println(ANSI_RED + agent + "FAILURE: " + s + ANSI_RESET); - } - - /** - * Logs an error event, and sets the allPassed boolean in this class to false. This can be used to - * check if there have been any failures in any tests after all tests have been run by the driver. - */ - public void error(String s) { - allPassed = false; - System.out.println(agent + "ERROR: " + s); - } - - /** Logs a time */ - public void time(String s) { - System.out.println(ANSI_CYAN + agent + "TIME: " + s + ANSI_RESET); - } - - /** Logs the initial payload the server has received */ - public void initialPayload(String s) { - System.out.println(agent + s); - } - - /** Logs the start of a test */ - public void teststart(String s) { - System.out.println(ANSI_BLUE + agent + "TEST STARTING: " + s + ANSI_RESET); - } - - /** - * Returns whether or not all tests up to the point this method is called, has passed - * - * @return false if there has been any failure or error, true if everything has passed - */ - public static boolean allPassed() { - return allPassed; - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/EchoSubscription.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/EchoSubscription.java deleted file mode 100644 index 38542c1c4..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/EchoSubscription.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2016 Facebook, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package io.rsocket.tckdrivers.common; - -import io.rsocket.Payload; -import io.rsocket.util.PayloadImpl; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -/** - * This class is a special subscription that allows us to implement echo tests without having to use - * ay complex complex Rx constructs. Subscriptions handle the sending of data to whoever is - * requesting it, this subscription has a very basic implementation of a backpressurebuffer that - * allows for flow control, and allows that the rate at which elements are produced to it can differ - * from the rate at which they are consumed. - * - *

This class should be passed inside of MySubscriber when one wants to do an echo test, so that - * all the values that the MySubscriber receives immediately gets buffered here and prepared to be - * sent. This implementation is needed because we want to send both the exact same data and - * metadata. If we used our ParseMarble class, we could append a function to allow dynamic changing - * of our argMap object, but even then, there are only small finite number of characters we can use - * in the marble diagram. - */ -public class EchoSubscription implements Subscription { - - /** This is our backpressure buffer */ - private Queue> q; - - private long numSent = 0; - private long numRequested = 0; - private Subscriber sub; - private boolean cancelled = false; - - public EchoSubscription(Subscriber sub) { - q = new ConcurrentLinkedQueue<>(); - this.sub = sub; - } - - /** - * Every time our buffer grows, if there are still requests to satisfy, we need to send as much as - * we can. We make this synchronized so we can avoid data races. - * - * @param payload - */ - public void add(Tuple payload) { - q.add(payload); - if (numSent < numRequested) request(0); - } - - @Override - public synchronized void request(long n) { - numRequested += n; - while (numSent < numRequested && !q.isEmpty() && !cancelled) { - Tuple tup = q.poll(); - System.out.println("Sending ... " + tup); - sub.onNext(new PayloadImpl(tup.getK(), tup.getV())); - numSent++; - } - } - - @Override - public void cancel() { - cancelled = true; - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/MySubscriber.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/MySubscriber.java deleted file mode 100644 index cab5457f5..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/MySubscriber.java +++ /dev/null @@ -1,243 +0,0 @@ - -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.tckdrivers.common; - -import io.reactivex.subscribers.TestSubscriber; -import io.rsocket.Payload; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import org.reactivestreams.*; - -// TODO Remove RXJava2 TestSubscriber and replace with Reactor-core StepVerifier -public class MySubscriber extends TestSubscriber { - - private ConsoleUtils consoleUtils; - - private long maxAwait = 5000; - /** - * this will be locked everytime we await at most some number of myValues, the await will always - * be with a timeout After the timeout, we look at the value inside the countdown latch to make - * sure we counted down the number of myValues we expected - */ - private CountDownLatch numOnNext = new CountDownLatch(Integer.MAX_VALUE); - /** This latch handles the logic in take. */ - private CountDownLatch takeLatch = new CountDownLatch(Integer.MAX_VALUE); - /** Keeps track if this test subscriber is myPassing */ - private boolean isPassing = true; - - private boolean isComplete = false; - - private EchoSubscription echosub; - - private boolean isEcho = false; - - public MySubscriber(long initialRequest, String agent) { - super(initialRequest); - this.consoleUtils = new ConsoleUtils(agent); - } - - @Override - public void onSubscribe(Subscription s) { - consoleUtils.info("MySubscriber: onSubscribe()"); - super.onSubscribe(s); - } - - @Override - public void onNext(T t) { - Payload p = (Payload) t; - Tuple tup = - new Tuple<>( - StandardCharsets.UTF_8.decode(p.getData()).toString(), - StandardCharsets.UTF_8.decode(p.getMetadata()).toString()); - consoleUtils.info("On NEXT got : " + tup.getK() + " " + tup.getV()); - if (isEcho) { - echosub.add(tup); - return; - } - super.onNext(t); - numOnNext.countDown(); - takeLatch.countDown(); - } - - public final boolean awaitAtLeast(long n) throws InterruptedException { - int waitIterations = 0; - while (valueCount() < n) { - if (waitIterations * 100 >= maxAwait) { - myFail("Await at least timed out"); - break; - } - numOnNext.await(100, TimeUnit.MILLISECONDS); - waitIterations++; - } - myPass("Got " + valueCount() + " out of " + n + " values expected"); - numOnNext = new CountDownLatch(Integer.MAX_VALUE); - return true; - } - - // could potentially have a race condition, but cancel is asynchronous anyways - public final boolean awaitNoEvents(long time) throws InterruptedException { - int nummyValues = values.size(); - boolean iscanceled = isCancelled(); - boolean iscompleted = isComplete; - Thread.sleep(time); - if (nummyValues == values.size() && iscanceled == isCancelled() && iscompleted == isComplete) { - myPass("No additional events"); - return true; - } else { - myFail("Received additional events"); - return false; - } - } - - public final boolean myAssertError(Throwable error) { - String prefix = ""; - if (done.getCount() != 0) { - prefix = "Subscriber still running! "; - } - int s = errors.size(); - if (s == 0) { - myFail(prefix + "No errors"); - return true; - } - myPass("Error received"); - return true; - } - - public final boolean assertValues(List> values) { - String prefix = ""; - if (done.getCount() != 0) { - prefix = "Subscriber still running! "; - } - if (!assertReceivedAtLeast(values.size())) { - return false; - } - for (int i = 0; i < values.size(); i++) { - Payload p = (Payload) this.values().get(i); - Tuple v = - new Tuple<>( - StandardCharsets.UTF_8.decode(p.getData()).toString(), - StandardCharsets.UTF_8.decode(p.getMetadata()).toString()); - Tuple u = values.get(i); - if (!Objects.equals(u, v)) { - myFail( - prefix - + "Values at position " - + i - + " differ; Expected: " - + valueAndClass(u) - + ", Actual: " - + valueAndClass(v)); - myFail("value does not match"); - return false; - } - } - myPass("All values match"); - return true; - } - - public final void assertValue(Tuple value) { - String prefix = ""; - if (done.getCount() != 0) { - prefix = "Subscriber still running! "; - } - int s = this.values.size(); - if (s != 1) { - myFail(prefix + "Expected: 1, Actual: " + valueCount()); - myFail("value does not match"); - } - Payload p = (Payload) values().get(0); - Tuple v = - new Tuple<>( - StandardCharsets.UTF_8.decode(p.getData()).toString(), - StandardCharsets.UTF_8.decode(p.getMetadata()).toString()); - if (!Objects.equals(value, v)) { - myFail(prefix + "Expected: " + valueAndClass(value) + ", Actual: " + valueAndClass(v)); - myFail("value does not match"); - } - myPass("Value matches"); - } - - public boolean assertReceivedAtLeast(int count) { - String prefix = ""; - if (done.getCount() != 0) { - prefix = "Subscriber still running! "; - } - int s = values.size(); - if (s < count) { - myFail(prefix + "Received less; Expected at least: " + count + ", Actual: " + s); - return false; - } - myPass("Received " + s + " myValues"); - return true; - } - - private void myFail(String message) { - isPassing = false; - consoleUtils.info("FAILED: " + message); - } - - private void myPass(String message) { - consoleUtils.info("PASSED: " + message); - } - - public boolean hasPassed() { - return isPassing; - } - - // there might be a race condition with take, so this behavior is defined as: either wait until we have received n - // myValues and then cancel, or cancel if we already have n myValues - public final void take(long n) { - if (values.size() >= n) { - // if we've already received at least n myValues, then we cancel - cancel(); - return; - } - int waitIterations = 0; - while (Integer.MAX_VALUE - takeLatch.getCount() < n) { - try { - // we keep track of how long we've waited for - if (waitIterations * 100 >= maxAwait) { - fail("Timeout in take"); - break; - } - takeLatch.await(100, TimeUnit.MILLISECONDS); - waitIterations++; - } catch (Exception e) { - consoleUtils.error("interrupted"); - } - } - } - - public Tuple getElement(int n) { - assert (n < values.size()); - Payload p = (Payload) values().get(n); - Tuple tup = - new Tuple<>( - StandardCharsets.UTF_8.decode(p.getData()).toString(), - StandardCharsets.UTF_8.decode(p.getMetadata()).toString()); - return tup; - } - - public final void setEcho(EchoSubscription echosub) { - isEcho = true; - this.echosub = echosub; - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ParseChannel.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ParseChannel.java deleted file mode 100644 index a80916a89..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ParseChannel.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 2016 Facebook, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package io.rsocket.tckdrivers.common; - -import io.rsocket.Payload; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; - -/** This class is exclusively used to parse channel commands on both the client and the server */ -public class ParseChannel { - - private List commands; - private MySubscriber sub; - private ParseMarble parseMarble; - private String name = ""; - private CountDownLatch prevRespondLatch; - private CountDownLatch currentRespondLatch; - private boolean pass = true; - public ConsoleUtils consoleUtils; - - public ParseChannel( - List commands, MySubscriber sub, ParseMarble parseMarble, String agent) { - this.commands = commands; - this.sub = sub; - this.parseMarble = parseMarble; - ParseThread parseThread = new ParseThread(parseMarble); - parseThread.start(); - consoleUtils = new ConsoleUtils(agent); - } - - public ParseChannel( - List commands, - MySubscriber sub, - ParseMarble parseMarble, - String name, - boolean pass, - String agent) { - this.commands = commands; - this.sub = sub; - this.parseMarble = parseMarble; - this.name = name; - ParseThread parseThread = new ParseThread(parseMarble); - parseThread.start(); - this.pass = pass; - consoleUtils = new ConsoleUtils(agent); - } - - /** - * This parses through each line of the marble test and executes the commands in each line Most of - * the functionality is the same as the switch statement in the JavaClientDriver, but this also - * allows for the channel to stage items to emit. - */ - public void parse() { - for (String line : commands) { - String[] args = line.split("%%"); - switch (args[0]) { - case "respond": - handleResponse(args); - break; - case "await": - switch (args[1]) { - case "terminal": - sub.awaitTerminalEvent(); - break; - case "atLeast": - try { - sub.awaitAtLeast(Long.parseLong(args[3])); - } catch (InterruptedException e) { - consoleUtils.error("interrupted"); - } - break; - case "no_events": - try { - sub.awaitNoEvents(Long.parseLong(args[3])); - } catch (InterruptedException e) { - consoleUtils.error("interrupted"); - } - break; - } - break; - case "assert": - switch (args[1]) { - case "no_error": - sub.assertNoErrors(); - break; - case "error": - sub.assertError(new Throwable()); - break; - case "received": - handleReceived(args); - break; - case "received_n": - sub.assertValueCount(Integer.parseInt(args[3])); - break; - case "received_at_least": - sub.assertReceivedAtLeast(Integer.parseInt(args[3])); - break; - case "completed": - sub.assertComplete(); - break; - case "no_completed": - sub.assertNotComplete(); - break; - case "canceled": - sub.isCancelled(); - break; - } - break; - case "take": - sub.take(Long.parseLong(args[1])); - break; - case "request": - sub.request(Long.parseLong(args[1])); - consoleUtils.info("requesting " + args[1]); - break; - case "cancel": - sub.cancel(); - break; - } - } - if (name.equals("")) { - name = "CHANNEL"; - } - if (sub.hasPassed() && this.pass) consoleUtils.success(name); - else if (!sub.hasPassed() && !this.pass) consoleUtils.success(name); - else consoleUtils.failure(name); - } - - /** - * On handling a command to respond with something, we create an AddThread and pass in latches to - * make sure that we don't let this thread request to append something before the previous thread - * has added something. - * - * @param args - */ - private void handleResponse(String[] args) { - consoleUtils.info("responding " + args); - if (currentRespondLatch == null) currentRespondLatch = new CountDownLatch(1); - AddThread addThread = - new AddThread(args[1], parseMarble, prevRespondLatch, currentRespondLatch); - prevRespondLatch = currentRespondLatch; - currentRespondLatch = new CountDownLatch(1); - addThread.start(); - } - - /** - * This verifies that the data received by our MySubscriber matches what we expected - * - * @param args - */ - private void handleReceived(String[] args) { - String[] values = args[3].split("&&"); - if (values.length == 1) { - String[] temp = values[0].split(","); - sub.assertValue(new Tuple<>(temp[0], temp[1])); - } else if (values.length > 1) { - List> assertList = new ArrayList<>(); - for (String v : values) { - String[] vals = v.split(","); - assertList.add(new Tuple<>(vals[0], vals[1])); - } - sub.assertValues(assertList); - } - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ParseChannelThread.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ParseChannelThread.java deleted file mode 100644 index 6446a5019..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ParseChannelThread.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2016 Facebook, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package io.rsocket.tckdrivers.common; - -/** This thread parses through channel tests */ -public class ParseChannelThread implements Runnable { - - private ParseChannel pc; - private Thread t; - - public ParseChannelThread(ParseChannel pc) { - this.pc = pc; - this.t = new Thread(this); - } - - @Override - public void run() { - pc.parse(); - } - - public void start() { - t.start(); - } - - public void join() { - try { - t.join(); - } catch (InterruptedException e) { - this.pc.consoleUtils.error("interrupted"); - } - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ParseMarble.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ParseMarble.java deleted file mode 100644 index 60b392c04..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ParseMarble.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright 2016 Facebook, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package io.rsocket.tckdrivers.common; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.rsocket.Payload; -import io.rsocket.util.PayloadImpl; -import java.util.*; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.CountDownLatch; -import org.reactivestreams.Subscriber; - -/** - * This class parses through a marble diagram, but also implements a backpressure buffer so that the - * rate at which producers append values can be much faster than the rate at which consumers consume - * values. The backpressure buffer is the marble queue. The append function synchronously grows the - * marble queue, and the request function synchronously increments the data requested as well as - * unblocks the latches that are basically preventing the parse() method from emitting data in the - * backpressure buffer that it should not. - */ -public class ParseMarble { - - private Queue marble; - private Subscriber s; - private boolean cancelled = false; - private Map> argMap; - private long numSent = 0; - private long numRequested = 0; - private CountDownLatch parseLatch; - private CountDownLatch sendLatch; - private ConsoleUtils consoleUtils; - - /** - * This constructor is useful if one already has the entire marble diagram before hand, so - * append() does not need to be called. - * - * @param marble the whole marble diagram - * @param s the subscriber - */ - public ParseMarble(String marble, Subscriber s, String agent) { - this.s = s; - this.marble = new ConcurrentLinkedQueue<>(); - if (marble.contains("&&")) { - String[] temp = marble.split("&&"); - marble = temp[0]; - ObjectMapper mapper = new ObjectMapper(); - try { - argMap = - mapper.readValue(temp[1], new TypeReference>>() {}); - } catch (Exception e) { - System.out.println("couldn't convert argmap"); - } - } - // we want to filter out and disregard '-' since we don't care about time - for (char c : marble.toCharArray()) { - if (c != '-') this.marble.add(c); - } - parseLatch = new CountDownLatch(1); - sendLatch = new CountDownLatch(1); - consoleUtils = new ConsoleUtils(agent); - } - - /** - * This constructor is useful for channel, when the marble diagram will be build incrementally. - * - * @param s the subscriber - */ - public ParseMarble(Subscriber s, String agent) { - this.s = s; - this.marble = new ConcurrentLinkedQueue<>(); - parseLatch = new CountDownLatch(1); - sendLatch = new CountDownLatch(1); - consoleUtils = new ConsoleUtils(agent); - } - - /** - * This method is synchronized because we don't want two threads to try to append to the string at - * once. Calling this method also unblocks the parseLatch, which allows non-emittable symbols to - * be sent. In other words, it allows onNext and onComplete to be sent even if we've sent all the - * values we've been requested of. - * - * @param m - */ - public synchronized void add(String m) { - consoleUtils.info("adding " + m); - for (char c : m.toCharArray()) { - if (c != '-') this.marble.add(c); - } - if (!marble.isEmpty()) parseLatch.countDown(); - } - - /** - * This method is synchronized because we only want to process one request at one time. Calling - * this method unblocks the sendLatch as well as the parseLatch if we have more requests, as it - * allows both emitted and non-emitted symbols to be sent, - * - * @param n - */ - public synchronized void request(long n) { - numRequested += n; - if (!marble.isEmpty()) { - parseLatch.countDown(); - } - if (n > 0) sendLatch.countDown(); - } - - /** This function calls parse and executes the specified behavior in each line of commands */ - public void parse() { - try { - // if cancel has been called, don't do anything - if (cancelled) return; - while (true) { - if (marble.isEmpty()) { - synchronized (parseLatch) { - if (parseLatch.getCount() == 0) parseLatch = new CountDownLatch(1); - parseLatch.await(); - } - parseLatch = new CountDownLatch(1); - } - char c = marble.poll(); - switch (c) { - case '|': - s.onComplete(); - consoleUtils.info("On complete sent"); - break; - case '#': - s.onError(new Throwable()); - consoleUtils.info("On error sent"); - break; - default: - if (numSent >= numRequested) { - synchronized (sendLatch) { - if (sendLatch.getCount() == 0) sendLatch = new CountDownLatch(1); - sendLatch.await(); - } - sendLatch = new CountDownLatch(1); - } - consoleUtils.info("numSent " + numSent + ": numRequested " + numRequested); - if (argMap != null) { - // this is hacky, but we only expect one key and one value - Map tempMap = argMap.get(c + ""); - if (tempMap == null) { - s.onNext(new PayloadImpl(c + "", c + "")); - consoleUtils.info("DATA SENT " + c + ", " + c); - } else { - List key = new ArrayList<>(tempMap.keySet()); - List value = new ArrayList<>(tempMap.values()); - s.onNext(new PayloadImpl(key.get(0), value.get(0))); - consoleUtils.info("DATA SENT " + key.get(0) + ", " + value.get(0)); - } - } else { - this.s.onNext(new PayloadImpl(c + "", c + "")); - consoleUtils.info("DATA SENT " + c + ", " + c); - } - - numSent++; - break; - } - } - } catch (InterruptedException e) { - consoleUtils.error("interrupted"); - } - } - - /** - * Since cancel is async, it just means that we will eventually, and rather quickly, stop emitting - * values. We do this to follow the reactive streams specifications that cancel should mean that - * the observable eventually stops emitting items. - */ - public void cancel() { - cancelled = true; - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ParseThread.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ParseThread.java deleted file mode 100644 index 8d2f999b8..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ParseThread.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2016 Facebook, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package io.rsocket.tckdrivers.common; - -/** This thread calls parse on the parseMarble object. */ -public class ParseThread implements Runnable { - - private ParseMarble pm; - private Thread t; - - public ParseThread(ParseMarble pm) { - this.pm = pm; - this.t = new Thread(this); - } - - @Override - public void run() { - pm.parse(); - } - - public void start() { - t.start(); - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ServerThread.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ServerThread.java deleted file mode 100644 index 4b277fc84..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/ServerThread.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.tckdrivers.common; - -import io.rsocket.tckdrivers.server.JavaTCPServer; - -public class ServerThread implements Runnable { - private Thread t; - private int port; - private String serverfile; - private JavaTCPServer server; - - public ServerThread(int port, String serverfile) { - t = new Thread(this); - this.port = port; - this.serverfile = serverfile; - server = new JavaTCPServer(); - } - - @Override - public void run() { - server.run(serverfile, port); - } - - public void start() { - t.start(); - } - - public void awaitStart() { - server.awaitStart(); - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/Tuple.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/Tuple.java deleted file mode 100644 index 6fb8bfc6c..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/common/Tuple.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2016 Facebook, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package io.rsocket.tckdrivers.common; - -import org.apache.commons.lang3.builder.HashCodeBuilder; - -/** - * Simple implementation of a tuple - * - * @param - * @param - */ -public class Tuple { - - private final K k; - private final V v; - - public Tuple(K k, V v) { - this.k = k; - this.v = v; - } - - /** - * Returns K - * - * @return K - */ - public K getK() { - return this.k; - } - - /** - * Returns V - * - * @return V - */ - public V getV() { - return this.v; - } - - @Override - public boolean equals(Object o) { - if (!o.getClass().isInstance(this)) { - return false; - } - @SuppressWarnings("unchecked") - Tuple temp = (Tuple) o; - return temp.getV().equals(this.getV()) && temp.getK().equals(this.getK()); - } - - @Override - public int hashCode() { - return new HashCodeBuilder() - .append(this.getK().hashCode()) - .append(this.getV().hashCode()) - .toHashCode(); - } - - @Override - public String toString() { - return getV().toString() + "," + getK().toString(); - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/main/Main.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/main/Main.java deleted file mode 100644 index e7a21d5af..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/main/Main.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.tckdrivers.main; - -import io.airlift.airline.Command; -import io.airlift.airline.Option; -import io.airlift.airline.SingleCommand; -import io.rsocket.tckdrivers.client.JavaTCPClient; -import io.rsocket.tckdrivers.server.JavaTCPServer; -import java.util.ArrayList; -import java.util.Arrays; - -/** This class is used to run both the server and the client, depending on the options given */ -@Command( - name = "rsocket-driver", - description = "This runs the client and servers that use the driver" -) -public class Main { - - @Option(name = "--debug", description = "set if you want frame level output") - public static boolean debug; - - @Option(name = "--server", description = "set if you want to run the server") - public static boolean server; - - @Option(name = "--client", description = "set if you want to run the client") - public static boolean client; - - @Option(name = "--host", description = "The host to connect to for the client") - public static String host; - - @Option(name = "--port", description = "The port") - public static int port; - - @Option( - name = "--file", - description = - "The script file to parse, make sure to give the client and server the " + "correct files" - ) - public static String file; - - @Option( - name = "--tests", - description = - "For the client only, optional argument to list out the tests you" - + " want to run, should be comma separated names" - ) - public static String tests; - - public static void main(String[] args) { - SingleCommand

cmd = SingleCommand.singleCommand(Main.class); - cmd.parse(args); - boolean passed = true; - if (server) { - new JavaTCPServer().run(file, port); - } else if (client) { - try { - if (tests != null) { - passed = - new JavaTCPClient().run(file, host, port, debug, Arrays.asList(tests.split(","))); - } else { - passed = new JavaTCPClient().run(file, host, port, debug, new ArrayList<>()); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - System.exit(passed ? 0 : 1); - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/main/TestMain.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/main/TestMain.java deleted file mode 100644 index 33a1d5718..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/main/TestMain.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.tckdrivers.main; - -import io.airlift.airline.Command; -import io.airlift.airline.Option; -import io.airlift.airline.SingleCommand; -import io.rsocket.tckdrivers.client.JavaTCPClient; -import io.rsocket.tckdrivers.common.ConsoleUtils; -import io.rsocket.tckdrivers.common.ServerThread; -import java.util.ArrayList; -import java.util.Arrays; - -/** - * This class fires up both the client and the server, is used for the Gradle task to run the tests - */ -@Command( - name = "rsocket-test-driver", - description = "This runs the client and servers that use the driver" -) -public class TestMain { - - @Option(name = "--debug", description = "set if you want frame level output") - public static boolean debug; - - @Option(name = "--port", description = "The port") - public static int port; - - @Option( - name = "--serverfile", - description = "The script file to parse, make sure to give the server the " + "correct file" - ) - public static String serverfile; - - @Option(name = "--clientfile", description = "The script file for the client to parse") - public static String clientfile; - - @Option( - name = "--tests", - description = - "For the client only, optional argument to list out the tests you" - + " want to run, should be comma separated names" - ) - public static String tests; - - public static void main(String[] args) { - SingleCommand cmd = SingleCommand.singleCommand(TestMain.class); - cmd.parse(args); - ServerThread st = new ServerThread(port, serverfile); - st.start(); - st.awaitStart(); - try { - if (tests != null) - new JavaTCPClient() - .run(clientfile, "localhost", port, debug, Arrays.asList(tests.split(","))); - else new JavaTCPClient().run(clientfile, "localhost", port, debug, new ArrayList<>()); - } catch (Exception e) { - e.printStackTrace(); - } - if (ConsoleUtils.allPassed()) { - System.out.println("ALL TESTS PASSED"); - System.exit(0); - } else { - System.out.println("SOME TESTS FAILED"); - System.exit(1); // exit with code 1 so that the gradle build process fails - } - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/server/JavaServerDriver.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/server/JavaServerDriver.java deleted file mode 100644 index 5a0e2c823..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/server/JavaServerDriver.java +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright 2016 Facebook, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package io.rsocket.tckdrivers.server; - -import io.rsocket.AbstractRSocket; -import io.rsocket.Closeable; -import io.rsocket.ConnectionSetupPayload; -import io.rsocket.Payload; -import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; -import io.rsocket.SocketAcceptor; -import io.rsocket.tckdrivers.common.ConsoleUtils; -import io.rsocket.tckdrivers.common.EchoSubscription; -import io.rsocket.tckdrivers.common.MySubscriber; -import io.rsocket.tckdrivers.common.ParseChannel; -import io.rsocket.tckdrivers.common.ParseChannelThread; -import io.rsocket.tckdrivers.common.ParseMarble; -import io.rsocket.tckdrivers.common.ParseThread; -import io.rsocket.tckdrivers.common.Tuple; -import io.rsocket.transport.netty.server.TcpServerTransport; -import java.io.BufferedReader; -import java.io.FileReader; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscription; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** This is the driver for the server. */ -public class JavaServerDriver { - - // these map initial payload -> marble, which dictates the behavior of the server - private Map, String> requestResponseMarbles; - private Map, String> requestStreamMarbles; - private Map, String> requestSubscriptionMarbles; - // channel doesn't have an initial payload, but maybe the first payload sent can be viewed as the "initial" - private Map, List> requestChannelCommands; - private Set> requestChannelFail; - private Set> requestEchoChannel; - // first try to implement single channel subscriber - private BufferedReader reader; - // the instance of the server so we can shut it down - private TcpServerTransport server; - private Closeable serverCloseable; - private CountDownLatch waitStart; - private ConsoleUtils consoleUtils = new ConsoleUtils("[SERVER]"); - - public JavaServerDriver(String path) { - requestResponseMarbles = new HashMap<>(); - requestStreamMarbles = new HashMap<>(); - requestSubscriptionMarbles = new HashMap<>(); - requestChannelCommands = new HashMap<>(); - requestEchoChannel = new HashSet<>(); - try { - reader = new BufferedReader(new FileReader(path)); - } catch (Exception e) { - consoleUtils.error("File not found"); - } - requestChannelFail = new HashSet<>(); - } - - // should be used if we want the server to be shutdown upon receiving some EOF packet - public JavaServerDriver(String path, TcpServerTransport server, CountDownLatch waitStart) { - this(path); - this.server = server; - this.waitStart = waitStart; - requestChannelFail = new HashSet<>(); - } - - /** Starts up the server */ - public void run() { - this.parse(); - this.serverCloseable = - RSocketFactory.receive() - .acceptor(new SocketAcceptorImpl()) - .transport(this.server) - .start() - .block(); - this.waitStart.countDown(); - serverCloseable.onClose().block(); - } - - class SocketAcceptorImpl implements SocketAcceptor { - @Override - public Mono accept(ConnectionSetupPayload setupPayload, RSocket reactiveSocket) { - AbstractRSocket abstractRSocket = - new AbstractRSocket() { - @Override - public Flux requestChannel(Publisher payloads) { - return Flux.from( - s -> { - try { - MySubscriber sub = new MySubscriber<>(0L, "[SERVER]"); - payloads.subscribe(sub); - // want to get equivalent of "initial payload" so we can route behavior, this might change in the future - sub.request(1); - sub.awaitAtLeast(1); - Tuple initpayload = - new Tuple<>(sub.getElement(0).getK(), sub.getElement(0).getV()); - consoleUtils.initialPayload( - "Received Channel " + initpayload.getK() + " " + initpayload.getV()); - // if this is a normal channel handler, then initiate the normal setup - if (requestChannelCommands.containsKey(initpayload)) { - ParseMarble pm = new ParseMarble(s, "[SERVER]"); - s.onSubscribe(new TestSubscription(pm)); - ParseChannel pc; - if (requestChannelFail.contains(initpayload)) - pc = - new ParseChannel( - requestChannelCommands.get(initpayload), - sub, - pm, - "CHANNEL", - false, - "[SERVER]"); - else - pc = - new ParseChannel( - requestChannelCommands.get(initpayload), sub, pm, "[SERVER]"); - ParseChannelThread pct = new ParseChannelThread(pc); - pct.start(); - } else if (requestEchoChannel.contains(initpayload)) { - EchoSubscription echoSubscription = new EchoSubscription(s); - s.onSubscribe(echoSubscription); - sub.setEcho(echoSubscription); - sub.request( - 10000); // request a large number, which basically means the client can send whatever - } else { - consoleUtils.error( - "Request channel payload " - + initpayload.getK() - + " " - + initpayload.getV() - + "has no handler"); - } - - } catch (Exception e) { - consoleUtils.failure("Interrupted"); - } - }); - } - - @Override - public final Mono fireAndForget(Payload payload) { - return Mono.from( - s -> { - Tuple initialPayload = - new Tuple<>( - StandardCharsets.UTF_8.decode(payload.getData()).toString(), - StandardCharsets.UTF_8.decode(payload.getMetadata()).toString()); - consoleUtils.initialPayload( - "Received firenforget " - + initialPayload.getK() - + " " - + initialPayload.getV()); - if (initialPayload.getK().equals("shutdown") - && initialPayload.getV().equals("shutdown")) { - try { - Thread.sleep(2000); - serverCloseable.close().subscribe(); - } catch (Exception e) { - e.printStackTrace(); - } - } - }); - } - - @Override - public Mono requestResponse(Payload payload) { - return Mono.from( - s -> { - Tuple initialPayload = - new Tuple<>( - StandardCharsets.UTF_8.decode(payload.getData()).toString(), - StandardCharsets.UTF_8.decode(payload.getMetadata()).toString()); - String marble = requestResponseMarbles.get(initialPayload); - consoleUtils.initialPayload( - "Received requestresponse " - + initialPayload.getK() - + " " - + initialPayload.getV()); - if (marble != null) { - ParseMarble pm = new ParseMarble(marble, s, "[SERVER]"); - s.onSubscribe(new TestSubscription(pm)); - new ParseThread(pm).start(); - } else { - consoleUtils.failure( - "Request response payload " - + initialPayload.getK() - + " " - + initialPayload.getV() - + "has no handler"); - } - }); - } - - @Override - public Flux requestStream(Payload payload) { - return Flux.from( - s -> { - Tuple initialPayload = - new Tuple<>( - StandardCharsets.UTF_8.decode(payload.getData()).toString(), - StandardCharsets.UTF_8.decode(payload.getMetadata()).toString()); - String marble = requestStreamMarbles.get(initialPayload); - consoleUtils.initialPayload( - "Received Stream " + initialPayload.getK() + " " + initialPayload.getV()); - if (marble != null) { - ParseMarble pm = new ParseMarble(marble, s, "[SERVER]"); - s.onSubscribe(new TestSubscription(pm)); - new ParseThread(pm).start(); - } else { - consoleUtils.failure( - "Request stream payload " - + initialPayload.getK() - + " " - + initialPayload.getV() - + "has no handler"); - } - }); - } - }; - - return Mono.just(abstractRSocket); - } - } - - /** - * This function parses through each line of the server handlers and primes the supporting data - * structures to be prepared for the first request. We return a RequestHandler object, which tells - * the RSocket server how to handle each type of request. The code inside the RequestHandler is - * lazily evaluated, and only does so before the first request. This may lead to a sort of bug, - * where getting concurrent requests as an initial request will nondeterministically lead to some - * data structures to not be initialized. - * - * @return a RequestHandler that details how to handle each type of request. - */ - public void parse() { - try { - String line = reader.readLine(); - while (line != null) { - String[] args = line.split("%%"); - switch (args[0]) { - case "rr": - // put the request response marble in the hash table - requestResponseMarbles.put(new Tuple<>(args[1], args[2]), args[3]); - break; - case "rs": - requestStreamMarbles.put(new Tuple<>(args[1], args[2]), args[3]); - break; - case "sub": - requestSubscriptionMarbles.put(new Tuple<>(args[1], args[2]), args[3]); - break; - case "channel": - handleChannel(args, reader); - case "echochannel": - requestEchoChannel.add(new Tuple<>(args[1], args[2])); - break; - default: - break; - } - - line = reader.readLine(); - } - - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * This handles the creation of a channel handler, it basically groups together all the lines of - * the channel script and put it in a map for later access - * - * @param args - * @param reader - * @throws IOException - */ - private void handleChannel(String[] args, BufferedReader reader) throws IOException { - Tuple initialPayload = new Tuple<>(args[1], args[2]); - if (args.length == 5) { - // we know that this test should fail - requestChannelFail.add(initialPayload); - } - String line = reader.readLine(); - List commands = new ArrayList<>(); - while (!line.equals("}")) { - commands.add(line); - line = reader.readLine(); - } - requestChannelCommands.put(initialPayload, commands); - } - - /** A trivial subscription used to interface with the ParseMarble object */ - private class TestSubscription implements Subscription { - private ParseMarble pm; - - public TestSubscription(ParseMarble pm) { - this.pm = pm; - } - - @Override - public void cancel() { - pm.cancel(); - } - - @Override - public void request(long n) { - consoleUtils.info("TestSubscription: request received for " + n); - pm.request(n); - } - } -} diff --git a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/server/JavaTCPServer.java b/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/server/JavaTCPServer.java deleted file mode 100644 index b147927fd..000000000 --- a/rsocket-tck-drivers/src/main/java/io/rsocket/tckdrivers/server/JavaTCPServer.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2016 Facebook, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package io.rsocket.tckdrivers.server; - -import io.rsocket.transport.netty.server.TcpServerTransport; -import java.util.concurrent.CountDownLatch; -import reactor.ipc.netty.tcp.TcpServer; - -/** An example of how to run the JavaServerDriver using the RSocket server creation tool in Java. */ -public class JavaTCPServer { - - private CountDownLatch mutex; - - public JavaTCPServer() { - mutex = new CountDownLatch(1); - } - - public void run(String realfile, int port) { - - String file = "rsocket-tck-drivers/src/test/resources/servertest.txt"; - - if (realfile != null) { - file = realfile; - } - - TcpServerTransport server = TcpServerTransport.create(TcpServer.create(port)); - - JavaServerDriver jsd = new JavaServerDriver(file, server, mutex); - jsd.run(); - } - - /** Blocks until the server has started */ - public void awaitStart() { - try { - mutex.await(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } -} diff --git a/rsocket-tck-drivers/src/test/resources/clientFireAndForgetTests.txt b/rsocket-tck-drivers/src/test/resources/clientFireAndForgetTests.txt deleted file mode 100644 index 48e561057..000000000 --- a/rsocket-tck-drivers/src/test/resources/clientFireAndForgetTests.txt +++ /dev/null @@ -1,15 +0,0 @@ -! -name%%fireAndForget2 -subscribe%%fnf%%22b84540-f916-4f7f-9211-12e235e30225%%c%%d -request%%1%%22b84540-f916-4f7f-9211-12e235e30225 -await%%terminal%%22b84540-f916-4f7f-9211-12e235e30225 -assert%%no_error%%22b84540-f916-4f7f-9211-12e235e30225 -assert%%completed%%22b84540-f916-4f7f-9211-12e235e30225 -! -name%%fireAndForget -subscribe%%fnf%%01a1ba74-cbd2-4c51-a759-a276b8051ca7%%a%%b -request%%1%%01a1ba74-cbd2-4c51-a759-a276b8051ca7 -await%%terminal%%01a1ba74-cbd2-4c51-a759-a276b8051ca7 -assert%%no_error%%01a1ba74-cbd2-4c51-a759-a276b8051ca7 -assert%%completed%%01a1ba74-cbd2-4c51-a759-a276b8051ca7 -EOF diff --git a/rsocket-tck-drivers/src/test/resources/clientRequestResponseTests.txt b/rsocket-tck-drivers/src/test/resources/clientRequestResponseTests.txt deleted file mode 100644 index 267df7a71..000000000 --- a/rsocket-tck-drivers/src/test/resources/clientRequestResponseTests.txt +++ /dev/null @@ -1,74 +0,0 @@ -! -name%%requestResponseMultipleCancel -subscribe%%rr%%0a72756d-d85a-40ee-91cc-94105e09b0b9%%aa%%bb -subscribe%%rr%%04c46a6c-2e73-4354-a5d7-b8fca320e837%%aa%%bb -subscribe%%rr%%038aa2d0-c9de-47c3-8df0-9f87b9e853d9%%aa%%bb -subscribe%%rr%%cf949ae5-6deb-41ea-b7ba-0ab6be29282c%%aa%%bb -request%%1%%0a72756d-d85a-40ee-91cc-94105e09b0b9 -request%%1%%04c46a6c-2e73-4354-a5d7-b8fca320e837 -request%%1%%038aa2d0-c9de-47c3-8df0-9f87b9e853d9 -request%%1%%cf949ae5-6deb-41ea-b7ba-0ab6be29282c -cancel%%0a72756d-d85a-40ee-91cc-94105e09b0b9 -cancel%%04c46a6c-2e73-4354-a5d7-b8fca320e837 -cancel%%038aa2d0-c9de-47c3-8df0-9f87b9e853d9 -cancel%%cf949ae5-6deb-41ea-b7ba-0ab6be29282c -! -name%%requestResponseRequestAfterCancel -subscribe%%rr%%6c77cfe0-7974-4a42-87b5-50f0060203b0%%request%%cancel -cancel%%6c77cfe0-7974-4a42-87b5-50f0060203b0 -request%%1%%6c77cfe0-7974-4a42-87b5-50f0060203b0 -await%%no_events%%6c77cfe0-7974-4a42-87b5-50f0060203b0%%2000 -assert%%received_n%%6c77cfe0-7974-4a42-87b5-50f0060203b0%%0 -! -name%%requestResponseMultipleSuccession -subscribe%%rr%%ce828eea-44c4-4148-8d85-d5ec522db267%%aa%%bb -subscribe%%rr%%d42957b6-6b27-4c9c-aebc-05898d0f155b%%aa%%bb -subscribe%%rr%%b4ae32f6-fcef-4812-95f6-08b74a1d87b7%%aa%%bb -subscribe%%rr%%a777b787-b45a-4f58-a2a5-5a749afc865c%%aa%%bb -request%%1%%ce828eea-44c4-4148-8d85-d5ec522db267 -request%%1%%d42957b6-6b27-4c9c-aebc-05898d0f155b -request%%1%%b4ae32f6-fcef-4812-95f6-08b74a1d87b7 -request%%1%%a777b787-b45a-4f58-a2a5-5a749afc865c -await%%atLeast%%ce828eea-44c4-4148-8d85-d5ec522db267%%1%%100 -await%%atLeast%%d42957b6-6b27-4c9c-aebc-05898d0f155b%%1%%100 -await%%atLeast%%b4ae32f6-fcef-4812-95f6-08b74a1d87b7%%1%%100 -await%%atLeast%%a777b787-b45a-4f58-a2a5-5a749afc865c%%1%%100 -! -name%%requestResponseInterleave -subscribe%%rr%%25287ba1-f155-4a4f-8380-7889cbb56cc4%%i%%j -subscribe%%rr%%5c2843f9-14ac-48b2-80c0-930c30d577cd%%k%%l -request%%1%%25287ba1-f155-4a4f-8380-7889cbb56cc4 -subscribe%%rr%%0963a539-797d-4a28-8694-f36d552d360b%%m%%n -request%%1%%0963a539-797d-4a28-8694-f36d552d360b -await%%atLeast%%25287ba1-f155-4a4f-8380-7889cbb56cc4%%1%%100 -request%%1%%5c2843f9-14ac-48b2-80c0-930c30d577cd -await%%atLeast%%5c2843f9-14ac-48b2-80c0-930c30d577cd%%1%%100 -await%%atLeast%%0963a539-797d-4a28-8694-f36d552d360b%%1%%100 -assert%%received%%25287ba1-f155-4a4f-8380-7889cbb56cc4%%homer,simpson -assert%%received%%5c2843f9-14ac-48b2-80c0-930c30d577cd%%bart,simpson -assert%%received%%0963a539-797d-4a28-8694-f36d552d360b%%seymour,skinner -! -name%%requestResponseCancel -subscribe%%rr%%638bd75b-93af-4e59-91a9-e4d39dc843a1%%g%%h -cancel%%638bd75b-93af-4e59-91a9-e4d39dc843a1 -assert%%canceled%%638bd75b-93af-4e59-91a9-e4d39dc843a1 -assert%%no_error%%638bd75b-93af-4e59-91a9-e4d39dc843a1 -assert%%no_completed%%638bd75b-93af-4e59-91a9-e4d39dc843a1 -assert%%received_n%%638bd75b-93af-4e59-91a9-e4d39dc843a1%%0 -! -name%%requestResponseError -subscribe%%rr%%cf946389-305a-4161-a937-ad605f27ab16%%c%%d -request%%1%%cf946389-305a-4161-a937-ad605f27ab16 -await%%terminal%%cf946389-305a-4161-a937-ad605f27ab16 -assert%%received_n%%cf946389-305a-4161-a937-ad605f27ab16%%0 -assert%%no_completed%%cf946389-305a-4161-a937-ad605f27ab16 -assert%%error%%cf946389-305a-4161-a937-ad605f27ab16 -! -name%%requestResponsePass -subscribe%%rr%%e30658d8-649e-4af6-b611-c4b0fdab06ec%%a%%b -request%%1%%e30658d8-649e-4af6-b611-c4b0fdab06ec -await%%atLeast%%e30658d8-649e-4af6-b611-c4b0fdab06ec%%1%%100 -assert%%no_error%%e30658d8-649e-4af6-b611-c4b0fdab06ec -assert%%completed%%e30658d8-649e-4af6-b611-c4b0fdab06ec -assert%%received%%e30658d8-649e-4af6-b611-c4b0fdab06ec%%a,a -EOF diff --git a/rsocket-tck-drivers/src/test/resources/clientRequestStreamTests.txt b/rsocket-tck-drivers/src/test/resources/clientRequestStreamTests.txt deleted file mode 100644 index 5d6dbd88a..000000000 --- a/rsocket-tck-drivers/src/test/resources/clientRequestStreamTests.txt +++ /dev/null @@ -1,136 +0,0 @@ -! -name%%requestStreamAfterCancel -subscribe%%rs%%3e26248d-b113-474c-91a6-e471a2d5bdf4%%after%%cancel -request%%1%%3e26248d-b113-474c-91a6-e471a2d5bdf4 -await%%atLeast%%3e26248d-b113-474c-91a6-e471a2d5bdf4%%1%%100 -assert%%received_n%%3e26248d-b113-474c-91a6-e471a2d5bdf4%%1 -cancel%%3e26248d-b113-474c-91a6-e471a2d5bdf4 -assert%%canceled%%3e26248d-b113-474c-91a6-e471a2d5bdf4 -request%%1%%3e26248d-b113-474c-91a6-e471a2d5bdf4 -await%%no_events%%3e26248d-b113-474c-91a6-e471a2d5bdf4%%2000 -assert%%received_n%%3e26248d-b113-474c-91a6-e471a2d5bdf4%%1 -! -name%%requestStreamMultipleCancel -subscribe%%rs%%1b1c2a49-39c2-400e-9e1a-90a676b8d8af%%aa%%bb -subscribe%%rs%%e0e9d447-3eb8-49f8-8e4d-095b6ac6c55c%%aa%%bb -subscribe%%rs%%4be5ba8b-db66-44ee-bc3b-a09586ba0423%%aa%%bb -subscribe%%rs%%f2204180-ab64-4446-954c-4546d869860d%%aa%%bb -request%%1%%1b1c2a49-39c2-400e-9e1a-90a676b8d8af -request%%1%%e0e9d447-3eb8-49f8-8e4d-095b6ac6c55c -request%%1%%4be5ba8b-db66-44ee-bc3b-a09586ba0423 -request%%1%%f2204180-ab64-4446-954c-4546d869860d -cancel%%1b1c2a49-39c2-400e-9e1a-90a676b8d8af -cancel%%e0e9d447-3eb8-49f8-8e4d-095b6ac6c55c -cancel%%4be5ba8b-db66-44ee-bc3b-a09586ba0423 -cancel%%f2204180-ab64-4446-954c-4546d869860d -! -name%%requestStreamMultipleSuccession -subscribe%%rs%%c6b7a8ca-f21b-4034-ba35-60552342aff8%%aa%%bb -subscribe%%rs%%aafc9e11-d861-4b67-9642-9629e30d61b9%%aa%%bb -subscribe%%rs%%adae6221-5a8a-466b-a74f-c93069847536%%aa%%bb -subscribe%%rs%%5cd313c7-6a23-4ced-aec3-9e0702796e9e%%aa%%bb -request%%1%%c6b7a8ca-f21b-4034-ba35-60552342aff8 -request%%1%%aafc9e11-d861-4b67-9642-9629e30d61b9 -request%%1%%adae6221-5a8a-466b-a74f-c93069847536 -request%%1%%5cd313c7-6a23-4ced-aec3-9e0702796e9e -await%%atLeast%%c6b7a8ca-f21b-4034-ba35-60552342aff8%%1%%100 -await%%atLeast%%aafc9e11-d861-4b67-9642-9629e30d61b9%%1%%100 -await%%atLeast%%adae6221-5a8a-466b-a74f-c93069847536%%1%%100 -await%%atLeast%%5cd313c7-6a23-4ced-aec3-9e0702796e9e%%1%%100 -! -name%%requestStreamInterleave -subscribe%%rs%%bd571431-baa9-4fc5-9f1a-5990084bad19%%o%%p -subscribe%%rs%%707358df-d310-49b3-be8b-0e5014ed3c76%%q%%r -request%%1%%bd571431-baa9-4fc5-9f1a-5990084bad19 -request%%2%%707358df-d310-49b3-be8b-0e5014ed3c76 -request%%1%%bd571431-baa9-4fc5-9f1a-5990084bad19 -await%%atLeast%%bd571431-baa9-4fc5-9f1a-5990084bad19%%2%%100 -await%%atLeast%%707358df-d310-49b3-be8b-0e5014ed3c76%%2%%100 -request%%2%%707358df-d310-49b3-be8b-0e5014ed3c76 -assert%%received_n%%bd571431-baa9-4fc5-9f1a-5990084bad19%%2 -await%%atLeast%%707358df-d310-49b3-be8b-0e5014ed3c76%%4%%100 -assert%%received_n%%707358df-d310-49b3-be8b-0e5014ed3c76%%4 -await%%terminal%%bd571431-baa9-4fc5-9f1a-5990084bad19 -await%%terminal%%707358df-d310-49b3-be8b-0e5014ed3c76 -assert%%completed%%bd571431-baa9-4fc5-9f1a-5990084bad19 -assert%%error%%707358df-d310-49b3-be8b-0e5014ed3c76 -! -name%%requestStreamCancel -subscribe%%rs%%1ba05cc1-77fd-49a4-8baa-46ba63bb9a4f%%m%%n -request%%1%%1ba05cc1-77fd-49a4-8baa-46ba63bb9a4f -cancel%%1ba05cc1-77fd-49a4-8baa-46ba63bb9a4f -await%%no_events%%1ba05cc1-77fd-49a4-8baa-46ba63bb9a4f%%1000 -assert%%canceled%%1ba05cc1-77fd-49a4-8baa-46ba63bb9a4f -assert%%no_error%%1ba05cc1-77fd-49a4-8baa-46ba63bb9a4f -assert%%no_completed%%1ba05cc1-77fd-49a4-8baa-46ba63bb9a4f -! -name%%requestStreamFlowControl2 -subscribe%%rs%%fc23fa57-fde4-42b9-a5ac-745271b3c2b1%%m%%n -request%%10%%fc23fa57-fde4-42b9-a5ac-745271b3c2b1 -await%%terminal%%fc23fa57-fde4-42b9-a5ac-745271b3c2b1 -await%%no_events%%fc23fa57-fde4-42b9-a5ac-745271b3c2b1%%2000 -assert%%received_at_least%%fc23fa57-fde4-42b9-a5ac-745271b3c2b1%%4 -assert%%no_error%%fc23fa57-fde4-42b9-a5ac-745271b3c2b1 -! -name%%requestStreamFlowControl -subscribe%%rs%%c2bf4531-c0a1-46a3-8d0c-ffec8153b2fd%%g%%h -request%%4%%c2bf4531-c0a1-46a3-8d0c-ffec8153b2fd -await%%atLeast%%c2bf4531-c0a1-46a3-8d0c-ffec8153b2fd%%4%%100 -await%%no_events%%c2bf4531-c0a1-46a3-8d0c-ffec8153b2fd%%2000 -assert%%received_n%%c2bf4531-c0a1-46a3-8d0c-ffec8153b2fd%%4 -assert%%no_completed%%c2bf4531-c0a1-46a3-8d0c-ffec8153b2fd -! -name%%requestStreamValueThenError -subscribe%%rs%%c66ae7ea-cf61-4e36-8f02-253831aa577e%%k%%l -request%%10%%c66ae7ea-cf61-4e36-8f02-253831aa577e -await%%atLeast%%c66ae7ea-cf61-4e36-8f02-253831aa577e%%1%%100 -assert%%received_at_least%%c66ae7ea-cf61-4e36-8f02-253831aa577e%%1 -await%%terminal%%c66ae7ea-cf61-4e36-8f02-253831aa577e -assert%%error%%c66ae7ea-cf61-4e36-8f02-253831aa577e -assert%%no_completed%%c66ae7ea-cf61-4e36-8f02-253831aa577e -! -name%%requestStreamError -subscribe%%rs%%dec1afa6-5367-4311-a8d4-0158c94d6bab%%i%%j -request%%1%%dec1afa6-5367-4311-a8d4-0158c94d6bab -await%%terminal%%dec1afa6-5367-4311-a8d4-0158c94d6bab -assert%%no_completed%%dec1afa6-5367-4311-a8d4-0158c94d6bab -assert%%error%%dec1afa6-5367-4311-a8d4-0158c94d6bab -assert%%received_n%%dec1afa6-5367-4311-a8d4-0158c94d6bab%%0 -! -name%%requestStreamInfinite -subscribe%%rs%%e01def13-cacc-46f4-93fb-9cb931465746%%g%%h -request%%3%%e01def13-cacc-46f4-93fb-9cb931465746 -await%%atLeast%%e01def13-cacc-46f4-93fb-9cb931465746%%3%%100 -request%%10%%e01def13-cacc-46f4-93fb-9cb931465746 -await%%atLeast%%e01def13-cacc-46f4-93fb-9cb931465746%%10%%100 -assert%%no_completed%%e01def13-cacc-46f4-93fb-9cb931465746 -assert%%no_error%%e01def13-cacc-46f4-93fb-9cb931465746 -assert%%received_n%%e01def13-cacc-46f4-93fb-9cb931465746%%13 -! -name%%requestStreamMultivalue -subscribe%%rs%%3e66165b-f6e8-4e62-b898-c97ec2016f4e%%e%%f -request%%3%%3e66165b-f6e8-4e62-b898-c97ec2016f4e -await%%atLeast%%3e66165b-f6e8-4e62-b898-c97ec2016f4e%%3%%100 -await%%terminal%%3e66165b-f6e8-4e62-b898-c97ec2016f4e -assert%%received_n%%3e66165b-f6e8-4e62-b898-c97ec2016f4e%%3 -assert%%completed%%3e66165b-f6e8-4e62-b898-c97ec2016f4e -assert%%no_error%%3e66165b-f6e8-4e62-b898-c97ec2016f4e -assert%%received%%3e66165b-f6e8-4e62-b898-c97ec2016f4e%%a,a&&b,b&&c,c -! -name%%requestStreamSingle -subscribe%%rs%%f188dc8a-406d-481b-9962-3d45574498d2%%c%%d -request%%1%%f188dc8a-406d-481b-9962-3d45574498d2 -await%%terminal%%f188dc8a-406d-481b-9962-3d45574498d2 -assert%%received_n%%f188dc8a-406d-481b-9962-3d45574498d2%%1 -assert%%no_error%%f188dc8a-406d-481b-9962-3d45574498d2 -assert%%completed%%f188dc8a-406d-481b-9962-3d45574498d2 -assert%%received%%f188dc8a-406d-481b-9962-3d45574498d2%%jimbo,jones -! -name%%requestStreamEmpty -subscribe%%rs%%3451f5a1-b5b0-43da-a4ad-9866b0cd6d2a%%a%%b -request%%1%%3451f5a1-b5b0-43da-a4ad-9866b0cd6d2a -await%%terminal%%3451f5a1-b5b0-43da-a4ad-9866b0cd6d2a -assert%%completed%%3451f5a1-b5b0-43da-a4ad-9866b0cd6d2a -assert%%received_n%%3451f5a1-b5b0-43da-a4ad-9866b0cd6d2a%%0 -assert%%no_error%%3451f5a1-b5b0-43da-a4ad-9866b0cd6d2a -EOF diff --git a/rsocket-tck-drivers/src/test/resources/clienttest.txt b/rsocket-tck-drivers/src/test/resources/clienttest.txt deleted file mode 100644 index 3ce154d10..000000000 --- a/rsocket-tck-drivers/src/test/resources/clienttest.txt +++ /dev/null @@ -1,50 +0,0 @@ -! -name%%streamTest3 -subscribe%%rs%%cf114994-3782-4cb6-943f-78e4a45e9359%%c%%d -request%%5%%cf114994-3782-4cb6-943f-78e4a45e9359 -await%%atLeast%%cf114994-3782-4cb6-943f-78e4a45e9359%%5%%100 -await%%no_events%%cf114994-3782-4cb6-943f-78e4a45e9359%%5000 -cancel%%cf114994-3782-4cb6-943f-78e4a45e9359 -assert%%canceled%%cf114994-3782-4cb6-943f-78e4a45e9359 -assert%%no_error%%cf114994-3782-4cb6-943f-78e4a45e9359 -! -name%%streamTest2 -subscribe%%rs%%a65df6ad-e0a9-4099-acab-096223cc49e0%%c%%d -request%%2%%a65df6ad-e0a9-4099-acab-096223cc49e0 -await%%atLeast%%a65df6ad-e0a9-4099-acab-096223cc49e0%%2%%100 -cancel%%a65df6ad-e0a9-4099-acab-096223cc49e0 -assert%%canceled%%a65df6ad-e0a9-4099-acab-096223cc49e0 -assert%%no_error%%a65df6ad-e0a9-4099-acab-096223cc49e0 -! -name%%streamTest1 -subscribe%%rs%%54a7116a-ef5a-4822-8339-94c2b281ab75%%a%%b -request%%3%%54a7116a-ef5a-4822-8339-94c2b281ab75 -await%%atLeast%%54a7116a-ef5a-4822-8339-94c2b281ab75%%3%%100 -assert%%received%%54a7116a-ef5a-4822-8339-94c2b281ab75%%a,b&&c,d&&e,f -request%%3%%54a7116a-ef5a-4822-8339-94c2b281ab75 -await%%terminal%%54a7116a-ef5a-4822-8339-94c2b281ab75 -assert%%completed%%54a7116a-ef5a-4822-8339-94c2b281ab75 -assert%%no_error%%54a7116a-ef5a-4822-8339-94c2b281ab75 -assert%%received_n%%54a7116a-ef5a-4822-8339-94c2b281ab75%%6 -! -name%%requestResponseTest3 -subscribe%%rr%%c07d2de3-f0e0-495c-9dfa-6df2cec90476%%e%%f -request%%1%%c07d2de3-f0e0-495c-9dfa-6df2cec90476 -await%%terminal%%c07d2de3-f0e0-495c-9dfa-6df2cec90476 -assert%%error%%c07d2de3-f0e0-495c-9dfa-6df2cec90476 -assert%%no_completed%%c07d2de3-f0e0-495c-9dfa-6df2cec90476 -! -name%%requestResponseTest2 -subscribe%%rr%%939d5a6b-605e-43b0-ab1e-6e726ba0aace%%c%%d -request%%1%%939d5a6b-605e-43b0-ab1e-6e726ba0aace -await%%terminal%%939d5a6b-605e-43b0-ab1e-6e726ba0aace -assert%%received%%939d5a6b-605e-43b0-ab1e-6e726ba0aace%%ding,dong -assert%%completed%%939d5a6b-605e-43b0-ab1e-6e726ba0aace -assert%%no_error%%939d5a6b-605e-43b0-ab1e-6e726ba0aace -! -name%%requestResponseTest1 -subscribe%%rr%%529c4e98-1b7a-4922-b25b-9afe6596c6c5%%a%%b -request%%1%%529c4e98-1b7a-4922-b25b-9afe6596c6c5 -await%%terminal%%529c4e98-1b7a-4922-b25b-9afe6596c6c5 -assert%%completed%%529c4e98-1b7a-4922-b25b-9afe6596c6c5 -EOF diff --git a/rsocket-tck-drivers/src/test/resources/serverFireAndForgetTests.txt b/rsocket-tck-drivers/src/test/resources/serverFireAndForgetTests.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/rsocket-tck-drivers/src/test/resources/serverRequestResponseTests.txt b/rsocket-tck-drivers/src/test/resources/serverRequestResponseTests.txt deleted file mode 100644 index 2ab7d61bd..000000000 --- a/rsocket-tck-drivers/src/test/resources/serverRequestResponseTests.txt +++ /dev/null @@ -1,8 +0,0 @@ -rr%%a%%b%%a-| -rr%%c%%d%%# -rr%%g%%h%%- -rr%%i%%j%%x-|&&{"x":{"homer":"simpson"}} -rr%%k%%l%%y-|&&{"y":{"bart":"simpson"}} -rr%%m%%n%%z-|&&{"z":{"seymour":"skinner"}} -rr%%aa%%bb%%a-| -rr%%request%%cancel%%a-| diff --git a/rsocket-tck-drivers/src/test/resources/serverRequestStreamTests.txt b/rsocket-tck-drivers/src/test/resources/serverRequestStreamTests.txt deleted file mode 100644 index 0e015338c..000000000 --- a/rsocket-tck-drivers/src/test/resources/serverRequestStreamTests.txt +++ /dev/null @@ -1,11 +0,0 @@ -rs%%a%%b%%-| -rs%%c%%d%%x-|&&{"x":{"jimbo":"jones"}} -rs%%e%%f%%a-b-c-| -rs%%g%%h%%a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-| -rs%%i%%j%%# -rs%%k%%l%%a-# -rs%%m%%n%%a-b-c-d-| -rs%%o%%p%%a-b-| -rs%%q%%r%%a-b-c--d-# -rs%%aa%%bb%%a-b-c-d-| -rs%%after%%cancel%%a-b-c-d-e-| diff --git a/rsocket-tck-drivers/src/test/resources/servertest.txt b/rsocket-tck-drivers/src/test/resources/servertest.txt deleted file mode 100644 index 8e22bca54..000000000 --- a/rsocket-tck-drivers/src/test/resources/servertest.txt +++ /dev/null @@ -1,6 +0,0 @@ -rs%%a%%b%%---a-----b-----c-----d--e--f---|&&{"a":{"a":"b"},"b":{"c":"d"},"c":{"e":"f"}} -rs%%c%%d%%---a-----b-----c-----d--e--f---|&&{"a":{"a":"b"},"b":{"c":"d"},"c":{"e":"f"}} -rr%%a%%b%%------------x------------------------|&&{"x":{"hello":"goodbye"}} -rr%%c%%d%%--------------------------------x--------------------------------|&&{"x":{"ding":"dong"}} -rr%%e%%f%%------------------------------------------# -rr%%g%%h%%- \ No newline at end of file diff --git a/rsocket-test/build.gradle b/rsocket-test/build.gradle index ecdcfbe60..282a65829 100644 --- a/rsocket-test/build.gradle +++ b/rsocket-test/build.gradle @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,11 +14,23 @@ * limitations under the License. */ +plugins { + id 'java-library' + id 'maven-publish' + id 'signing' +} + dependencies { - compile project(':rsocket-core') - compile 'junit:junit:4.12' - compile 'org.mockito:mockito-core:1.10.19' - compile 'org.hamcrest:hamcrest-library:1.3' - compile 'org.hdrhistogram:HdrHistogram:latest.release' - compile "io.projectreactor:reactor-test:3.1.0.M2" + api project(':rsocket-core') + api 'org.hdrhistogram:HdrHistogram' + api 'org.junit.jupiter:junit-jupiter-api' + + implementation 'io.projectreactor:reactor-test' + implementation 'org.assertj:assertj-core' + implementation 'org.mockito:mockito-core' + + // TODO: Remove after JUnit5 migration + implementation 'junit:junit' } + +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 5dfa0ad04..d74f59fd8 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/BaseClientServerTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/BaseClientServerTest.java @@ -1,17 +1,17 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; @@ -19,8 +19,9 @@ import static org.junit.Assert.assertEquals; import io.rsocket.Payload; -import io.rsocket.util.PayloadImpl; -import java.nio.charset.StandardCharsets; +import io.rsocket.util.DefaultPayload; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import reactor.core.publisher.Flux; @@ -34,7 +35,7 @@ public abstract class BaseClientServerTest> { public void testFireNForget10() { long outputCount = Flux.range(1, 10) - .flatMap(i -> setup.getRSocket().fireAndForget(new PayloadImpl("hello", "metadata"))) + .flatMap(i -> setup.getRSocket().fireAndForget(testPayload(i))) .doOnError(Throwable::printStackTrace) .count() .block(); @@ -46,7 +47,7 @@ public void testFireNForget10() { public void testPushMetadata10() { long outputCount = Flux.range(1, 10) - .flatMap(i -> setup.getRSocket().metadataPush(new PayloadImpl("", "metadata"))) + .flatMap(i -> setup.getRSocket().metadataPush(DefaultPayload.create("", "metadata"))) .doOnError(Throwable::printStackTrace) .count() .block(); @@ -54,17 +55,12 @@ public void testPushMetadata10() { assertEquals(0, outputCount); } - @Test(timeout = 10000) + @Test // (timeout = 10000) public void testRequestResponse1() { long outputCount = Flux.range(1, 1) .flatMap( - i -> - setup - .getRSocket() - .requestResponse(new PayloadImpl("hello", "metadata")) - .map( - payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString())) + i -> setup.getRSocket().requestResponse(testPayload(i)).map(Payload::getDataUtf8)) .doOnError(Throwable::printStackTrace) .count() .block(); @@ -77,12 +73,7 @@ public void testRequestResponse10() { long outputCount = Flux.range(1, 10) .flatMap( - i -> - setup - .getRSocket() - .requestResponse(new PayloadImpl("hello", "metadata")) - .map( - payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString())) + i -> setup.getRSocket().requestResponse(testPayload(i)).map(Payload::getDataUtf8)) .doOnError(Throwable::printStackTrace) .count() .block(); @@ -90,17 +81,28 @@ public void testRequestResponse10() { assertEquals(10, outputCount); } + private Payload testPayload(int metadataPresent) { + String metadata; + switch (metadataPresent % 5) { + case 0: + metadata = null; + break; + case 1: + metadata = ""; + break; + default: + metadata = "metadata"; + break; + } + return DefaultPayload.create("hello", metadata); + } + @Test(timeout = 10000) public void testRequestResponse100() { long outputCount = Flux.range(1, 100) .flatMap( - i -> - setup - .getRSocket() - .requestResponse(new PayloadImpl("hello", "metadata")) - .map( - payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString())) + i -> setup.getRSocket().requestResponse(testPayload(i)).map(Payload::getDataUtf8)) .doOnError(Throwable::printStackTrace) .count() .block(); @@ -113,12 +115,7 @@ public void testRequestResponse10_000() { long outputCount = Flux.range(1, 10_000) .flatMap( - i -> - setup - .getRSocket() - .requestResponse(new PayloadImpl("hello", "metadata")) - .map( - payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString())) + i -> setup.getRSocket().requestResponse(testPayload(i)).map(Payload::getDataUtf8)) .doOnError(Throwable::printStackTrace) .count() .block(); @@ -128,20 +125,28 @@ public void testRequestResponse10_000() { @Test(timeout = 10000) public void testRequestStream() { - Flux publisher = - setup.getRSocket().requestStream(new PayloadImpl("hello", "metadata")); + Flux publisher = setup.getRSocket().requestStream(testPayload(3)); long count = publisher.take(5).count().block(); assertEquals(5, count); } + @Test(timeout = 10000) + public void testRequestStreamAll() { + Flux publisher = setup.getRSocket().requestStream(testPayload(3)); + + long count = publisher.count().block(); + + assertEquals(10000, count); + } + @Test(timeout = 10000) public void testRequestStreamWithRequestN() { CountdownBaseSubscriber ts = new CountdownBaseSubscriber(); ts.expect(5); - setup.getRSocket().requestStream(new PayloadImpl("hello", "metadata")).subscribe(ts); + setup.getRSocket().requestStream(testPayload(3)).subscribe(ts); ts.await(); assertEquals(5, ts.count()); @@ -157,7 +162,7 @@ public void testRequestStreamWithRequestN() { public void testRequestStreamWithDelayedRequestN() { CountdownBaseSubscriber ts = new CountdownBaseSubscriber(); - setup.getRSocket().requestStream(new PayloadImpl("hello", "metadata")).subscribe(ts); + setup.getRSocket().requestStream(testPayload(3)).subscribe(ts); ts.expect(5); @@ -170,4 +175,72 @@ public void testRequestStreamWithDelayedRequestN() { assertEquals(10, ts.count()); } + + @Test(timeout = 10000) + public void testChannel0() { + Flux publisher = setup.getRSocket().requestChannel(Flux.empty()); + + long count = publisher.count().block(); + + assertEquals(0, count); + } + + @Test(timeout = 10000) + public void testChannel1() { + Flux publisher = setup.getRSocket().requestChannel(Flux.just(testPayload(0))); + + long count = publisher.count().block(); + + assertEquals(1, count); + } + + @Test(timeout = 10000) + public void testChannel3() { + Flux publisher = + setup + .getRSocket() + .requestChannel(Flux.just(testPayload(0), testPayload(1), testPayload(2))); + + long count = publisher.count().block(); + + assertEquals(3, count); + } + + @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); + } + + @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); + } + + @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); + } + + @Test(timeout = 60_000) + @Ignore + 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); + } } 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 67419d3a7..6f562875f 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/ClientSetupRule.java +++ b/rsocket-test/src/main/java/io/rsocket/test/ClientSetupRule.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -18,7 +18,8 @@ import io.rsocket.Closeable; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import java.util.function.BiFunction; @@ -30,6 +31,8 @@ import reactor.core.publisher.Mono; public class ClientSetupRule extends ExternalResource { + private static final String data = "hello world"; + private static final String metadata = "metadata"; private Supplier addressSupplier; private BiFunction clientConnector; @@ -45,19 +48,14 @@ public ClientSetupRule( this.serverInit = address -> - RSocketFactory.receive() - .acceptor((setup, sendingSocket) -> Mono.just(new TestRSocket())) - .transport(serverTransportSupplier.apply(address)) - .start() - .map(s -> (S) s) // TODO fix casting + RSocketServer.create((setup, rsocket) -> Mono.just(new TestRSocket(data, metadata))) + .bind(serverTransportSupplier.apply(address)) .block(); this.clientConnector = (address, server) -> - RSocketFactory.connect() - .transport(clientTransportSupplier.apply(address, server)) - .start() - .doOnError(t -> t.printStackTrace()) + RSocketConnector.connectWith(clientTransportSupplier.apply(address, server)) + .doOnError(Throwable::printStackTrace) .block(); } @@ -70,7 +68,7 @@ public void evaluate() throws Throwable { S server = serverInit.apply(address); client = clientConnector.apply(address, server); base.evaluate(); - server.close().block(); + server.dispose(); } }; } @@ -78,4 +76,12 @@ public void evaluate() throws Throwable { public RSocket getRSocket() { return client; } + + public String expectedPayloadData() { + return data; + } + + public String expectedPayloadMetadata() { + return metadata; + } } diff --git a/rsocket-test/src/main/java/io/rsocket/test/CountdownBaseSubscriber.java b/rsocket-test/src/main/java/io/rsocket/test/CountdownBaseSubscriber.java index 1f7d79ba5..8fb948e9f 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/CountdownBaseSubscriber.java +++ b/rsocket-test/src/main/java/io/rsocket/test/CountdownBaseSubscriber.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.rsocket.test; import io.rsocket.Payload; diff --git a/rsocket-test/src/main/java/io/rsocket/test/PerfTest.java b/rsocket-test/src/main/java/io/rsocket/test/PerfTest.java new file mode 100644 index 000000000..3830ec1bc --- /dev/null +++ b/rsocket-test/src/main/java/io/rsocket/test/PerfTest.java @@ -0,0 +1,17 @@ +package io.rsocket.test; + +import java.lang.annotation.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +/** + * {@code @PerfTest} is used to signal that the annotated test class or method is performance test, + * and is disabled unless enabled via setting the {@code TEST_PERF_ENABLED} environment variable to + * {@code true}. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@EnabledIfEnvironmentVariable(named = "TEST_PERF_ENABLED", matches = "(?i)true") +@Test +public @interface PerfTest {} diff --git a/rsocket-test/src/main/java/io/rsocket/test/PingClient.java b/rsocket-test/src/main/java/io/rsocket/test/PingClient.java index a2b8ec334..9017e854b 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/PingClient.java +++ b/rsocket-test/src/main/java/io/rsocket/test/PingClient.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -18,9 +18,11 @@ import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.util.PayloadImpl; +import io.rsocket.util.ByteBufPayload; import java.time.Duration; +import java.util.function.BiFunction; import org.HdrHistogram.Recorder; +import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -31,7 +33,7 @@ public class PingClient { public PingClient(Mono client) { this.client = client; - this.payload = new PayloadImpl("hello"); + this.payload = ByteBufPayload.create("hello"); } public Recorder startTracker(Duration interval) { @@ -49,7 +51,18 @@ public Recorder startTracker(Duration interval) { return histogram; } - public Flux startPingPong(int count, final Recorder histogram) { + public Flux requestResponsePingPong(int count, final Recorder histogram) { + return pingPong(RSocket::requestResponse, count, histogram); + } + + public Flux requestStreamPingPong(int count, final Recorder histogram) { + return pingPong(RSocket::requestStream, count, histogram); + } + + Flux pingPong( + BiFunction> interaction, + int count, + final Recorder histogram) { return client .flatMapMany( rsocket -> @@ -57,15 +70,15 @@ public Flux startPingPong(int count, final Recorder histogram) { .flatMap( i -> { long start = System.nanoTime(); - return rsocket - .requestResponse(payload) + return Flux.from(interaction.apply(rsocket, payload.retain())) + .doOnNext(Payload::release) .doFinally( signalType -> { long diff = System.nanoTime() - start; histogram.recordValue(diff); }); }, - 16)) + 64)) .doOnError(Throwable::printStackTrace); } } diff --git a/rsocket-test/src/main/java/io/rsocket/test/PingHandler.java b/rsocket-test/src/main/java/io/rsocket/test/PingHandler.java index f349a978c..47f40a59d 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/PingHandler.java +++ b/rsocket-test/src/main/java/io/rsocket/test/PingHandler.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,13 +16,13 @@ package io.rsocket.test; -import io.rsocket.AbstractRSocket; import io.rsocket.ConnectionSetupPayload; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; -import io.rsocket.util.PayloadImpl; +import io.rsocket.util.ByteBufPayload; import java.util.concurrent.ThreadLocalRandom; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class PingHandler implements SocketAcceptor { @@ -32,20 +32,27 @@ public class PingHandler implements SocketAcceptor { public PingHandler() { byte[] data = new byte[1024]; ThreadLocalRandom.current().nextBytes(data); - pong = new PayloadImpl(data); + pong = ByteBufPayload.create(data); } public PingHandler(byte[] data) { - pong = new PayloadImpl(data); + pong = ByteBufPayload.create(data); } @Override public Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket) { return Mono.just( - new AbstractRSocket() { + new RSocket() { @Override public Mono requestResponse(Payload payload) { - return Mono.just(pong); + payload.release(); + return Mono.just(pong.retain()); + } + + @Override + public Flux requestStream(Payload payload) { + payload.release(); + return Flux.range(0, 100).map(v -> pong.retain()); } }); } diff --git a/rsocket-test/src/main/java/io/rsocket/test/SlowTest.java b/rsocket-test/src/main/java/io/rsocket/test/SlowTest.java new file mode 100644 index 000000000..596cc0ffb --- /dev/null +++ b/rsocket-test/src/main/java/io/rsocket/test/SlowTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.test; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +/** + * {@code @SlowTest} is used to signal that the annotated test class or test method is slow running + * and will be disabled unless enabled via setting the {@code TEST_SLOW_ENABLED} environment + * variable to {@code true}. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@EnabledIfEnvironmentVariable(named = "TEST_SLOW_ENABLED", matches = "(?i)true") +@Test +public @interface SlowTest {} diff --git a/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java b/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java new file mode 100644 index 000000000..1e66abc5e --- /dev/null +++ b/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java @@ -0,0 +1,108 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.test; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.Payload; +import io.rsocket.frame.*; +import io.rsocket.util.DefaultPayload; +import io.rsocket.util.EmptyPayload; + +/** Test instances of all frame types. */ +public final class TestFrames { + private static final ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + private static final Payload emptyPayload = DefaultPayload.create(Unpooled.EMPTY_BUFFER); + + private TestFrames() {} + + /** @return {@link ByteBuf} representing test instance of Cancel frame */ + public static ByteBuf createTestCancelFrame() { + return CancelFrameCodec.encode(allocator, 1); + } + + /** @return {@link ByteBuf} representing test instance of Error frame */ + public static ByteBuf createTestErrorFrame() { + return ErrorFrameCodec.encode(allocator, 1, new RuntimeException()); + } + + /** @return {@link ByteBuf} representing test instance of Extension frame */ + public static ByteBuf createTestExtensionFrame() { + return ExtensionFrameCodec.encode( + allocator, 1, 1, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); + } + + /** @return {@link ByteBuf} representing test instance of Keep-Alive frame */ + public static ByteBuf createTestKeepaliveFrame() { + return KeepAliveFrameCodec.encode(allocator, false, 1, Unpooled.EMPTY_BUFFER); + } + + /** @return {@link ByteBuf} representing test instance of Lease frame */ + public static ByteBuf createTestLeaseFrame() { + return LeaseFrameCodec.encode(allocator, 1, 1, null); + } + + /** @return {@link ByteBuf} representing test instance of Metadata-Push frame */ + public static ByteBuf createTestMetadataPushFrame() { + return MetadataPushFrameCodec.encode(allocator, Unpooled.EMPTY_BUFFER); + } + + /** @return {@link ByteBuf} representing test instance of Payload frame */ + public static ByteBuf createTestPayloadFrame() { + return PayloadFrameCodec.encode(allocator, 1, false, true, false, null, Unpooled.EMPTY_BUFFER); + } + + /** @return {@link ByteBuf} representing test instance of Request-Channel frame */ + public static ByteBuf createTestRequestChannelFrame() { + return RequestChannelFrameCodec.encode( + allocator, 1, false, false, 1, null, Unpooled.EMPTY_BUFFER); + } + + /** @return {@link ByteBuf} representing test instance of Fire-and-Forget frame */ + public static ByteBuf createTestRequestFireAndForgetFrame() { + return RequestFireAndForgetFrameCodec.encode(allocator, 1, false, null, Unpooled.EMPTY_BUFFER); + } + + /** @return {@link ByteBuf} representing test instance of Request-N frame */ + public static ByteBuf createTestRequestNFrame() { + return RequestNFrameCodec.encode(allocator, 1, 1); + } + + /** @return {@link ByteBuf} representing test instance of Request-Response frame */ + public static ByteBuf createTestRequestResponseFrame() { + return RequestResponseFrameCodec.encodeReleasingPayload(allocator, 1, emptyPayload); + } + + /** @return {@link ByteBuf} representing test instance of Request-Stream frame */ + public static ByteBuf createTestRequestStreamFrame() { + return RequestStreamFrameCodec.encodeReleasingPayload(allocator, 1, 1L, emptyPayload); + } + + /** @return {@link ByteBuf} representing test instance of Setup frame */ + public static ByteBuf createTestSetupFrame() { + return SetupFrameCodec.encode( + allocator, + false, + 1, + 1, + Unpooled.EMPTY_BUFFER, + "metadataType", + "dataType", + EmptyPayload.INSTANCE); + } +} diff --git a/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java b/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java index 4e89be889..d48700445 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,17 +16,25 @@ package io.rsocket.test; -import io.rsocket.AbstractRSocket; import io.rsocket.Payload; -import io.rsocket.util.PayloadImpl; +import io.rsocket.RSocket; +import io.rsocket.util.DefaultPayload; +import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -public class TestRSocket extends AbstractRSocket { +public class TestRSocket implements RSocket { + private final String data; + private final String metadata; + + public TestRSocket(String data, String metadata) { + this.data = data; + this.metadata = metadata; + } @Override public Mono requestResponse(Payload payload) { - return Mono.just(new PayloadImpl("hello world", "metadata")); + return Mono.just(DefaultPayload.create(data, metadata)); } @Override @@ -43,4 +51,10 @@ public Mono metadataPush(Payload payload) { public Mono fireAndForget(Payload payload) { return Mono.empty(); } + + @Override + public Flux requestChannel(Publisher payloads) { + // TODO is defensive copy neccesary? + return Flux.from(payloads).map(Payload::retain); + } } diff --git a/rsocket-test/src/main/java/io/rsocket/test/TestSubscriber.java b/rsocket-test/src/main/java/io/rsocket/test/TestSubscriber.java index ceabb0fdb..62b6c242b 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TestSubscriber.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TestSubscriber.java @@ -1,6 +1,22 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.rsocket.test; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import io.rsocket.Payload; @@ -14,11 +30,14 @@ public static Subscriber create() { } public static Subscriber create(long initialRequest) { - Subscriber mock = mock(Subscriber.class); + @SuppressWarnings("unchecked") + Subscriber mock = mock(Subscriber.class); Mockito.doAnswer( invocation -> { - ((Subscription) invocation.getArguments()[0]).request(initialRequest); + if (initialRequest > 0) { + ((Subscription) invocation.getArguments()[0]).request(initialRequest); + } return null; }) .when(mock) @@ -32,7 +51,8 @@ public static Payload anyPayload() { } public static Subscriber createCancelling() { - Subscriber mock = mock(Subscriber.class); + @SuppressWarnings("unchecked") + Subscriber mock = mock(Subscriber.class); Mockito.doAnswer( invocation -> { diff --git a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java new file mode 100644 index 000000000..fc059c7d1 --- /dev/null +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -0,0 +1,418 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.test; + +import io.rsocket.Closeable; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.transport.ClientTransport; +import io.rsocket.transport.ServerTransport; +import io.rsocket.util.DefaultPayload; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.zip.GZIPInputStream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +public interface TransportTest { + + String MOCK_DATA = "test-data"; + String MOCK_METADATA = "metadata"; + String LARGE_DATA = read("words.shakespeare.txt.gz"); + Payload LARGE_PAYLOAD = DefaultPayload.create(LARGE_DATA, LARGE_DATA); + + static String read(String resourceName) { + + try (BufferedReader br = + new BufferedReader( + new InputStreamReader( + new GZIPInputStream( + TransportTest.class.getClassLoader().getResourceAsStream(resourceName))))) { + + return br.lines().map(String::toLowerCase).collect(Collectors.joining("\n\r")); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + @BeforeEach + default void setUp() { + Hooks.onOperatorDebug(); + } + + @AfterEach + default void close() { + getTransportPair().dispose(); + Hooks.resetOnOperatorDebug(); + } + + default Payload createTestPayload(int metadataPresent) { + String metadata1; + + switch (metadataPresent % 5) { + case 0: + metadata1 = null; + break; + case 1: + metadata1 = ""; + break; + default: + metadata1 = MOCK_METADATA; + break; + } + String metadata = metadata1; + + return DefaultPayload.create(MOCK_DATA, metadata); + } + + @DisplayName("makes 10 fireAndForget requests") + @Test + default void fireAndForget10() { + Flux.range(1, 10) + .flatMap(i -> getClient().fireAndForget(createTestPayload(i))) + .as(StepVerifier::create) + .expectNextCount(0) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 10 fireAndForget with Large Payload in Requests") + @Test + default void largePayloadFireAndForget10() { + Flux.range(1, 10) + .flatMap(i -> getClient().fireAndForget(LARGE_PAYLOAD)) + .as(StepVerifier::create) + .expectNextCount(0) + .expectComplete() + .verify(getTimeout()); + } + + default RSocket getClient() { + return getTransportPair().getClient(); + } + + Duration getTimeout(); + + TransportPair getTransportPair(); + + @DisplayName("makes 10 metadataPush requests") + @Test + default void metadataPush10() { + Flux.range(1, 10) + .flatMap(i -> getClient().metadataPush(DefaultPayload.create("", "test-metadata"))) + .as(StepVerifier::create) + .expectNextCount(0) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 10 metadataPush with Large Metadata in requests") + @Test + default void largePayloadMetadataPush10() { + Flux.range(1, 10) + .flatMap(i -> getClient().metadataPush(DefaultPayload.create("", LARGE_DATA))) + .as(StepVerifier::create) + .expectNextCount(0) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 1 requestChannel request with 0 payloads") + @Test + default void requestChannel0() { + getClient() + .requestChannel(Flux.empty()) + .as(StepVerifier::create) + .expectNextCount(0) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 1 requestChannel request with 1 payloads") + @Test + default void requestChannel1() { + getClient() + .requestChannel(Mono.just(createTestPayload(0))) + .as(StepVerifier::create) + .expectNextCount(1) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 1 requestChannel request with 200,000 payloads") + @Test + default void requestChannel200_000() { + Flux payloads = Flux.range(0, 200_000).map(this::createTestPayload); + + getClient() + .requestChannel(payloads) + .as(StepVerifier::create) + .expectNextCount(200_000) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 1 requestChannel request with 200 large payloads") + @Test + default void largePayloadRequestChannel200() { + Flux payloads = Flux.range(0, 200).map(__ -> LARGE_PAYLOAD); + + getClient() + .requestChannel(payloads) + .as(StepVerifier::create) + .expectNextCount(200) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 1 requestChannel request with 20,000 payloads") + @Test + default void requestChannel20_000() { + Flux payloads = Flux.range(0, 20_000).map(metadataPresent -> createTestPayload(7)); + + getClient() + .requestChannel(payloads) + .doOnNext(this::assertChannelPayload) + .as(StepVerifier::create) + .expectNextCount(20_000) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 1 requestChannel request with 2,000,000 payloads") + @SlowTest + default void requestChannel2_000_000() { + Flux payloads = Flux.range(0, 2_000_000).map(this::createTestPayload); + + getClient() + .requestChannel(payloads) + .as(StepVerifier::create) + .expectNextCount(2_000_000) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 1 requestChannel request with 3 payloads") + @Test + default void requestChannel3() { + AtomicLong requested = new AtomicLong(); + Flux payloads = + Flux.range(0, 3).doOnRequest(requested::addAndGet).map(this::createTestPayload); + + getClient() + .requestChannel(payloads) + .as(publisher -> StepVerifier.create(publisher, 3)) + .expectNextCount(3) + .expectComplete() + .verify(getTimeout()); + + Assertions.assertThat(requested.get()).isEqualTo(3L); + } + + @DisplayName("makes 1 requestChannel request with 512 payloads") + @Test + default void requestChannel512() { + Flux payloads = Flux.range(0, 512).map(this::createTestPayload); + + Flux.range(0, 1024) + .flatMap( + v -> Mono.fromRunnable(() -> check(payloads)).subscribeOn(Schedulers.elastic()), 12) + .blockLast(); + } + + default void check(Flux payloads) { + getClient() + .requestChannel(payloads) + .as(StepVerifier::create) + .expectNextCount(512) + .as("expected 512 items") + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 1 requestResponse request") + @Test + default void requestResponse1() { + getClient() + .requestResponse(createTestPayload(1)) + .doOnNext(this::assertPayload) + .as(StepVerifier::create) + .expectNextCount(1) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 10 requestResponse requests") + @Test + default void requestResponse10() { + Flux.range(1, 10) + .flatMap( + i -> getClient().requestResponse(createTestPayload(i)).doOnNext(v -> assertPayload(v))) + .as(StepVerifier::create) + .expectNextCount(10) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 100 requestResponse requests") + @Test + default void requestResponse100() { + Flux.range(1, 100) + .flatMap(i -> getClient().requestResponse(createTestPayload(i)).map(Payload::getDataUtf8)) + .as(StepVerifier::create) + .expectNextCount(100) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 100 requestResponse requests") + @Test + default void largePayloadRequestResponse100() { + Flux.range(1, 100) + .flatMap(i -> getClient().requestResponse(LARGE_PAYLOAD).map(Payload::getDataUtf8)) + .as(StepVerifier::create) + .expectNextCount(100) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 10,000 requestResponse requests") + @Test + default void requestResponse10_000() { + Flux.range(1, 10_000) + .flatMap(i -> getClient().requestResponse(createTestPayload(i)).map(Payload::getDataUtf8)) + .as(StepVerifier::create) + .expectNextCount(10_000) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 1 requestStream request and receives 10,000 responses") + @Test + default void requestStream10_000() { + getClient() + .requestStream(createTestPayload(3)) + .doOnNext(this::assertPayload) + .as(StepVerifier::create) + .expectNextCount(10_000) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 1 requestStream request and receives 5 responses") + @Test + default void requestStream5() { + getClient() + .requestStream(createTestPayload(3)) + .doOnNext(this::assertPayload) + .take(5) + .as(StepVerifier::create) + .expectNextCount(5) + .expectComplete() + .verify(getTimeout()); + } + + @DisplayName("makes 1 requestStream request and consumes result incrementally") + @Test + default void requestStreamDelayedRequestN() { + getClient() + .requestStream(createTestPayload(3)) + .take(10) + .as(StepVerifier::create) + .thenRequest(5) + .expectNextCount(5) + .thenRequest(5) + .expectNextCount(5) + .expectComplete() + .verify(getTimeout()); + } + + default void assertPayload(Payload p) { + TransportPair transportPair = getTransportPair(); + if (!transportPair.expectedPayloadData().equals(p.getDataUtf8()) + || !transportPair.expectedPayloadMetadata().equals(p.getMetadataUtf8())) { + throw new IllegalStateException("Unexpected payload"); + } + } + + default void assertChannelPayload(Payload p) { + if (!MOCK_DATA.equals(p.getDataUtf8()) || !MOCK_METADATA.equals(p.getMetadataUtf8())) { + throw new IllegalStateException("Unexpected payload"); + } + } + + final class TransportPair implements Disposable { + private static final String data = "hello world"; + private static final String metadata = "metadata"; + + private final RSocket client; + + private final S server; + + public TransportPair( + Supplier addressSupplier, + BiFunction clientTransportSupplier, + Function> serverTransportSupplier) { + + T address = addressSupplier.get(); + + server = + RSocketServer.create((setup, sendingSocket) -> Mono.just(new TestRSocket(data, metadata))) + .bind(serverTransportSupplier.apply(address)) + .block(); + + client = + RSocketConnector.connectWith(clientTransportSupplier.apply(address, server)) + .doOnError(Throwable::printStackTrace) + .block(); + } + + @Override + public void dispose() { + server.dispose(); + } + + RSocket getClient() { + return client; + } + + public String expectedPayloadData() { + return data; + } + + public String expectedPayloadMetadata() { + return metadata; + } + } +} diff --git a/rsocket-transport-aeron/build.gradle b/rsocket-test/src/main/java/io/rsocket/test/package-info.java similarity index 68% rename from rsocket-transport-aeron/build.gradle rename to rsocket-test/src/main/java/io/rsocket/test/package-info.java index 7069718ec..600ac2b82 100644 --- a/rsocket-transport-aeron/build.gradle +++ b/rsocket-test/src/main/java/io/rsocket/test/package-info.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,11 +14,8 @@ * limitations under the License. */ -apply plugin: 'java' +/** Utilities for testing RSocket components. */ +@NonNullApi +package io.rsocket.test; -dependencies { - compile project(':rsocket-core') - compile 'io.aeron:aeron-all:1.0.1' - - testCompile project(':rsocket-test') -} +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-test/src/main/resources/words.shakespeare.txt.gz b/rsocket-test/src/main/resources/words.shakespeare.txt.gz new file mode 100644 index 000000000..422a4b331 Binary files /dev/null and b/rsocket-test/src/main/resources/words.shakespeare.txt.gz differ diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/AeronDuplexConnection.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/AeronDuplexConnection.java deleted file mode 100644 index 1dc0e18ed..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/AeronDuplexConnection.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.aeron; - -import io.netty.buffer.Unpooled; -import io.rsocket.DuplexConnection; -import io.rsocket.Frame; -import io.rsocket.aeron.internal.reactivestreams.AeronChannel; -import org.agrona.concurrent.UnsafeBuffer; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; - -/** - * Implementation of {@link DuplexConnection} over Aeron using an {@link - * io.rsocket.aeron.internal.reactivestreams.AeronChannel} - */ -public class AeronDuplexConnection implements DuplexConnection { - private final String name; - private final AeronChannel channel; - private final MonoProcessor emptySubject; - - public AeronDuplexConnection(String name, AeronChannel channel) { - this.name = name; - this.channel = channel; - this.emptySubject = MonoProcessor.create(); - } - - @Override - public Mono send(Publisher frame) { - Flux buffers = - Flux.from(frame).map(f -> new UnsafeBuffer(f.content().nioBuffer())); - - return channel.send(buffers); - } - - @Override - public Flux receive() { - return channel - .receive() - .map(b -> Frame.from(Unpooled.wrappedBuffer(b.byteBuffer()))) - .doOnError(throwable -> throwable.printStackTrace()); - } - - @Override - public double availability() { - return channel.isActive() ? 1.0 : 0.0; - } - - @Override - public Mono close() { - return Mono.defer( - () -> { - try { - channel.close(); - emptySubject.onComplete(); - } catch (Exception e) { - emptySubject.onError(e); - } - return emptySubject; - }); - } - - @Override - public Mono onClose() { - return emptySubject; - } - - @Override - public String toString() { - return "AeronDuplexConnection{" - + "name='" - + name - + '\'' - + ", channel=" - + channel - + ", emptySubject=" - + emptySubject - + '}'; - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/client/AeronClientTransport.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/client/AeronClientTransport.java deleted file mode 100644 index 6b9ec1e73..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/client/AeronClientTransport.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.aeron.client; - -import io.rsocket.DuplexConnection; -import io.rsocket.aeron.AeronDuplexConnection; -import io.rsocket.aeron.internal.reactivestreams.AeronChannel; -import io.rsocket.aeron.internal.reactivestreams.AeronClientChannelConnector; -import io.rsocket.transport.ClientTransport; -import java.util.Objects; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Mono; - -/** {@link ClientTransport} implementation that uses Aeron as a transport */ -public class AeronClientTransport implements ClientTransport { - private final AeronClientChannelConnector connector; - private final AeronClientChannelConnector.AeronClientConfig config; - - public AeronClientTransport( - AeronClientChannelConnector connector, AeronClientChannelConnector.AeronClientConfig config) { - Objects.requireNonNull(config); - Objects.requireNonNull(connector); - this.connector = connector; - this.config = config; - } - - @Override - public Mono connect() { - Publisher channelPublisher = connector.apply(config); - - return Mono.from(channelPublisher) - .map(aeronChannel -> new AeronDuplexConnection("client", aeronChannel)); - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/AeronWrapper.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/AeronWrapper.java deleted file mode 100644 index 01cced728..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/AeronWrapper.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.aeron.internal; - -import io.aeron.Aeron; -import io.aeron.Image; -import io.aeron.Publication; -import io.aeron.Subscription; -import java.util.function.Function; - -/** */ -public interface AeronWrapper { - Aeron getAeron(); - - void availableImageHandler(Function handler); - - void unavailableImageHandlers(Function handler); - - default Subscription addSubscription(String channel, int streamdId) { - return getAeron().addSubscription(channel, streamdId); - } - - default Publication addPublication(String channel, int streamdId) { - return getAeron().addPublication(channel, streamdId); - } - - default void close() { - getAeron().close(); - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/Constants.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/Constants.java deleted file mode 100644 index c030bc9a1..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/Constants.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.aeron.internal; - -import java.util.concurrent.TimeUnit; -import org.agrona.concurrent.BackoffIdleStrategy; -import org.agrona.concurrent.IdleStrategy; -import org.agrona.concurrent.NoOpIdleStrategy; -import org.agrona.concurrent.SleepingIdleStrategy; - -public final class Constants { - - public static final int SERVER_STREAM_ID = 0; - public static final int CLIENT_STREAM_ID = 1; - public static final int SERVER_MANAGEMENT_STREAM_ID = 10; - public static final int CLIENT_MANAGEMENT_STREAM_ID = 11; - public static final IdleStrategy EVENT_LOOP_IDLE_STRATEGY; - public static final int AERON_MTU_SIZE = Integer.getInteger("aeron.mtu.length", 4096); - - static { - String idlStrategy = System.getProperty("idleStrategy"); - - if (NoOpIdleStrategy.class.getName().equalsIgnoreCase(idlStrategy)) { - EVENT_LOOP_IDLE_STRATEGY = new NoOpIdleStrategy(); - } else if (SleepingIdleStrategy.class.getName().equalsIgnoreCase(idlStrategy)) { - EVENT_LOOP_IDLE_STRATEGY = new SleepingIdleStrategy(TimeUnit.MILLISECONDS.toNanos(10)); - } else { - EVENT_LOOP_IDLE_STRATEGY = new BackoffIdleStrategy(1, 10, 1_000, 100_000); - } - } - - private Constants() {} -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/DefaultAeronWrapper.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/DefaultAeronWrapper.java deleted file mode 100644 index 526b5e871..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/DefaultAeronWrapper.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.aeron.internal; - -import io.aeron.Aeron; -import io.aeron.Image; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.function.Function; - -/** */ -public class DefaultAeronWrapper implements AeronWrapper { - private Set> availableImageHandlers; - private Set> unavailableImageHandlers; - - private Aeron aeron; - - public DefaultAeronWrapper() { - this.availableImageHandlers = new CopyOnWriteArraySet<>(); - this.unavailableImageHandlers = new CopyOnWriteArraySet<>(); - - Aeron.Context ctx = new Aeron.Context(); - - ctx.availableImageHandler(this::availableImageHandler); - ctx.unavailableImageHandler(this::unavailableImageHandler); - - this.aeron = Aeron.connect(ctx); - } - - public Aeron getAeron() { - return aeron; - } - - public void availableImageHandler(Function handler) { - availableImageHandlers.add(handler); - } - - public void unavailableImageHandlers(Function handler) { - unavailableImageHandlers.add(handler); - } - - private void availableImageHandler(Image image) { - Iterator> iterator = availableImageHandlers.iterator(); - - Set> itemsToRemove = new HashSet<>(); - while (iterator.hasNext()) { - Function handler = iterator.next(); - if (handler.apply(image)) { - itemsToRemove.add(handler); - } - } - - availableImageHandlers.removeAll(itemsToRemove); - } - - private void unavailableImageHandler(Image image) { - Iterator> iterator = unavailableImageHandlers.iterator(); - - while (iterator.hasNext()) { - Function handler = iterator.next(); - if (handler.apply(image)) { - iterator.remove(); - } - } - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/EventLoop.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/EventLoop.java deleted file mode 100644 index 5bd302f5d..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/EventLoop.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.aeron.internal; - -import java.util.function.IntSupplier; - -/** Interface for an EventLoop used by Aeron */ -public interface EventLoop { - /** - * Executes an IntSupplier that returns a number greater than 0 if it wants the the event loop to - * keep processing items, and zero its okay for the eventloop to execute an idle strategy - * - * @param r - */ - boolean execute(IntSupplier r); -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/SingleThreadedEventLoop.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/SingleThreadedEventLoop.java deleted file mode 100644 index 0a2b1c2bc..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/SingleThreadedEventLoop.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.aeron.internal; - -import java.util.concurrent.locks.LockSupport; -import java.util.function.IntSupplier; -import org.agrona.concurrent.IdleStrategy; -import org.agrona.concurrent.OneToOneConcurrentArrayQueue; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** */ -public class SingleThreadedEventLoop implements EventLoop { - private static final Logger logger = LoggerFactory.getLogger(SingleThreadedEventLoop.class); - private final String name; - private final Thread thread; - private final OneToOneConcurrentArrayQueue events = - new OneToOneConcurrentArrayQueue<>(32768); - - public SingleThreadedEventLoop(String name) { - this.name = name; - logger.info("Starting event loop named => {}", name); - - thread = new Thread(new SingleThreadedEventLoopRunnable()); - thread.setDaemon(true); - thread.setName("aeron-single-threaded-event-loop-" + name); - thread.start(); - } - - @Override - public boolean execute(IntSupplier r) { - boolean offer; - - if (thread == Thread.currentThread()) { - offer = events.offer(r); - } else { - synchronized (this) { - offer = events.offer(r); - } - LockSupport.unpark(thread); - } - - return offer; - } - - private int drain() { - int count = 0; - while (!events.isEmpty()) { - IntSupplier poll = events.poll(); - if (poll != null) { - count += poll.getAsInt(); - } - } - - return count; - } - - private class SingleThreadedEventLoopRunnable implements Runnable { - final IdleStrategy idleStrategy = Constants.EVENT_LOOP_IDLE_STRATEGY; - - @Override - public void run() { - while (true) { - try { - int count = drain(); - //if (count > 100) { - // System.out.println(name + " drained..." + count); - // } - idleStrategy.idle(count); - } catch (Throwable t) { - System.err.println("Something bad happened - an error made it to the event loop"); - t.printStackTrace(); - } - } - } - } - - @Override - public String toString() { - return "SingleThreadedEventLoop{" + "name='" + name + '\'' + '}'; - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronChannel.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronChannel.java deleted file mode 100644 index 95340a335..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronChannel.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.aeron.internal.reactivestreams; - -import io.aeron.Publication; -import io.aeron.Subscription; -import io.rsocket.aeron.internal.EventLoop; -import java.util.Objects; -import org.agrona.DirectBuffer; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** */ -public class AeronChannel implements ReactiveStreamsRemote.Channel, AutoCloseable { - private final String name; - private final Publication destination; - private final Subscription source; - private final AeronOutPublisher outPublisher; - private final EventLoop eventLoop; - - /** - * Creates on end of a bi-directional channel - * - * @param name name of the channel - * @param destination {@code Publication} to send data to - * @param source Aeron {@code Subscription} to listen to data on - * @param eventLoop {@link EventLoop} used to poll data on - * @param sessionId sessionId between the {@code Publication} and the remote {@code Subscription} - */ - public AeronChannel( - String name, - Publication destination, - Subscription source, - EventLoop eventLoop, - int sessionId) { - this.destination = destination; - this.source = source; - this.name = name; - this.eventLoop = eventLoop; - this.outPublisher = new AeronOutPublisher(name, sessionId, source, eventLoop); - } - - /** - * Subscribes to a stream of DirectBuffers and sends the to an Aeron Publisher - * - * @param in - * @return - */ - public Mono send(Flux in) { - AeronInSubscriber inSubscriber = new AeronInSubscriber(name, destination); - Objects.requireNonNull(in, "in must not be null"); - return Mono.create( - sink -> in.doOnComplete(sink::success).doOnError(sink::error).subscribe(inSubscriber)); - } - - /** - * Returns ReactiveStreamsRemote.Out of DirectBuffer that can only be subscribed to once per - * channel - * - * @return ReactiveStreamsRemote.Out of DirectBuffer - */ - public Flux receive() { - return outPublisher; - } - - @Override - public void close() throws Exception { - try { - destination.close(); - source.close(); - } catch (Throwable t) { - throw new Exception(t); - } - } - - @Override - public String toString() { - return "AeronChannel{" + "name='" + name + '\'' + '}'; - } - - @Override - public boolean isActive() { - return !destination.isClosed() && !source.isClosed(); - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelServer.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelServer.java deleted file mode 100644 index ee07b6582..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelServer.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.aeron.internal.reactivestreams; - -import io.aeron.FragmentAssembler; -import io.aeron.Publication; -import io.aeron.Subscription; -import io.aeron.logbuffer.FragmentHandler; -import io.aeron.logbuffer.Header; -import io.rsocket.Closeable; -import io.rsocket.aeron.internal.AeronWrapper; -import io.rsocket.aeron.internal.Constants; -import io.rsocket.aeron.internal.EventLoop; -import io.rsocket.aeron.internal.NotConnectedException; -import io.rsocket.aeron.internal.reactivestreams.messages.AckConnectEncoder; -import io.rsocket.aeron.internal.reactivestreams.messages.ConnectDecoder; -import io.rsocket.aeron.internal.reactivestreams.messages.MessageHeaderDecoder; -import io.rsocket.aeron.internal.reactivestreams.messages.MessageHeaderEncoder; -import java.net.SocketAddress; -import java.nio.ByteBuffer; -import java.time.Duration; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import org.agrona.DirectBuffer; -import org.agrona.concurrent.UnsafeBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; - -/** - * Implementation of {@link - * io.rsocket.aeron.internal.reactivestreams.ReactiveStreamsRemote.ChannelServer} that manages - * {@link AeronChannel}s. - */ -public class AeronChannelServer - extends ReactiveStreamsRemote.ChannelServer { - private static final Logger logger = LoggerFactory.getLogger(AeronChannelServer.class); - private final AeronWrapper aeronWrapper; - private final AeronSocketAddress managementSubscriptionSocket; - private final AtomicBoolean started = new AtomicBoolean(false); - private final ConcurrentHashMap serverSubscriptions; - private volatile boolean running = true; - private final EventLoop eventLoop; - private Subscription managementSubscription; - private AeronChannelStartedServer startServer; - - private AeronChannelServer( - AeronChannelConsumer channelConsumer, - AeronWrapper aeronWrapper, - AeronSocketAddress managementSubscriptionSocket, - EventLoop eventLoop) { - super(channelConsumer); - this.aeronWrapper = aeronWrapper; - this.managementSubscriptionSocket = managementSubscriptionSocket; - this.eventLoop = eventLoop; - this.serverSubscriptions = new ConcurrentHashMap<>(); - } - - public static AeronChannelServer create( - AeronChannelConsumer channelConsumer, - AeronWrapper aeronWrapper, - AeronSocketAddress managementSubscriptionSocket, - EventLoop eventLoop) { - return new AeronChannelServer( - channelConsumer, aeronWrapper, managementSubscriptionSocket, eventLoop); - } - - @Override - public AeronChannelStartedServer start() { - if (!started.compareAndSet(false, true)) { - throw new IllegalStateException("server already started"); - } - - logger.debug( - "management server starting on {}, stream id {}", - managementSubscriptionSocket.getChannel(), - Constants.SERVER_MANAGEMENT_STREAM_ID); - - this.managementSubscription = - aeronWrapper.addSubscription( - managementSubscriptionSocket.getChannel(), Constants.SERVER_MANAGEMENT_STREAM_ID); - - this.startServer = new AeronChannelStartedServer(); - - poll(); - - return startServer; - } - - private final FragmentAssembler fragmentAssembler = - new FragmentAssembler( - new FragmentHandler() { - private final MessageHeaderDecoder messageHeaderDecoder = new MessageHeaderDecoder(); - private final ConnectDecoder connectDecoder = new ConnectDecoder(); - private final MessageHeaderEncoder messageHeaderEncoder = new MessageHeaderEncoder(); - private final AckConnectEncoder ackConnectEncoder = new AckConnectEncoder(); - - @Override - public void onFragment(DirectBuffer buffer, int offset, int length, Header header) { - messageHeaderDecoder.wrap(buffer, offset); - - // Do not change the order or remove fields - final int actingBlockLength = messageHeaderDecoder.blockLength(); - final int templateId = messageHeaderDecoder.templateId(); - final int schemaId = messageHeaderDecoder.schemaId(); - final int actingVersion = messageHeaderDecoder.version(); - - if (templateId == ConnectDecoder.TEMPLATE_ID) { - offset += messageHeaderDecoder.encodedLength(); - connectDecoder.wrap(buffer, offset, actingBlockLength, actingVersion); - - // Do not change the order or remove fields - long channelId = connectDecoder.channelId(); - String receivingChannel = connectDecoder.receivingChannel(); - int receivingStreamId = connectDecoder.receivingStreamId(); - String sendingChannel = connectDecoder.sendingChannel(); - int sendingStreamId = connectDecoder.sendingStreamId(); - int clientSessionId = connectDecoder.clientSessionId(); - String clientManagementChannel = connectDecoder.clientManagementChannel(); - - logger.debug( - "server creating a AeronChannel with channel id {} receiving on receivingChannel {}, receivingStreamId {}, sendingChannel {}, sendingStreamId {}", - channelId, - receivingChannel, - receivingStreamId, - sendingChannel, - sendingStreamId); - - // Server sends to receiving Channel - Publication destination = - aeronWrapper.addPublication(receivingChannel, receivingStreamId); - int sessionId = destination.sessionId(); - logger.debug( - "server created publication to channel {}, stream id {}, and session id {}", - receivingChannel, - receivingStreamId, - sessionId); - - // Server listens to sending channel - Subscription source = - serverSubscriptions.computeIfAbsent( - sendingChannel, - s -> aeronWrapper.addSubscription(sendingChannel, sendingStreamId)); - logger.debug( - "server created subscription to channel {}, stream id {}", - sendingChannel, - sendingStreamId); - - AeronChannel aeronChannel = - new AeronChannel("server", destination, source, eventLoop, clientSessionId); - logger.debug( - "server create AeronChannel with destination channel {}, source channel {}, and clientSesseionId {}"); - - channelConsumer.accept(aeronChannel); - - Publication managementPublication = - aeronWrapper.addPublication( - clientManagementChannel, Constants.CLIENT_MANAGEMENT_STREAM_ID); - logger.debug( - "server created management publication to channel {}", clientManagementChannel); - - final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4096); - final UnsafeBuffer directBuffer = new UnsafeBuffer(byteBuffer); - int bufferOffset = 0; - - messageHeaderEncoder - .wrap(directBuffer, bufferOffset) - .blockLength(AckConnectEncoder.BLOCK_LENGTH) - .templateId(AckConnectEncoder.TEMPLATE_ID) - .schemaId(AckConnectEncoder.SCHEMA_ID) - .version(AckConnectEncoder.SCHEMA_VERSION); - - bufferOffset += messageHeaderEncoder.encodedLength(); - - ackConnectEncoder - .wrap(directBuffer, bufferOffset) - .channelId(channelId) - .serverSessionId(destination.sessionId()); - - logger.debug( - "server sending AckConnect message to channel {}", clientManagementChannel); - - long offer; - do { - offer = managementPublication.offer(directBuffer); - if (offer == Publication.CLOSED) { - throw new NotConnectedException(); - } - } while (offer < 0); - } - } - }); - - private int poll() { - int poll; - try { - poll = managementSubscription.poll(fragmentAssembler, 4096); - } finally { - if (running) { - boolean execute = eventLoop.execute(this::poll); - if (!execute) { - running = false; - throw new IllegalStateException("unable to keep polling, eventLoop rejection"); - } - } - } - - return poll; - } - - public interface AeronChannelConsumer - extends ReactiveStreamsRemote.ChannelConsumer {} - - public class AeronChannelStartedServer implements ReactiveStreamsRemote.StartedServer, Closeable { - private final MonoProcessor onClose = MonoProcessor.create(); - - private CountDownLatch latch = new CountDownLatch(1); - - public AeronWrapper getAeronWrapper() { - return aeronWrapper; - } - - public EventLoop getEventLoop() { - return eventLoop; - } - - @Override - public SocketAddress getServerAddress() { - return managementSubscriptionSocket; - } - - @Override - public int getServerPort() { - return managementSubscriptionSocket.getPort(); - } - - @Override - public void awaitShutdown(long duration, TimeUnit durationUnit) { - Duration d = Duration.ofMillis(durationUnit.toMillis(duration)); - close().block(d); - } - - @Override - public void awaitShutdown() { - close().block(); - } - - @Override - public void shutdown() { - close().subscribe(); - } - - @Override - public Mono close() { - return Mono.defer( - () -> { - running = false; - managementSubscription.close(); - return onClose; - }); - } - - @Override - public Mono onClose() { - return onClose; - } - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronClientChannelConnector.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronClientChannelConnector.java deleted file mode 100644 index e103712c4..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronClientChannelConnector.java +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.aeron.internal.reactivestreams; - -import io.aeron.FragmentAssembler; -import io.aeron.Publication; -import io.aeron.Subscription; -import io.aeron.logbuffer.FragmentHandler; -import io.aeron.logbuffer.Header; -import io.rsocket.aeron.internal.AeronWrapper; -import io.rsocket.aeron.internal.Constants; -import io.rsocket.aeron.internal.EventLoop; -import io.rsocket.aeron.internal.NotConnectedException; -import io.rsocket.aeron.internal.reactivestreams.messages.AckConnectDecoder; -import io.rsocket.aeron.internal.reactivestreams.messages.ConnectEncoder; -import io.rsocket.aeron.internal.reactivestreams.messages.MessageHeaderDecoder; -import io.rsocket.aeron.internal.reactivestreams.messages.MessageHeaderEncoder; -import java.nio.ByteBuffer; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.IntConsumer; -import org.agrona.DirectBuffer; -import org.agrona.concurrent.UnsafeBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoSource; -import reactor.core.publisher.Operators; - -/** Brokers a connection to a remote Aeron server. */ -public class AeronClientChannelConnector - implements ReactiveStreamsRemote.ClientChannelConnector< - AeronClientChannelConnector.AeronClientConfig, AeronChannel>, - AutoCloseable { - - private static final Logger logger = LoggerFactory.getLogger(AeronClientChannelConnector.class); - - private static final AtomicLong CHANNEL_ID_COUNTER = new AtomicLong(); - - private final AeronWrapper aeronWrapper; - - // Subscriptions clients listen to responses on - private final ConcurrentHashMap clientSubscriptions; - private final ConcurrentHashMap serverSessionIdConsumerMap; - - private final Subscription managementSubscription; - - private final EventLoop eventLoop; - - private volatile boolean running = true; - - private AeronClientChannelConnector( - AeronWrapper aeronWrapper, - AeronSocketAddress managementSubscriptionSocket, - EventLoop eventLoop) { - this.aeronWrapper = aeronWrapper; - - logger.debug( - "client creating a management subscription on channel {}, stream id {}", - managementSubscriptionSocket.getChannel(), - Constants.CLIENT_MANAGEMENT_STREAM_ID); - - this.managementSubscription = - aeronWrapper.addSubscription( - managementSubscriptionSocket.getChannel(), Constants.CLIENT_MANAGEMENT_STREAM_ID); - this.eventLoop = eventLoop; - this.clientSubscriptions = new ConcurrentHashMap<>(); - this.serverSessionIdConsumerMap = new ConcurrentHashMap<>(); - - poll(); - } - - public static AeronClientChannelConnector create( - AeronWrapper wrapper, AeronSocketAddress managementSubscriptionSocket, EventLoop eventLoop) { - return new AeronClientChannelConnector(wrapper, managementSubscriptionSocket, eventLoop); - } - - private final FragmentAssembler fragmentAssembler = - new FragmentAssembler( - new FragmentHandler() { - private final MessageHeaderDecoder messageHeaderDecoder = new MessageHeaderDecoder(); - private final AckConnectDecoder ackConnectDecoder = new AckConnectDecoder(); - - @Override - public void onFragment(DirectBuffer buffer, int offset, int length, Header header) { - messageHeaderDecoder.wrap(buffer, offset); - - // Do not change the order or remove fields - final int actingBlockLength = messageHeaderDecoder.blockLength(); - final int templateId = messageHeaderDecoder.templateId(); - final int schemaId = messageHeaderDecoder.schemaId(); - final int actingVersion = messageHeaderDecoder.version(); - - if (templateId == AckConnectDecoder.TEMPLATE_ID) { - logger.debug("client received an ack message on session id {}", header.sessionId()); - offset += messageHeaderDecoder.encodedLength(); - ackConnectDecoder.wrap(buffer, offset, actingBlockLength, actingVersion); - long channelId = ackConnectDecoder.channelId(); - int serverSessionId = ackConnectDecoder.serverSessionId(); - - logger.debug( - "client received ack message for channel id {} and server session id {}", - channelId, - serverSessionId); - - IntConsumer intConsumer = serverSessionIdConsumerMap.remove(channelId); - - if (intConsumer != null) { - intConsumer.accept(serverSessionId); - } else { - throw new IllegalStateException("no channel found for channel id " + channelId); - } - } else { - throw new IllegalStateException("received unknown template id " + templateId); - } - } - }); - - private int poll() { - int poll; - try { - poll = managementSubscription.poll(fragmentAssembler, 4096); - } finally { - if (running) { - boolean execute = eventLoop.execute(this::poll); - if (!execute) { - running = false; - throw new IllegalStateException("unable to keep polling, eventLoop rejection"); - } - } - } - - return poll; - } - - @Override - public Mono apply(AeronClientConfig aeronClientConfig) { - return MonoSource.wrap( - subscriber -> { - subscriber.onSubscribe(Operators.emptySubscription()); - final long channelId = CHANNEL_ID_COUNTER.get(); - try { - - logger.debug("Creating new client channel with id {}", channelId); - final Publication destination = - aeronWrapper.addPublication( - aeronClientConfig.sendSocketAddress.getChannel(), - aeronClientConfig.sendStreamId); - int destinationStreamId = destination.streamId(); - - logger.debug( - "Client created publication to {}, on stream id {}, and session id {}", - aeronClientConfig.sendSocketAddress, - aeronClientConfig.sendStreamId, - destination.sessionId()); - - final Subscription source = - clientSubscriptions.computeIfAbsent( - aeronClientConfig.receiveSocketAddress, - address -> { - Subscription subscription = - aeronWrapper.addSubscription( - aeronClientConfig.receiveSocketAddress.getChannel(), - aeronClientConfig.receiveStreamId); - logger.debug( - "Client created subscription to {}, on stream id {}", - aeronClientConfig.receiveSocketAddress, - aeronClientConfig.receiveStreamId); - return subscription; - }); - - IntConsumer sessionIdConsumer = - sessionId -> { - try { - AeronChannel aeronChannel = - new AeronChannel( - "client", destination, source, aeronClientConfig.eventLoop, sessionId); - logger.debug( - "created client AeronChannel for destination {}, source {}, destination stream id {}, source stream id {}, client session id, and server session id {}", - aeronClientConfig.sendSocketAddress, - aeronClientConfig.receiveSocketAddress, - destination.streamId(), - source.streamId(), - destination.sessionId(), - sessionId); - subscriber.onNext(aeronChannel); - subscriber.onComplete(); - } catch (Throwable t) { - subscriber.onError(t); - } - }; - - serverSessionIdConsumerMap.putIfAbsent(channelId, sessionIdConsumer); - - aeronWrapper.unavailableImageHandlers( - image -> { - if (destinationStreamId == image.sessionId()) { - clientSubscriptions.remove(channelId); - return true; - } else { - return false; - } - }); - - Publication managementPublication = - aeronWrapper.addPublication( - aeronClientConfig.sendSocketAddress.getChannel(), - Constants.SERVER_MANAGEMENT_STREAM_ID); - logger.debug( - "Client created management publication to channel {}, stream id {}", - managementPublication.channel(), - managementPublication.streamId()); - - DirectBuffer buffer = - encodeConnectMessage(channelId, aeronClientConfig, destination.sessionId()); - long offer; - do { - offer = managementPublication.offer(buffer); - if (offer == Publication.CLOSED) { - subscriber.onError(new NotConnectedException()); - } - } while (offer < 0); - logger.debug("Client sent create message to {}", managementPublication.channel()); - - } catch (Throwable t) { - logger.error("Error creating a channel to {}", aeronClientConfig); - clientSubscriptions.remove(channelId); - subscriber.onError(t); - } - }); - } - - public DirectBuffer encodeConnectMessage( - long channelId, AeronClientConfig config, int clientSessionId) { - final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4096); - final UnsafeBuffer directBuffer = new UnsafeBuffer(byteBuffer); - int bufferOffset = 0; - - MessageHeaderEncoder messageHeaderEncoder = new MessageHeaderEncoder(); - - // Do not channel the order - messageHeaderEncoder - .wrap(directBuffer, bufferOffset) - .blockLength(ConnectEncoder.BLOCK_LENGTH) - .templateId(ConnectEncoder.TEMPLATE_ID) - .schemaId(ConnectEncoder.SCHEMA_ID) - .version(ConnectEncoder.SCHEMA_VERSION); - - bufferOffset += messageHeaderEncoder.encodedLength(); - - ConnectEncoder connectEncoder = new ConnectEncoder(); - - // Do not change the order - connectEncoder - .wrap(directBuffer, bufferOffset) - .channelId(channelId) - .receivingChannel(config.receiveSocketAddress.getChannel()) - .receivingStreamId(config.receiveStreamId) - .sendingChannel(config.sendSocketAddress.getChannel()) - .sendingStreamId(config.sendStreamId) - .clientSessionId(clientSessionId) - .clientManagementChannel(managementSubscription.channel()); - - return directBuffer; - } - - public static class AeronClientConfig implements ReactiveStreamsRemote.ClientChannelConfig { - private final AeronSocketAddress receiveSocketAddress; - private final AeronSocketAddress sendSocketAddress; - private final int receiveStreamId; - private final int sendStreamId; - private final EventLoop eventLoop; - - private AeronClientConfig( - AeronSocketAddress receiveSocketAddress, - AeronSocketAddress sendSocketAddress, - int receiveStreamId, - int sendStreamId, - EventLoop eventLoop) { - this.receiveSocketAddress = receiveSocketAddress; - this.sendSocketAddress = sendSocketAddress; - this.receiveStreamId = receiveStreamId; - this.sendStreamId = sendStreamId; - this.eventLoop = eventLoop; - } - - /** - * Creates client a new {@code AeronClientConfig} for a {@link AeronChannel} - * - * @param receiveSocketAddress the address the channels receives data on - * @param sendSocketAddress the address the channel sends data too - * @return new {@code AeronClientConfig} - */ - public static AeronClientConfig create( - AeronSocketAddress receiveSocketAddress, - AeronSocketAddress sendSocketAddress, - int receiveStreamId, - int sendStreamId, - EventLoop eventLoop) { - return new AeronClientConfig( - receiveSocketAddress, sendSocketAddress, receiveStreamId, sendStreamId, eventLoop); - } - - @Override - public String toString() { - return "AeronClientConfig{" - + "receiveSocketAddress=" - + receiveSocketAddress - + ", sendSocketAddress=" - + sendSocketAddress - + ", receiveStreamId=" - + receiveStreamId - + ", sendStreamId=" - + sendStreamId - + ", eventLoop=" - + eventLoop - + '}'; - } - } - - @Override - public void close() throws Exception { - running = false; - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronInSubscriber.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronInSubscriber.java deleted file mode 100644 index 8eef85f2f..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronInSubscriber.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.aeron.internal.reactivestreams; - -import io.aeron.Publication; -import io.aeron.logbuffer.BufferClaim; -import io.rsocket.aeron.internal.Constants; -import io.rsocket.aeron.internal.NotConnectedException; -import org.agrona.DirectBuffer; -import org.agrona.MutableDirectBuffer; -import org.agrona.concurrent.OneToOneConcurrentArrayQueue; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** */ -public class AeronInSubscriber implements Subscriber { - private static final Logger logger = LoggerFactory.getLogger(AeronInSubscriber.class); - private static final ThreadLocal bufferClaims = - ThreadLocal.withInitial(BufferClaim::new); - private static final int BUFFER_SIZE = 128; - private static final int REFILL = BUFFER_SIZE / 3; - - private static final OneToOneConcurrentArrayQueue> - queues = new OneToOneConcurrentArrayQueue<>(BUFFER_SIZE); - - private final OneToOneConcurrentArrayQueue buffers; - private final String name; - private final Publication destination; - - private Subscription subscription; - - private volatile boolean complete; - private volatile boolean erred = false; - - private volatile long requested; - - public AeronInSubscriber(String name, Publication destination) { - this.name = name; - this.destination = destination; - OneToOneConcurrentArrayQueue poll; - synchronized (queues) { - poll = queues.poll(); - } - buffers = poll != null ? poll : new OneToOneConcurrentArrayQueue<>(BUFFER_SIZE); - } - - @Override - public synchronized void onSubscribe(Subscription subscription) { - this.subscription = subscription; - requested = BUFFER_SIZE; - subscription.request(BUFFER_SIZE); - } - - @Override - public void onNext(DirectBuffer buffer) { - if (!erred) { - if (logger.isTraceEnabled()) { - logger.trace( - name - + " sending to destination => " - + destination.channel() - + " and aeron stream " - + destination.streamId() - + " and session id " - + destination.sessionId()); - } - boolean offer; - synchronized (buffers) { - offer = buffers.offer(buffer); - } - if (!offer) { - onError(new IllegalStateException("missing back-pressure")); - } - - tryEmit(); - } - } - - private boolean emitting = false; - private boolean missed = false; - - void tryEmit() { - synchronized (this) { - if (emitting) { - missed = true; - return; - } - } - - emit(); - } - - void emit() { - try { - for (; ; ) { - synchronized (this) { - missed = false; - } - while (!buffers.isEmpty()) { - DirectBuffer buffer = buffers.poll(); - tryClaimOrOffer(buffer); - requested--; - if (requested < REFILL) { - synchronized (buffers) { - if (!complete) { - long diff = BUFFER_SIZE - requested; - requested = BUFFER_SIZE; - subscription.request(diff); - } - } - } - } - - synchronized (this) { - if (!missed) { - emitting = false; - break; - } - } - } - } catch (Throwable t) { - onError(t); - } - - if (complete && buffers.isEmpty()) { - synchronized (queues) { - queues.offer(buffers); - } - } - } - - private void tryClaimOrOffer(DirectBuffer buffer) { - boolean successful = false; - - int capacity = buffer.capacity(); - if (capacity < Constants.AERON_MTU_SIZE) { - BufferClaim bufferClaim = bufferClaims.get(); - - while (!successful) { - long offer = destination.tryClaim(capacity, bufferClaim); - if (offer >= 0) { - try { - final MutableDirectBuffer b = bufferClaim.buffer(); - int offset = bufferClaim.offset(); - b.putBytes(offset, buffer, 0, capacity); - } finally { - bufferClaim.commit(); - successful = true; - } - } else { - if (offer == Publication.CLOSED) { - onError(new NotConnectedException(name)); - } - - successful = false; - } - } - - } else { - while (!successful) { - long offer = destination.offer(buffer); - - if (offer < 0) { - if (offer == Publication.CLOSED) { - onError(new NotConnectedException(name)); - } - } else { - successful = true; - } - } - } - } - - @Override - public synchronized void onError(Throwable t) { - if (!erred) { - erred = true; - subscription.cancel(); - } - - t.printStackTrace(); - } - - @Override - public synchronized void onComplete() { - complete = true; - tryEmit(); - } - - @Override - public String toString() { - return "AeronInSubscriber{" + "name='" + name + '\'' + '}'; - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronOutPublisher.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronOutPublisher.java deleted file mode 100644 index a1a77764b..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronOutPublisher.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.aeron.internal.reactivestreams; - -import io.aeron.ControlledFragmentAssembler; -import io.aeron.logbuffer.ControlledFragmentHandler; -import io.aeron.logbuffer.Header; -import io.rsocket.aeron.internal.EventLoop; -import io.rsocket.aeron.internal.NotConnectedException; -import java.nio.ByteBuffer; -import java.util.Objects; -import java.util.function.IntSupplier; -import org.agrona.DirectBuffer; -import org.agrona.concurrent.UnsafeBuffer; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; - -/** */ -public class AeronOutPublisher extends Flux { - private static final Logger logger = LoggerFactory.getLogger(AeronOutPublisher.class); - private final io.aeron.Subscription source; - private final EventLoop eventLoop; - - private String name; - private volatile long requested; - private volatile long processed; - private Subscriber destination; - private AeronOutProcessorSubscription subscription; - private final int sessionId; - - /** - * Creates a publication for a unique session - * - * @param name publication's name - * @param sessionId sessionId between the source and the remote publication - * @param source Aeron {@code Subscription} publish data from - * @param eventLoop {@link EventLoop} to poll the source with - */ - public AeronOutPublisher( - String name, int sessionId, io.aeron.Subscription source, EventLoop eventLoop) { - this.name = name; - this.source = source; - this.eventLoop = eventLoop; - this.sessionId = sessionId; - } - - @Override - public void subscribe(Subscriber destination) { - Objects.requireNonNull(destination); - synchronized (this) { - if (this.destination != null && subscription.canEmit()) { - throw new IllegalStateException( - "only allows one subscription => channel " - + source.channel() - + " and stream id => " - + source.streamId()); - } - this.destination = destination; - } - - this.subscription = new AeronOutProcessorSubscription(destination); - destination.onSubscribe(subscription); - } - - void onError(Throwable t) { - subscription.erred = true; - if (destination != null) { - destination.onError(t); - } - } - - void cancel() { - if (subscription != null) { - subscription.cancel(); - } - } - - @Override - public String toString() { - return "AeronOutPublisher{" + "name='" + name + '\'' + '}'; - } - - private class AeronOutProcessorSubscription implements Subscription { - private volatile boolean erred = false; - private volatile boolean cancelled = false; - private final Subscriber destination; - private final ControlledFragmentAssembler assembler; - - public AeronOutProcessorSubscription(Subscriber destination) { - this.destination = destination; - this.assembler = new ControlledFragmentAssembler(this::onFragment, 4096); - } - - boolean emitting = false; - boolean missed = false; - - @Override - public void request(long n) { - if (n < 0) { - onError(new IllegalStateException("n must be greater than zero")); - } - - synchronized (AeronOutPublisher.this) { - long r; - if (requested != Long.MAX_VALUE && n > 0) { - r = requested + n; - requested = r < 0 ? Long.MAX_VALUE : r; - } - } - - tryEmit(); - } - - // allocate this once - final IntSupplier supplier = this::emit; - - void tryEmit() { - synchronized (AeronOutPublisher.this) { - if (emitting) { - missed = true; - return; - } - emitting = true; - eventLoop.execute(supplier); - } - } - - ControlledFragmentHandler.Action onFragment( - DirectBuffer buffer, int offset, int length, Header header) { - if (sessionId != header.sessionId()) { - if (source.imageBySessionId(header.sessionId()) == null) { - return ControlledFragmentHandler.Action.CONTINUE; - } - - return ControlledFragmentHandler.Action.ABORT; - } - - try { - ByteBuffer bytes = ByteBuffer.allocate(length); - buffer.getBytes(offset, bytes, length); - - if (canEmit()) { - destination.onNext(new UnsafeBuffer(bytes)); - } - } catch (Throwable t) { - onError(t); - } - - return ControlledFragmentHandler.Action.COMMIT; - } - - int emit() { - int emitted = 0; - for (; ; ) { - synchronized (AeronOutPublisher.this) { - missed = false; - } - - try { - if (source.isClosed()) { - onError(new NotConnectedException(name)); - return 0; - } - - while (processed < requested) { - - int poll = source.controlledPoll(assembler, 4096); - - if (poll < 1) { - break; - } else { - emitted++; - processed++; - } - } - - synchronized (AeronOutPublisher.this) { - emitting = false; - break; - } - - } catch (Throwable t) { - onError(t); - } - } - - if (canEmit()) { - tryEmit(); - } - - return emitted; - } - - @Override - public void cancel() { - cancelled = true; - } - - private boolean canEmit() { - return !cancelled && !erred; - } - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronSocketAddress.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronSocketAddress.java deleted file mode 100644 index 801d1eb1a..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronSocketAddress.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.aeron.internal.reactivestreams; - -import java.net.SocketAddress; - -/** SocketAddress that represents an Aeron Channel */ -public class AeronSocketAddress extends SocketAddress { - private static final String FORMAT = "%s?endpoint=%s:%d"; - private final String protocol; - private final String host; - private final int port; - private final String channel; - - private AeronSocketAddress(String protocol, String host, int port) { - this.protocol = protocol; - this.host = host; - this.port = port; - this.channel = String.format(FORMAT, protocol, host, port); - } - - public static AeronSocketAddress create(String protocol, String host, int port) { - return new AeronSocketAddress(protocol, host, port); - } - - public String getProtocol() { - return protocol; - } - - public String getHost() { - return host; - } - - public int getPort() { - return port; - } - - public String getChannel() { - return channel; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - AeronSocketAddress that = (AeronSocketAddress) o; - - return channel != null ? channel.equals(that.channel) : that.channel == null; - } - - @Override - public int hashCode() { - return channel != null ? channel.hashCode() : 0; - } - - @Override - public String toString() { - return "AeronSocketAddress{" - + "protocol='" - + protocol - + '\'' - + ", host='" - + host - + '\'' - + ", port=" - + port - + ", channel='" - + channel - + '\'' - + '}'; - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/ReactiveStreamsRemote.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/ReactiveStreamsRemote.java deleted file mode 100644 index 1d8d89546..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/ReactiveStreamsRemote.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.aeron.internal.reactivestreams; - -import io.rsocket.Closeable; -import java.net.SocketAddress; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; -import java.util.function.Function; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** Interfaces to define a ReactiveStream over a remote channel */ -public interface ReactiveStreamsRemote { - interface Channel { - Mono send(Flux in); - - default Mono send(T t) { - return send(Flux.just(t)); - } - - Flux receive(); - - boolean isActive(); - } - - interface ClientChannelConnector> - extends Function> {} - - interface ClientChannelConfig {} - - interface ChannelConsumer> extends Consumer {} - - abstract class ChannelServer> { - protected final C channelConsumer; - - public ChannelServer(C channelConsumer) { - this.channelConsumer = channelConsumer; - } - - public abstract StartedServer start(); - } - - interface StartedServer extends Closeable { - /** - * Address for this server. - * - * @return Address for this server. - */ - SocketAddress getServerAddress(); - - /** - * Port for this server. - * - * @return Port for this server. - */ - int getServerPort(); - - /** - * Blocks till this server shutsdown. - * - *

This does not shutdown the server. - */ - void awaitShutdown(); - - /** - * Blocks till this server shutsdown till the passed duration. - * - *

This does not shutdown the server. - */ - void awaitShutdown(long duration, TimeUnit durationUnit); - - /** Initiates the shutdown of this server. */ - void shutdown(); - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/AckConnectDecoder.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/AckConnectDecoder.java deleted file mode 100644 index d3691a63d..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/AckConnectDecoder.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* Generated SBE (Simple Binary Encoding) message codec */ -package io.rsocket.aeron.internal.reactivestreams.messages; - -import org.agrona.DirectBuffer; - -@javax.annotation.Generated( - value = {"io.rsocket.aeron.internal.reactivestreams.messages.AckConnectDecoder"} -) -@SuppressWarnings("all") -public class AckConnectDecoder { - public static final int BLOCK_LENGTH = 12; - public static final int TEMPLATE_ID = 2; - public static final int SCHEMA_ID = 1; - public static final int SCHEMA_VERSION = 0; - - private final AckConnectDecoder parentMessage = this; - private DirectBuffer buffer; - protected int offset; - protected int limit; - protected int actingBlockLength; - protected int actingVersion; - - public int sbeBlockLength() { - return BLOCK_LENGTH; - } - - public int sbeTemplateId() { - return TEMPLATE_ID; - } - - public int sbeSchemaId() { - return SCHEMA_ID; - } - - public int sbeSchemaVersion() { - return SCHEMA_VERSION; - } - - public String sbeSemanticType() { - return ""; - } - - public int offset() { - return offset; - } - - public AckConnectDecoder wrap( - final DirectBuffer buffer, - final int offset, - final int actingBlockLength, - final int actingVersion) { - this.buffer = buffer; - this.offset = offset; - this.actingBlockLength = actingBlockLength; - this.actingVersion = actingVersion; - limit(offset + actingBlockLength); - - return this; - } - - public int encodedLength() { - return limit - offset; - } - - public int limit() { - return limit; - } - - public void limit(final int limit) { - this.limit = limit; - } - - public static int channelIdId() { - return 1; - } - - public static String channelIdMetaAttribute(final MetaAttribute metaAttribute) { - switch (metaAttribute) { - case EPOCH: - return "unix"; - case TIME_UNIT: - return "nanosecond"; - case SEMANTIC_TYPE: - return ""; - } - - return ""; - } - - public static long channelIdNullValue() { - return -9223372036854775808L; - } - - public static long channelIdMinValue() { - return -9223372036854775807L; - } - - public static long channelIdMaxValue() { - return 9223372036854775807L; - } - - public long channelId() { - return buffer.getLong(offset + 0, java.nio.ByteOrder.LITTLE_ENDIAN); - } - - public static int serverSessionIdId() { - return 2; - } - - public static String serverSessionIdMetaAttribute(final MetaAttribute metaAttribute) { - switch (metaAttribute) { - case EPOCH: - return "unix"; - case TIME_UNIT: - return "nanosecond"; - case SEMANTIC_TYPE: - return ""; - } - - return ""; - } - - public static int serverSessionIdNullValue() { - return -2147483648; - } - - public static int serverSessionIdMinValue() { - return -2147483647; - } - - public static int serverSessionIdMaxValue() { - return 2147483647; - } - - public int serverSessionId() { - return buffer.getInt(offset + 8, java.nio.ByteOrder.LITTLE_ENDIAN); - } - - public String toString() { - return appendTo(new StringBuilder(100)).toString(); - } - - public StringBuilder appendTo(final StringBuilder builder) { - final int originalLimit = limit(); - limit(offset + actingBlockLength); - builder.append("[AckConnect](sbeTemplateId="); - builder.append(TEMPLATE_ID); - builder.append("|sbeSchemaId="); - builder.append(SCHEMA_ID); - builder.append("|sbeSchemaVersion="); - if (actingVersion != SCHEMA_VERSION) { - builder.append(actingVersion); - builder.append('/'); - } - builder.append(SCHEMA_VERSION); - builder.append("|sbeBlockLength="); - if (actingBlockLength != BLOCK_LENGTH) { - builder.append(actingBlockLength); - builder.append('/'); - } - builder.append(BLOCK_LENGTH); - builder.append("):"); - //Token{signal=BEGIN_FIELD, name='channelId', description='The AeronChannel id', id=1, version=0, encodedLength=0, offset=0, componentTokenCount=3, encoding=Encoding{presence=REQUIRED, primitiveType=null, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - //Token{signal=ENCODING, name='int64', description='The AeronChannel id', id=-1, version=0, encodedLength=8, offset=0, componentTokenCount=1, encoding=Encoding{presence=REQUIRED, primitiveType=INT64, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - builder.append("channelId="); - builder.append(channelId()); - builder.append('|'); - //Token{signal=BEGIN_FIELD, name='serverSessionId', description='The session id for the server publication', id=2, version=0, encodedLength=0, offset=8, componentTokenCount=3, encoding=Encoding{presence=REQUIRED, primitiveType=null, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - //Token{signal=ENCODING, name='int32', description='The session id for the server publication', id=-1, version=0, encodedLength=4, offset=8, componentTokenCount=1, encoding=Encoding{presence=REQUIRED, primitiveType=INT32, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - builder.append("serverSessionId="); - builder.append(serverSessionId()); - - limit(originalLimit); - - return builder; - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/AckConnectEncoder.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/AckConnectEncoder.java deleted file mode 100644 index 613ce03d4..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/AckConnectEncoder.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* Generated SBE (Simple Binary Encoding) message codec */ -package io.rsocket.aeron.internal.reactivestreams.messages; - -import org.agrona.MutableDirectBuffer; - -@javax.annotation.Generated( - value = {"io.rsocket.aeron.internal.reactivestreams.messages.AckConnectEncoder"} -) -@SuppressWarnings("all") -public class AckConnectEncoder { - public static final int BLOCK_LENGTH = 12; - public static final int TEMPLATE_ID = 2; - public static final int SCHEMA_ID = 1; - public static final int SCHEMA_VERSION = 0; - - private final AckConnectEncoder parentMessage = this; - private MutableDirectBuffer buffer; - protected int offset; - protected int limit; - protected int actingBlockLength; - protected int actingVersion; - - public int sbeBlockLength() { - return BLOCK_LENGTH; - } - - public int sbeTemplateId() { - return TEMPLATE_ID; - } - - public int sbeSchemaId() { - return SCHEMA_ID; - } - - public int sbeSchemaVersion() { - return SCHEMA_VERSION; - } - - public String sbeSemanticType() { - return ""; - } - - public int offset() { - return offset; - } - - public AckConnectEncoder wrap(final MutableDirectBuffer buffer, final int offset) { - this.buffer = buffer; - this.offset = offset; - limit(offset + BLOCK_LENGTH); - - return this; - } - - public int encodedLength() { - return limit - offset; - } - - public int limit() { - return limit; - } - - public void limit(final int limit) { - this.limit = limit; - } - - public static long channelIdNullValue() { - return -9223372036854775808L; - } - - public static long channelIdMinValue() { - return -9223372036854775807L; - } - - public static long channelIdMaxValue() { - return 9223372036854775807L; - } - - public AckConnectEncoder channelId(final long value) { - buffer.putLong(offset + 0, value, java.nio.ByteOrder.LITTLE_ENDIAN); - return this; - } - - public static int serverSessionIdNullValue() { - return -2147483648; - } - - public static int serverSessionIdMinValue() { - return -2147483647; - } - - public static int serverSessionIdMaxValue() { - return 2147483647; - } - - public AckConnectEncoder serverSessionId(final int value) { - buffer.putInt(offset + 8, value, java.nio.ByteOrder.LITTLE_ENDIAN); - return this; - } - - public String toString() { - return appendTo(new StringBuilder(100)).toString(); - } - - public StringBuilder appendTo(final StringBuilder builder) { - AckConnectDecoder writer = new AckConnectDecoder(); - writer.wrap(buffer, offset, BLOCK_LENGTH, SCHEMA_VERSION); - - return writer.appendTo(builder); - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/ConnectDecoder.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/ConnectDecoder.java deleted file mode 100644 index 423e8bd0d..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/ConnectDecoder.java +++ /dev/null @@ -1,504 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* Generated SBE (Simple Binary Encoding) message codec */ -package io.rsocket.aeron.internal.reactivestreams.messages; - -import org.agrona.DirectBuffer; -import org.agrona.MutableDirectBuffer; - -@javax.annotation.Generated( - value = {"io.rsocket.aeron.internal.reactivestreams.messages.ConnectDecoder"} -) -@SuppressWarnings("all") -public class ConnectDecoder { - public static final int BLOCK_LENGTH = 20; - public static final int TEMPLATE_ID = 1; - public static final int SCHEMA_ID = 1; - public static final int SCHEMA_VERSION = 0; - - private final ConnectDecoder parentMessage = this; - private DirectBuffer buffer; - protected int offset; - protected int limit; - protected int actingBlockLength; - protected int actingVersion; - - public int sbeBlockLength() { - return BLOCK_LENGTH; - } - - public int sbeTemplateId() { - return TEMPLATE_ID; - } - - public int sbeSchemaId() { - return SCHEMA_ID; - } - - public int sbeSchemaVersion() { - return SCHEMA_VERSION; - } - - public String sbeSemanticType() { - return ""; - } - - public int offset() { - return offset; - } - - public ConnectDecoder wrap( - final DirectBuffer buffer, - final int offset, - final int actingBlockLength, - final int actingVersion) { - this.buffer = buffer; - this.offset = offset; - this.actingBlockLength = actingBlockLength; - this.actingVersion = actingVersion; - limit(offset + actingBlockLength); - - return this; - } - - public int encodedLength() { - return limit - offset; - } - - public int limit() { - return limit; - } - - public void limit(final int limit) { - this.limit = limit; - } - - public static int channelIdId() { - return 1; - } - - public static String channelIdMetaAttribute(final MetaAttribute metaAttribute) { - switch (metaAttribute) { - case EPOCH: - return "unix"; - case TIME_UNIT: - return "nanosecond"; - case SEMANTIC_TYPE: - return ""; - } - - return ""; - } - - public static long channelIdNullValue() { - return -9223372036854775808L; - } - - public static long channelIdMinValue() { - return -9223372036854775807L; - } - - public static long channelIdMaxValue() { - return 9223372036854775807L; - } - - public long channelId() { - return buffer.getLong(offset + 0, java.nio.ByteOrder.LITTLE_ENDIAN); - } - - public static int sendingStreamIdId() { - return 2; - } - - public static String sendingStreamIdMetaAttribute(final MetaAttribute metaAttribute) { - switch (metaAttribute) { - case EPOCH: - return "unix"; - case TIME_UNIT: - return "nanosecond"; - case SEMANTIC_TYPE: - return ""; - } - - return ""; - } - - public static int sendingStreamIdNullValue() { - return -2147483648; - } - - public static int sendingStreamIdMinValue() { - return -2147483647; - } - - public static int sendingStreamIdMaxValue() { - return 2147483647; - } - - public int sendingStreamId() { - return buffer.getInt(offset + 8, java.nio.ByteOrder.LITTLE_ENDIAN); - } - - public static int receivingStreamIdId() { - return 3; - } - - public static String receivingStreamIdMetaAttribute(final MetaAttribute metaAttribute) { - switch (metaAttribute) { - case EPOCH: - return "unix"; - case TIME_UNIT: - return "nanosecond"; - case SEMANTIC_TYPE: - return ""; - } - - return ""; - } - - public static int receivingStreamIdNullValue() { - return -2147483648; - } - - public static int receivingStreamIdMinValue() { - return -2147483647; - } - - public static int receivingStreamIdMaxValue() { - return 2147483647; - } - - public int receivingStreamId() { - return buffer.getInt(offset + 12, java.nio.ByteOrder.LITTLE_ENDIAN); - } - - public static int clientSessionIdId() { - return 4; - } - - public static String clientSessionIdMetaAttribute(final MetaAttribute metaAttribute) { - switch (metaAttribute) { - case EPOCH: - return "unix"; - case TIME_UNIT: - return "nanosecond"; - case SEMANTIC_TYPE: - return ""; - } - - return ""; - } - - public static int clientSessionIdNullValue() { - return -2147483648; - } - - public static int clientSessionIdMinValue() { - return -2147483647; - } - - public static int clientSessionIdMaxValue() { - return 2147483647; - } - - public int clientSessionId() { - return buffer.getInt(offset + 16, java.nio.ByteOrder.LITTLE_ENDIAN); - } - - public static int sendingChannelId() { - return 5; - } - - public static String sendingChannelCharacterEncoding() { - return "UTF-8"; - } - - public static String sendingChannelMetaAttribute(final MetaAttribute metaAttribute) { - switch (metaAttribute) { - case EPOCH: - return "unix"; - case TIME_UNIT: - return "nanosecond"; - case SEMANTIC_TYPE: - return ""; - } - - return ""; - } - - public static int sendingChannelHeaderLength() { - return 4; - } - - public int sendingChannelLength() { - final int limit = parentMessage.limit(); - return (int) (buffer.getInt(limit, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF_FFFFL); - } - - public int getSendingChannel( - final MutableDirectBuffer dst, final int dstOffset, final int length) { - final int headerLength = 4; - final int limit = parentMessage.limit(); - final int dataLength = - (int) (buffer.getInt(limit, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF_FFFFL); - final int bytesCopied = Math.min(length, dataLength); - parentMessage.limit(limit + headerLength + dataLength); - buffer.getBytes(limit + headerLength, dst, dstOffset, bytesCopied); - - return bytesCopied; - } - - public int getSendingChannel(final byte[] dst, final int dstOffset, final int length) { - final int headerLength = 4; - final int limit = parentMessage.limit(); - final int dataLength = - (int) (buffer.getInt(limit, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF_FFFFL); - final int bytesCopied = Math.min(length, dataLength); - parentMessage.limit(limit + headerLength + dataLength); - buffer.getBytes(limit + headerLength, dst, dstOffset, bytesCopied); - - return bytesCopied; - } - - public String sendingChannel() { - final int headerLength = 4; - final int limit = parentMessage.limit(); - final int dataLength = - (int) (buffer.getInt(limit, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF_FFFFL); - parentMessage.limit(limit + headerLength + dataLength); - final byte[] tmp = new byte[dataLength]; - buffer.getBytes(limit + headerLength, tmp, 0, dataLength); - - final String value; - try { - value = new String(tmp, "UTF-8"); - } catch (final java.io.UnsupportedEncodingException ex) { - throw new RuntimeException(ex); - } - - return value; - } - - public static int receivingChannelId() { - return 6; - } - - public static String receivingChannelCharacterEncoding() { - return "UTF-8"; - } - - public static String receivingChannelMetaAttribute(final MetaAttribute metaAttribute) { - switch (metaAttribute) { - case EPOCH: - return "unix"; - case TIME_UNIT: - return "nanosecond"; - case SEMANTIC_TYPE: - return ""; - } - - return ""; - } - - public static int receivingChannelHeaderLength() { - return 4; - } - - public int receivingChannelLength() { - final int limit = parentMessage.limit(); - return (int) (buffer.getInt(limit, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF_FFFFL); - } - - public int getReceivingChannel( - final MutableDirectBuffer dst, final int dstOffset, final int length) { - final int headerLength = 4; - final int limit = parentMessage.limit(); - final int dataLength = - (int) (buffer.getInt(limit, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF_FFFFL); - final int bytesCopied = Math.min(length, dataLength); - parentMessage.limit(limit + headerLength + dataLength); - buffer.getBytes(limit + headerLength, dst, dstOffset, bytesCopied); - - return bytesCopied; - } - - public int getReceivingChannel(final byte[] dst, final int dstOffset, final int length) { - final int headerLength = 4; - final int limit = parentMessage.limit(); - final int dataLength = - (int) (buffer.getInt(limit, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF_FFFFL); - final int bytesCopied = Math.min(length, dataLength); - parentMessage.limit(limit + headerLength + dataLength); - buffer.getBytes(limit + headerLength, dst, dstOffset, bytesCopied); - - return bytesCopied; - } - - public String receivingChannel() { - final int headerLength = 4; - final int limit = parentMessage.limit(); - final int dataLength = - (int) (buffer.getInt(limit, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF_FFFFL); - parentMessage.limit(limit + headerLength + dataLength); - final byte[] tmp = new byte[dataLength]; - buffer.getBytes(limit + headerLength, tmp, 0, dataLength); - - final String value; - try { - value = new String(tmp, "UTF-8"); - } catch (final java.io.UnsupportedEncodingException ex) { - throw new RuntimeException(ex); - } - - return value; - } - - public static int clientManagementChannelId() { - return 6; - } - - public static String clientManagementChannelCharacterEncoding() { - return "UTF-8"; - } - - public static String clientManagementChannelMetaAttribute(final MetaAttribute metaAttribute) { - switch (metaAttribute) { - case EPOCH: - return "unix"; - case TIME_UNIT: - return "nanosecond"; - case SEMANTIC_TYPE: - return ""; - } - - return ""; - } - - public static int clientManagementChannelHeaderLength() { - return 4; - } - - public int clientManagementChannelLength() { - final int limit = parentMessage.limit(); - return (int) (buffer.getInt(limit, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF_FFFFL); - } - - public int getClientManagementChannel( - final MutableDirectBuffer dst, final int dstOffset, final int length) { - final int headerLength = 4; - final int limit = parentMessage.limit(); - final int dataLength = - (int) (buffer.getInt(limit, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF_FFFFL); - final int bytesCopied = Math.min(length, dataLength); - parentMessage.limit(limit + headerLength + dataLength); - buffer.getBytes(limit + headerLength, dst, dstOffset, bytesCopied); - - return bytesCopied; - } - - public int getClientManagementChannel(final byte[] dst, final int dstOffset, final int length) { - final int headerLength = 4; - final int limit = parentMessage.limit(); - final int dataLength = - (int) (buffer.getInt(limit, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF_FFFFL); - final int bytesCopied = Math.min(length, dataLength); - parentMessage.limit(limit + headerLength + dataLength); - buffer.getBytes(limit + headerLength, dst, dstOffset, bytesCopied); - - return bytesCopied; - } - - public String clientManagementChannel() { - final int headerLength = 4; - final int limit = parentMessage.limit(); - final int dataLength = - (int) (buffer.getInt(limit, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF_FFFFL); - parentMessage.limit(limit + headerLength + dataLength); - final byte[] tmp = new byte[dataLength]; - buffer.getBytes(limit + headerLength, tmp, 0, dataLength); - - final String value; - try { - value = new String(tmp, "UTF-8"); - } catch (final java.io.UnsupportedEncodingException ex) { - throw new RuntimeException(ex); - } - - return value; - } - - public String toString() { - return appendTo(new StringBuilder(100)).toString(); - } - - public StringBuilder appendTo(final StringBuilder builder) { - final int originalLimit = limit(); - limit(offset + actingBlockLength); - builder.append("[Connect](sbeTemplateId="); - builder.append(TEMPLATE_ID); - builder.append("|sbeSchemaId="); - builder.append(SCHEMA_ID); - builder.append("|sbeSchemaVersion="); - if (actingVersion != SCHEMA_VERSION) { - builder.append(actingVersion); - builder.append('/'); - } - builder.append(SCHEMA_VERSION); - builder.append("|sbeBlockLength="); - if (actingBlockLength != BLOCK_LENGTH) { - builder.append(actingBlockLength); - builder.append('/'); - } - builder.append(BLOCK_LENGTH); - builder.append("):"); - //Token{signal=BEGIN_FIELD, name='channelId', description='The AeronChannel id', id=1, version=0, encodedLength=0, offset=0, componentTokenCount=3, encoding=Encoding{presence=REQUIRED, primitiveType=null, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - //Token{signal=ENCODING, name='int64', description='The AeronChannel id', id=-1, version=0, encodedLength=8, offset=0, componentTokenCount=1, encoding=Encoding{presence=REQUIRED, primitiveType=INT64, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - builder.append("channelId="); - builder.append(channelId()); - builder.append('|'); - //Token{signal=BEGIN_FIELD, name='sendingStreamId', description='The stream id the connecting client will send traffic on', id=2, version=0, encodedLength=0, offset=8, componentTokenCount=3, encoding=Encoding{presence=REQUIRED, primitiveType=null, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - //Token{signal=ENCODING, name='int32', description='The stream id the connecting client will send traffic on', id=-1, version=0, encodedLength=4, offset=8, componentTokenCount=1, encoding=Encoding{presence=REQUIRED, primitiveType=INT32, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - builder.append("sendingStreamId="); - builder.append(sendingStreamId()); - builder.append('|'); - //Token{signal=BEGIN_FIELD, name='receivingStreamId', description='The stream id the connecting client will receive data on', id=3, version=0, encodedLength=0, offset=12, componentTokenCount=3, encoding=Encoding{presence=REQUIRED, primitiveType=null, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - //Token{signal=ENCODING, name='int32', description='The stream id the connecting client will receive data on', id=-1, version=0, encodedLength=4, offset=12, componentTokenCount=1, encoding=Encoding{presence=REQUIRED, primitiveType=INT32, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - builder.append("receivingStreamId="); - builder.append(receivingStreamId()); - builder.append('|'); - //Token{signal=BEGIN_FIELD, name='clientSessionId', description='The session id for the client publication', id=4, version=0, encodedLength=0, offset=16, componentTokenCount=3, encoding=Encoding{presence=REQUIRED, primitiveType=null, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - //Token{signal=ENCODING, name='int32', description='The session id for the client publication', id=-1, version=0, encodedLength=4, offset=16, componentTokenCount=1, encoding=Encoding{presence=REQUIRED, primitiveType=INT32, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - builder.append("clientSessionId="); - builder.append(clientSessionId()); - builder.append('|'); - //Token{signal=BEGIN_VAR_DATA, name='sendingChannel', description='The Aeron channel the client will send data on', id=5, version=0, encodedLength=0, offset=20, componentTokenCount=6, encoding=Encoding{presence=REQUIRED, primitiveType=null, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - builder.append("sendingChannel="); - builder.append(sendingChannel()); - builder.append('|'); - //Token{signal=BEGIN_VAR_DATA, name='receivingChannel', description='The Aeron channel the client will receive data on', id=6, version=0, encodedLength=0, offset=-1, componentTokenCount=6, encoding=Encoding{presence=REQUIRED, primitiveType=null, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - builder.append("receivingChannel="); - builder.append(receivingChannel()); - builder.append('|'); - //Token{signal=BEGIN_VAR_DATA, name='clientManagementChannel', description='The channel the client listens for management data on', id=6, version=0, encodedLength=0, offset=-1, componentTokenCount=6, encoding=Encoding{presence=REQUIRED, primitiveType=null, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='null', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - builder.append("clientManagementChannel="); - builder.append(clientManagementChannel()); - - limit(originalLimit); - - return builder; - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/ConnectEncoder.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/ConnectEncoder.java deleted file mode 100644 index 3ff9739fa..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/ConnectEncoder.java +++ /dev/null @@ -1,392 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* Generated SBE (Simple Binary Encoding) message codec */ -package io.rsocket.aeron.internal.reactivestreams.messages; - -import org.agrona.DirectBuffer; -import org.agrona.MutableDirectBuffer; - -@javax.annotation.Generated( - value = {"io.rsocket.aeron.internal.reactivestreams.messages.ConnectEncoder"} -) -@SuppressWarnings("all") -public class ConnectEncoder { - public static final int BLOCK_LENGTH = 20; - public static final int TEMPLATE_ID = 1; - public static final int SCHEMA_ID = 1; - public static final int SCHEMA_VERSION = 0; - - private final ConnectEncoder parentMessage = this; - private MutableDirectBuffer buffer; - protected int offset; - protected int limit; - protected int actingBlockLength; - protected int actingVersion; - - public int sbeBlockLength() { - return BLOCK_LENGTH; - } - - public int sbeTemplateId() { - return TEMPLATE_ID; - } - - public int sbeSchemaId() { - return SCHEMA_ID; - } - - public int sbeSchemaVersion() { - return SCHEMA_VERSION; - } - - public String sbeSemanticType() { - return ""; - } - - public int offset() { - return offset; - } - - public ConnectEncoder wrap(final MutableDirectBuffer buffer, final int offset) { - this.buffer = buffer; - this.offset = offset; - limit(offset + BLOCK_LENGTH); - - return this; - } - - public int encodedLength() { - return limit - offset; - } - - public int limit() { - return limit; - } - - public void limit(final int limit) { - this.limit = limit; - } - - public static long channelIdNullValue() { - return -9223372036854775808L; - } - - public static long channelIdMinValue() { - return -9223372036854775807L; - } - - public static long channelIdMaxValue() { - return 9223372036854775807L; - } - - public ConnectEncoder channelId(final long value) { - buffer.putLong(offset + 0, value, java.nio.ByteOrder.LITTLE_ENDIAN); - return this; - } - - public static int sendingStreamIdNullValue() { - return -2147483648; - } - - public static int sendingStreamIdMinValue() { - return -2147483647; - } - - public static int sendingStreamIdMaxValue() { - return 2147483647; - } - - public ConnectEncoder sendingStreamId(final int value) { - buffer.putInt(offset + 8, value, java.nio.ByteOrder.LITTLE_ENDIAN); - return this; - } - - public static int receivingStreamIdNullValue() { - return -2147483648; - } - - public static int receivingStreamIdMinValue() { - return -2147483647; - } - - public static int receivingStreamIdMaxValue() { - return 2147483647; - } - - public ConnectEncoder receivingStreamId(final int value) { - buffer.putInt(offset + 12, value, java.nio.ByteOrder.LITTLE_ENDIAN); - return this; - } - - public static int clientSessionIdNullValue() { - return -2147483648; - } - - public static int clientSessionIdMinValue() { - return -2147483647; - } - - public static int clientSessionIdMaxValue() { - return 2147483647; - } - - public ConnectEncoder clientSessionId(final int value) { - buffer.putInt(offset + 16, value, java.nio.ByteOrder.LITTLE_ENDIAN); - return this; - } - - public static int sendingChannelId() { - return 5; - } - - public static String sendingChannelCharacterEncoding() { - return "UTF-8"; - } - - public static String sendingChannelMetaAttribute(final MetaAttribute metaAttribute) { - switch (metaAttribute) { - case EPOCH: - return "unix"; - case TIME_UNIT: - return "nanosecond"; - case SEMANTIC_TYPE: - return ""; - } - - return ""; - } - - public static int sendingChannelHeaderLength() { - return 4; - } - - public ConnectEncoder putSendingChannel( - final DirectBuffer src, final int srcOffset, final int length) { - if (length > 1073741824) { - throw new IllegalArgumentException("length > max value for type: " + length); - } - - final int headerLength = 4; - final int limit = parentMessage.limit(); - parentMessage.limit(limit + headerLength + length); - buffer.putInt(limit, (int) length, java.nio.ByteOrder.LITTLE_ENDIAN); - buffer.putBytes(limit + headerLength, src, srcOffset, length); - - return this; - } - - public ConnectEncoder putSendingChannel(final byte[] src, final int srcOffset, final int length) { - if (length > 1073741824) { - throw new IllegalArgumentException("length > max value for type: " + length); - } - - final int headerLength = 4; - final int limit = parentMessage.limit(); - parentMessage.limit(limit + headerLength + length); - buffer.putInt(limit, (int) length, java.nio.ByteOrder.LITTLE_ENDIAN); - buffer.putBytes(limit + headerLength, src, srcOffset, length); - - return this; - } - - public ConnectEncoder sendingChannel(final String value) { - final byte[] bytes; - try { - bytes = value.getBytes("UTF-8"); - } catch (final java.io.UnsupportedEncodingException ex) { - throw new RuntimeException(ex); - } - - final int length = bytes.length; - if (length > 1073741824) { - throw new IllegalArgumentException("length > max value for type: " + length); - } - - final int headerLength = 4; - final int limit = parentMessage.limit(); - parentMessage.limit(limit + headerLength + length); - buffer.putInt(limit, (int) length, java.nio.ByteOrder.LITTLE_ENDIAN); - buffer.putBytes(limit + headerLength, bytes, 0, length); - - return this; - } - - public static int receivingChannelId() { - return 6; - } - - public static String receivingChannelCharacterEncoding() { - return "UTF-8"; - } - - public static String receivingChannelMetaAttribute(final MetaAttribute metaAttribute) { - switch (metaAttribute) { - case EPOCH: - return "unix"; - case TIME_UNIT: - return "nanosecond"; - case SEMANTIC_TYPE: - return ""; - } - - return ""; - } - - public static int receivingChannelHeaderLength() { - return 4; - } - - public ConnectEncoder putReceivingChannel( - final DirectBuffer src, final int srcOffset, final int length) { - if (length > 1073741824) { - throw new IllegalArgumentException("length > max value for type: " + length); - } - - final int headerLength = 4; - final int limit = parentMessage.limit(); - parentMessage.limit(limit + headerLength + length); - buffer.putInt(limit, (int) length, java.nio.ByteOrder.LITTLE_ENDIAN); - buffer.putBytes(limit + headerLength, src, srcOffset, length); - - return this; - } - - public ConnectEncoder putReceivingChannel( - final byte[] src, final int srcOffset, final int length) { - if (length > 1073741824) { - throw new IllegalArgumentException("length > max value for type: " + length); - } - - final int headerLength = 4; - final int limit = parentMessage.limit(); - parentMessage.limit(limit + headerLength + length); - buffer.putInt(limit, (int) length, java.nio.ByteOrder.LITTLE_ENDIAN); - buffer.putBytes(limit + headerLength, src, srcOffset, length); - - return this; - } - - public ConnectEncoder receivingChannel(final String value) { - final byte[] bytes; - try { - bytes = value.getBytes("UTF-8"); - } catch (final java.io.UnsupportedEncodingException ex) { - throw new RuntimeException(ex); - } - - final int length = bytes.length; - if (length > 1073741824) { - throw new IllegalArgumentException("length > max value for type: " + length); - } - - final int headerLength = 4; - final int limit = parentMessage.limit(); - parentMessage.limit(limit + headerLength + length); - buffer.putInt(limit, (int) length, java.nio.ByteOrder.LITTLE_ENDIAN); - buffer.putBytes(limit + headerLength, bytes, 0, length); - - return this; - } - - public static int clientManagementChannelId() { - return 6; - } - - public static String clientManagementChannelCharacterEncoding() { - return "UTF-8"; - } - - public static String clientManagementChannelMetaAttribute(final MetaAttribute metaAttribute) { - switch (metaAttribute) { - case EPOCH: - return "unix"; - case TIME_UNIT: - return "nanosecond"; - case SEMANTIC_TYPE: - return ""; - } - - return ""; - } - - public static int clientManagementChannelHeaderLength() { - return 4; - } - - public ConnectEncoder putClientManagementChannel( - final DirectBuffer src, final int srcOffset, final int length) { - if (length > 1073741824) { - throw new IllegalArgumentException("length > max value for type: " + length); - } - - final int headerLength = 4; - final int limit = parentMessage.limit(); - parentMessage.limit(limit + headerLength + length); - buffer.putInt(limit, (int) length, java.nio.ByteOrder.LITTLE_ENDIAN); - buffer.putBytes(limit + headerLength, src, srcOffset, length); - - return this; - } - - public ConnectEncoder putClientManagementChannel( - final byte[] src, final int srcOffset, final int length) { - if (length > 1073741824) { - throw new IllegalArgumentException("length > max value for type: " + length); - } - - final int headerLength = 4; - final int limit = parentMessage.limit(); - parentMessage.limit(limit + headerLength + length); - buffer.putInt(limit, (int) length, java.nio.ByteOrder.LITTLE_ENDIAN); - buffer.putBytes(limit + headerLength, src, srcOffset, length); - - return this; - } - - public ConnectEncoder clientManagementChannel(final String value) { - final byte[] bytes; - try { - bytes = value.getBytes("UTF-8"); - } catch (final java.io.UnsupportedEncodingException ex) { - throw new RuntimeException(ex); - } - - final int length = bytes.length; - if (length > 1073741824) { - throw new IllegalArgumentException("length > max value for type: " + length); - } - - final int headerLength = 4; - final int limit = parentMessage.limit(); - parentMessage.limit(limit + headerLength + length); - buffer.putInt(limit, (int) length, java.nio.ByteOrder.LITTLE_ENDIAN); - buffer.putBytes(limit + headerLength, bytes, 0, length); - - return this; - } - - public String toString() { - return appendTo(new StringBuilder(100)).toString(); - } - - public StringBuilder appendTo(final StringBuilder builder) { - ConnectDecoder writer = new ConnectDecoder(); - writer.wrap(buffer, offset, BLOCK_LENGTH, SCHEMA_VERSION); - - return writer.appendTo(builder); - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/MessageHeaderDecoder.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/MessageHeaderDecoder.java deleted file mode 100644 index d3c9cc986..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/MessageHeaderDecoder.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* Generated SBE (Simple Binary Encoding) message codec */ -package io.rsocket.aeron.internal.reactivestreams.messages; - -import org.agrona.DirectBuffer; - -@javax.annotation.Generated( - value = {"io.rsocket.aeron.internal.reactivestreams.messages.MessageHeaderDecoder"} -) -@SuppressWarnings("all") -public class MessageHeaderDecoder { - public static final int ENCODED_LENGTH = 8; - private DirectBuffer buffer; - private int offset; - - public MessageHeaderDecoder wrap(final DirectBuffer buffer, final int offset) { - this.buffer = buffer; - this.offset = offset; - - return this; - } - - public int encodedLength() { - return ENCODED_LENGTH; - } - - public static int blockLengthNullValue() { - return 65535; - } - - public static int blockLengthMinValue() { - return 0; - } - - public static int blockLengthMaxValue() { - return 65534; - } - - public int blockLength() { - return (buffer.getShort(offset + 0, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF); - } - - public static int templateIdNullValue() { - return 65535; - } - - public static int templateIdMinValue() { - return 0; - } - - public static int templateIdMaxValue() { - return 65534; - } - - public int templateId() { - return (buffer.getShort(offset + 2, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF); - } - - public static int schemaIdNullValue() { - return 65535; - } - - public static int schemaIdMinValue() { - return 0; - } - - public static int schemaIdMaxValue() { - return 65534; - } - - public int schemaId() { - return (buffer.getShort(offset + 4, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF); - } - - public static int versionNullValue() { - return 65535; - } - - public static int versionMinValue() { - return 0; - } - - public static int versionMaxValue() { - return 65534; - } - - public int version() { - return (buffer.getShort(offset + 6, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF); - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/MessageHeaderEncoder.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/MessageHeaderEncoder.java deleted file mode 100644 index c646a8a73..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/MessageHeaderEncoder.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* Generated SBE (Simple Binary Encoding) message codec */ -package io.rsocket.aeron.internal.reactivestreams.messages; - -import org.agrona.MutableDirectBuffer; - -@javax.annotation.Generated( - value = {"io.rsocket.aeron.internal.reactivestreams.messages.MessageHeaderEncoder"} -) -@SuppressWarnings("all") -public class MessageHeaderEncoder { - public static final int ENCODED_LENGTH = 8; - private MutableDirectBuffer buffer; - private int offset; - - public MessageHeaderEncoder wrap(final MutableDirectBuffer buffer, final int offset) { - this.buffer = buffer; - this.offset = offset; - - return this; - } - - public int encodedLength() { - return ENCODED_LENGTH; - } - - public static int blockLengthNullValue() { - return 65535; - } - - public static int blockLengthMinValue() { - return 0; - } - - public static int blockLengthMaxValue() { - return 65534; - } - - public MessageHeaderEncoder blockLength(final int value) { - buffer.putShort(offset + 0, (short) value, java.nio.ByteOrder.LITTLE_ENDIAN); - return this; - } - - public static int templateIdNullValue() { - return 65535; - } - - public static int templateIdMinValue() { - return 0; - } - - public static int templateIdMaxValue() { - return 65534; - } - - public MessageHeaderEncoder templateId(final int value) { - buffer.putShort(offset + 2, (short) value, java.nio.ByteOrder.LITTLE_ENDIAN); - return this; - } - - public static int schemaIdNullValue() { - return 65535; - } - - public static int schemaIdMinValue() { - return 0; - } - - public static int schemaIdMaxValue() { - return 65534; - } - - public MessageHeaderEncoder schemaId(final int value) { - buffer.putShort(offset + 4, (short) value, java.nio.ByteOrder.LITTLE_ENDIAN); - return this; - } - - public static int versionNullValue() { - return 65535; - } - - public static int versionMinValue() { - return 0; - } - - public static int versionMaxValue() { - return 65534; - } - - public MessageHeaderEncoder version(final int value) { - buffer.putShort(offset + 6, (short) value, java.nio.ByteOrder.LITTLE_ENDIAN); - return this; - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/MetaAttribute.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/MetaAttribute.java deleted file mode 100644 index bdf3eb985..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/MetaAttribute.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* Generated SBE (Simple Binary Encoding) message codec */ -package io.rsocket.aeron.internal.reactivestreams.messages; - -@javax.annotation.Generated( - value = {"io.rsocket.aeron.internal.reactivestreams.messages.MetaAttribute"} -) -public enum MetaAttribute { - EPOCH, - TIME_UNIT, - SEMANTIC_TYPE -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/VarDataEncodingDecoder.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/VarDataEncodingDecoder.java deleted file mode 100644 index 7381bd45a..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/VarDataEncodingDecoder.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* Generated SBE (Simple Binary Encoding) message codec */ -package io.rsocket.aeron.internal.reactivestreams.messages; - -import org.agrona.DirectBuffer; - -@javax.annotation.Generated( - value = {"io.rsocket.aeron.internal.reactivestreams.messages.VarDataEncodingDecoder"} -) -@SuppressWarnings("all") -public class VarDataEncodingDecoder { - public static final int ENCODED_LENGTH = -1; - private DirectBuffer buffer; - private int offset; - - public VarDataEncodingDecoder wrap(final DirectBuffer buffer, final int offset) { - this.buffer = buffer; - this.offset = offset; - - return this; - } - - public int encodedLength() { - return ENCODED_LENGTH; - } - - public static long lengthNullValue() { - return 4294967294L; - } - - public static long lengthMinValue() { - return 0L; - } - - public static long lengthMaxValue() { - return 1073741824L; - } - - public long length() { - return (buffer.getInt(offset + 0, java.nio.ByteOrder.LITTLE_ENDIAN) & 0xFFFF_FFFFL); - } - - public static short varDataNullValue() { - return (short) 255; - } - - public static short varDataMinValue() { - return (short) 0; - } - - public static short varDataMaxValue() { - return (short) 254; - } - - public String toString() { - return appendTo(new StringBuilder(100)).toString(); - } - - public StringBuilder appendTo(final StringBuilder builder) { - builder.append('('); - //Token{signal=ENCODING, name='length', description='The channel the client listens for management data on', id=-1, version=0, encodedLength=4, offset=0, componentTokenCount=1, encoding=Encoding{presence=REQUIRED, primitiveType=UINT32, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=1073741824, nullValue=null, constValue=null, characterEncoding='UTF-8', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - builder.append("length="); - builder.append(length()); - builder.append('|'); - //Token{signal=ENCODING, name='varData', description='The channel the client listens for management data on', id=-1, version=0, encodedLength=-1, offset=4, componentTokenCount=1, encoding=Encoding{presence=REQUIRED, primitiveType=UINT8, byteOrder=LITTLE_ENDIAN, minValue=null, maxValue=null, nullValue=null, constValue=null, characterEncoding='UTF-8', epoch='unix', timeUnit=nanosecond, semanticType='null'}} - builder.append(')'); - - return builder; - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/VarDataEncodingEncoder.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/VarDataEncodingEncoder.java deleted file mode 100644 index 6c100fff4..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/VarDataEncodingEncoder.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* Generated SBE (Simple Binary Encoding) message codec */ -package io.rsocket.aeron.internal.reactivestreams.messages; - -import org.agrona.MutableDirectBuffer; - -@javax.annotation.Generated( - value = {"io.rsocket.aeron.internal.reactivestreams.messages.VarDataEncodingEncoder"} -) -@SuppressWarnings("all") -public class VarDataEncodingEncoder { - public static final int ENCODED_LENGTH = -1; - private MutableDirectBuffer buffer; - private int offset; - - public VarDataEncodingEncoder wrap(final MutableDirectBuffer buffer, final int offset) { - this.buffer = buffer; - this.offset = offset; - - return this; - } - - public int encodedLength() { - return ENCODED_LENGTH; - } - - public static long lengthNullValue() { - return 4294967294L; - } - - public static long lengthMinValue() { - return 0L; - } - - public static long lengthMaxValue() { - return 1073741824L; - } - - public VarDataEncodingEncoder length(final long value) { - buffer.putInt(offset + 0, (int) value, java.nio.ByteOrder.LITTLE_ENDIAN); - return this; - } - - public static short varDataNullValue() { - return (short) 255; - } - - public static short varDataMinValue() { - return (short) 0; - } - - public static short varDataMaxValue() { - return (short) 254; - } - - public String toString() { - return appendTo(new StringBuilder(100)).toString(); - } - - public StringBuilder appendTo(final StringBuilder builder) { - VarDataEncodingDecoder writer = new VarDataEncodingDecoder(); - writer.wrap(buffer, offset); - - return writer.appendTo(builder); - } -} diff --git a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/server/AeronServerTransport.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/server/AeronServerTransport.java deleted file mode 100644 index 4deee205d..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/server/AeronServerTransport.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.aeron.server; - -import io.rsocket.Closeable; -import io.rsocket.DuplexConnection; -import io.rsocket.aeron.AeronDuplexConnection; -import io.rsocket.aeron.internal.AeronWrapper; -import io.rsocket.aeron.internal.EventLoop; -import io.rsocket.aeron.internal.reactivestreams.AeronChannelServer; -import io.rsocket.aeron.internal.reactivestreams.AeronSocketAddress; -import io.rsocket.transport.ServerTransport; -import reactor.core.publisher.Mono; - -/** */ -public class AeronServerTransport implements ServerTransport { - private final AeronWrapper aeronWrapper; - private final AeronSocketAddress managementSubscriptionSocket; - private final EventLoop eventLoop; - - private AeronChannelServer aeronChannelServer; - - public AeronServerTransport( - AeronWrapper aeronWrapper, - AeronSocketAddress managementSubscriptionSocket, - EventLoop eventLoop) { - this.aeronWrapper = aeronWrapper; - this.managementSubscriptionSocket = managementSubscriptionSocket; - this.eventLoop = eventLoop; - } - - @Override - public Mono start(ConnectionAcceptor acceptor) { - synchronized (this) { - if (aeronChannelServer != null) { - throw new IllegalStateException("server already ready started"); - } - - aeronChannelServer = - AeronChannelServer.create( - aeronChannel -> { - DuplexConnection connection = new AeronDuplexConnection("server", aeronChannel); - acceptor.apply(connection).subscribe(); - }, - aeronWrapper, - managementSubscriptionSocket, - eventLoop); - } - - return Mono.just(aeronChannelServer.start()); - } -} diff --git a/rsocket-transport-aeron/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler b/rsocket-transport-aeron/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler deleted file mode 100644 index bec0f3f46..000000000 --- a/rsocket-transport-aeron/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2016 Netflix, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -io.rsocket.aeron.AeronUriHandler diff --git a/rsocket-transport-aeron/src/main/resources/aeron-channel-schema.xml b/rsocket-transport-aeron/src/main/resources/aeron-channel-schema.xml deleted file mode 100644 index 2e9818812..000000000 --- a/rsocket-transport-aeron/src/main/resources/aeron-channel-schema.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/AeronClientSetupRule.java b/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/AeronClientSetupRule.java deleted file mode 100644 index 023f4c171..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/AeronClientSetupRule.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.aeron; - -import io.rsocket.Closeable; -import io.rsocket.aeron.client.AeronClientTransport; -import io.rsocket.aeron.internal.*; -import io.rsocket.aeron.internal.reactivestreams.AeronClientChannelConnector; -import io.rsocket.aeron.internal.reactivestreams.AeronSocketAddress; -import io.rsocket.aeron.server.AeronServerTransport; -import io.rsocket.test.ClientSetupRule; - -class AeronClientSetupRule extends ClientSetupRule { - - public static final AeronSocketAddress ADDRESS = - AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - - static { - MediaDriverHolder.getInstance(); - AeronWrapper aeronWrapper = new DefaultAeronWrapper(); - - EventLoop serverEventLoop = new SingleThreadedEventLoop("server"); - server = new AeronServerTransport(aeronWrapper, ADDRESS, serverEventLoop); - - // Create Client Connector - EventLoop clientEventLoop = new SingleThreadedEventLoop("client"); - - AeronClientChannelConnector.AeronClientConfig config = - AeronClientChannelConnector.AeronClientConfig.create( - ADDRESS, - ADDRESS, - Constants.CLIENT_STREAM_ID, - Constants.SERVER_STREAM_ID, - clientEventLoop); - - AeronClientChannelConnector connector = - AeronClientChannelConnector.create(aeronWrapper, ADDRESS, clientEventLoop); - - client = new AeronClientTransport(connector, config); - } - - private static final AeronServerTransport server; - private static final AeronClientTransport client; - - AeronClientSetupRule() { - super(() -> ADDRESS, (address, server) -> client, address -> server); - } -} diff --git a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/AeronPing.java b/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/AeronPing.java deleted file mode 100644 index 6fbc1e876..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/AeronPing.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.aeron; - -import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; -import io.rsocket.aeron.client.AeronClientTransport; -import io.rsocket.aeron.internal.*; -import io.rsocket.aeron.internal.reactivestreams.AeronClientChannelConnector; -import io.rsocket.aeron.internal.reactivestreams.AeronSocketAddress; -import io.rsocket.test.PingClient; -import java.time.Duration; -import org.HdrHistogram.Recorder; -import reactor.core.publisher.Mono; - -public final class AeronPing { - - public static void main(String... args) throws Exception { - // Create Client Connector - AeronWrapper aeronWrapper = new DefaultAeronWrapper(); - - AeronSocketAddress clientManagementSocketAddress = - AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - EventLoop clientEventLoop = new SingleThreadedEventLoop("client"); - - AeronSocketAddress receiveAddress = AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - AeronSocketAddress sendAddress = AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - - AeronClientChannelConnector.AeronClientConfig config = - AeronClientChannelConnector.AeronClientConfig.create( - receiveAddress, - sendAddress, - Constants.CLIENT_STREAM_ID, - Constants.SERVER_STREAM_ID, - clientEventLoop); - - AeronClientChannelConnector connector = - AeronClientChannelConnector.create( - aeronWrapper, clientManagementSocketAddress, clientEventLoop); - - AeronClientTransport aeronTransportClient = new AeronClientTransport(connector, config); - - Mono client = RSocketFactory.connect().transport(aeronTransportClient).start(); - PingClient pingClient = new PingClient(client); - Recorder recorder = pingClient.startTracker(Duration.ofSeconds(1)); - final int count = 1_000_000_000; - pingClient - .startPingPong(count, recorder) - .doOnTerminate(() -> System.out.println("Sent " + count + " messages.")) - .blockLast(); - - System.exit(0); - } -} diff --git a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/AeronPongServer.java b/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/AeronPongServer.java deleted file mode 100644 index 40be15f99..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/AeronPongServer.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.aeron; - -import io.aeron.driver.MediaDriver; -import io.aeron.driver.ThreadingMode; -import io.rsocket.RSocketFactory; -import io.rsocket.aeron.internal.AeronWrapper; -import io.rsocket.aeron.internal.DefaultAeronWrapper; -import io.rsocket.aeron.internal.EventLoop; -import io.rsocket.aeron.internal.SingleThreadedEventLoop; -import io.rsocket.aeron.internal.reactivestreams.AeronSocketAddress; -import io.rsocket.aeron.server.AeronServerTransport; -import io.rsocket.test.PingHandler; - -public final class AeronPongServer { - static { - final io.aeron.driver.MediaDriver.Context ctx = - new io.aeron.driver.MediaDriver.Context() - .threadingMode(ThreadingMode.SHARED_NETWORK) - .dirsDeleteOnStart(true); - MediaDriver.launch(ctx); - } - - public static void main(String... args) throws Exception { - MediaDriverHolder.getInstance(); - AeronWrapper aeronWrapper = new DefaultAeronWrapper(); - - AeronSocketAddress serverManagementSocketAddress = - AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - EventLoop serverEventLoop = new SingleThreadedEventLoop("server"); - AeronServerTransport server = - new AeronServerTransport(aeronWrapper, serverManagementSocketAddress, serverEventLoop); - - AeronServerTransport transport = - new AeronServerTransport(aeronWrapper, serverManagementSocketAddress, serverEventLoop); - RSocketFactory.receive().acceptor(new PingHandler()).transport(transport).start(); - } -} diff --git a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/ClientServerTest.java b/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/ClientServerTest.java deleted file mode 100644 index f87fe2afe..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/ClientServerTest.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.aeron; - -import static org.junit.Assert.assertEquals; - -import io.rsocket.Payload; -import io.rsocket.test.ClientSetupRule; -import io.rsocket.util.PayloadImpl; -import java.nio.charset.StandardCharsets; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import reactor.core.publisher.Flux; - -@Ignore -public class ClientServerTest { - - @Rule public final ClientSetupRule setup = new AeronClientSetupRule(); - - @Test(timeout = 10000) - public void testFireNForget10() { - long outputCount = - Flux.range(1, 10) - .flatMap(i -> setup.getRSocket().fireAndForget(new PayloadImpl("hello", "metadata"))) - .doOnError(Throwable::printStackTrace) - .count() - .block(); - - assertEquals(0, outputCount); - } - - @Test(timeout = 10000) - public void testPushMetadata10() { - long outputCount = - Flux.range(1, 10) - .flatMap(i -> setup.getRSocket().metadataPush(new PayloadImpl("", "metadata"))) - .doOnError(Throwable::printStackTrace) - .count() - .block(); - - assertEquals(0, outputCount); - } - - @Test(timeout = 5000000) - public void testRequestResponse1() { - long outputCount = - Flux.range(1, 1) - .flatMap( - i -> - setup - .getRSocket() - .requestResponse(new PayloadImpl("hello", "metadata")) - .map( - payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString())) - .doOnError(Throwable::printStackTrace) - .count() - .block(); - - assertEquals(1, outputCount); - } - - @Test(timeout = 2000) - public void testRequestResponse10() { - long outputCount = - Flux.range(1, 10) - .flatMap( - i -> - setup - .getRSocket() - .requestResponse(new PayloadImpl("hello", "metadata")) - .map( - payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString())) - .doOnError(Throwable::printStackTrace) - .count() - .block(); - - assertEquals(10, outputCount); - } - - @Test(timeout = 2000) - public void testRequestResponse100() { - long outputCount = - Flux.range(1, 100) - .flatMap( - i -> - setup - .getRSocket() - .requestResponse(new PayloadImpl("hello", "metadata")) - .map( - payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString())) - .doOnError(Throwable::printStackTrace) - .count() - .block(); - - assertEquals(100, outputCount); - } - - @Test(timeout = 5000) - public void testRequestResponse10_000() { - long outputCount = - Flux.range(1, 10_000) - .flatMap( - i -> - setup - .getRSocket() - .requestResponse(new PayloadImpl("hello", "metadata")) - .map( - payload -> StandardCharsets.UTF_8.decode(payload.getData()).toString())) - .doOnError(Throwable::printStackTrace) - .count() - .block(); - - assertEquals(10_000, outputCount); - } - - @Test(timeout = 10000) - public void testRequestStream() { - Flux publisher = - setup.getRSocket().requestStream(new PayloadImpl("hello", "metadata")); - - long count = publisher.take(5).count().block(); - - assertEquals(5, count); - } -} diff --git a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/MediaDriverHolder.java b/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/MediaDriverHolder.java deleted file mode 100644 index 6501975da..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/MediaDriverHolder.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.aeron; - -import io.aeron.driver.MediaDriver; -import io.aeron.driver.ThreadingMode; -import java.util.concurrent.TimeUnit; -import org.agrona.concurrent.SleepingIdleStrategy; - -public class MediaDriverHolder { - private static final MediaDriverHolder INSTANCE = new MediaDriverHolder(); - - static { - final io.aeron.driver.MediaDriver.Context ctx = - new io.aeron.driver.MediaDriver.Context() - .threadingMode(ThreadingMode.SHARED) - .dirsDeleteOnStart(true) - .conductorIdleStrategy(new SleepingIdleStrategy(TimeUnit.MILLISECONDS.toNanos(1))) - .receiverIdleStrategy(new SleepingIdleStrategy(TimeUnit.MILLISECONDS.toNanos(1))) - .senderIdleStrategy(new SleepingIdleStrategy(TimeUnit.MILLISECONDS.toNanos(1))); - - ctx.driverTimeoutMs(TimeUnit.MINUTES.toMillis(10)); - MediaDriver.launch(ctx); - } - - private MediaDriverHolder() {} - - public static MediaDriverHolder getInstance() { - return INSTANCE; - } -} diff --git a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelPing.java b/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelPing.java deleted file mode 100644 index 4b64269be..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelPing.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.aeron.internal.reactivestreams; - -import io.rsocket.aeron.internal.AeronWrapper; -import io.rsocket.aeron.internal.DefaultAeronWrapper; -import io.rsocket.aeron.internal.SingleThreadedEventLoop; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -import org.HdrHistogram.Recorder; -import org.agrona.concurrent.UnsafeBuffer; -import reactor.core.publisher.Flux; - -/** */ -public final class AeronChannelPing { - public static void main(String... args) throws Exception { - int count = 1_000_000_000; - final Recorder histogram = new Recorder(Long.MAX_VALUE, 3); - Executors.newSingleThreadScheduledExecutor() - .scheduleAtFixedRate( - () -> { - System.out.println("---- PING/ PONG HISTO ----"); - histogram - .getIntervalHistogram() - .outputPercentileDistribution(System.out, 5, 1000.0, false); - System.out.println("---- PING/ PONG HISTO ----"); - }, - 1, - 1, - TimeUnit.SECONDS); - - AeronWrapper wrapper = new DefaultAeronWrapper(); - AeronSocketAddress managementSocketAddress = - AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - SingleThreadedEventLoop eventLoop = new SingleThreadedEventLoop("client"); - AeronClientChannelConnector connector = - AeronClientChannelConnector.create(wrapper, managementSocketAddress, eventLoop); - - AeronSocketAddress receiveAddress = AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - AeronSocketAddress sendAddress = AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - - AeronClientChannelConnector.AeronClientConfig config = - AeronClientChannelConnector.AeronClientConfig.create( - receiveAddress, sendAddress, 1, 2, eventLoop); - - AeronChannel channel = connector.apply(config).block(); - - AtomicLong lastUpdate = new AtomicLong(System.nanoTime()); - channel - .receive() - .doOnNext( - b -> { - synchronized (wrapper) { - int anInt = b.getInt(0); - if (anInt % 1_000 == 0) { - long diff = System.nanoTime() - lastUpdate.get(); - histogram.recordValue(diff); - lastUpdate.set(System.nanoTime()); - } - } - }) - .doOnError(throwable -> throwable.printStackTrace()) - .subscribe(); - - byte[] b = new byte[1024]; - Flux.range(0, count) - .flatMap( - i -> { - UnsafeBuffer buffer = new UnsafeBuffer(b); - buffer.putInt(0, i); - return channel.send(buffer); - }, - 8) - .last(null) - .block(); - } -} diff --git a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelPongServer.java b/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelPongServer.java deleted file mode 100644 index 1fd877d1a..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelPongServer.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.aeron.internal.reactivestreams; - -import io.rsocket.aeron.MediaDriverHolder; -import io.rsocket.aeron.internal.AeronWrapper; -import io.rsocket.aeron.internal.DefaultAeronWrapper; -import io.rsocket.aeron.internal.SingleThreadedEventLoop; -import org.agrona.DirectBuffer; -import reactor.core.publisher.Flux; - -/** */ -public class AeronChannelPongServer { - public static void main(String... args) { - MediaDriverHolder.getInstance(); - AeronWrapper wrapper = new DefaultAeronWrapper(); - AeronSocketAddress managementSubscription = - AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - SingleThreadedEventLoop eventLoop = new SingleThreadedEventLoop("server"); - - AeronChannelServer.AeronChannelConsumer consumer = - new AeronChannelServer.AeronChannelConsumer() { - @Override - public void accept(AeronChannel aeronChannel) { - Flux receive = aeronChannel.receive(); - //.doOnNext(b -> System.out.println("server got => " + b.getInt(0))); - - aeronChannel - .send(receive) - .doOnError(throwable -> throwable.printStackTrace()) - .subscribe(); - } - }; - - AeronChannelServer server = - AeronChannelServer.create(consumer, wrapper, managementSubscription, eventLoop); - AeronChannelServer.AeronChannelStartedServer start = server.start(); - start.awaitShutdown(); - } -} diff --git a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelTest.java b/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelTest.java deleted file mode 100644 index 16ca90f02..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelTest.java +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.aeron.internal.reactivestreams; - -import io.aeron.Aeron; -import io.aeron.Publication; -import io.aeron.Subscription; -import io.rsocket.aeron.MediaDriverHolder; -import io.rsocket.aeron.internal.Constants; -import io.rsocket.aeron.internal.EventLoop; -import io.rsocket.aeron.internal.SingleThreadedEventLoop; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ThreadLocalRandom; -import org.agrona.BitUtil; -import org.agrona.LangUtil; -import org.agrona.concurrent.UnsafeBuffer; -import org.junit.Ignore; -import org.junit.Test; -import reactor.core.publisher.Flux; - -/** */ -@Ignore("travis does not like me") -public class AeronChannelTest { - static { - // System.setProperty("aeron.publication.linger.timeout", String.valueOf(50_000_000_000L)); - // System.setProperty("aeron.client.liveness.timeout", String.valueOf(50_000_000_000L)); - MediaDriverHolder.getInstance(); - } - - @Test - @Ignore - public void testPing() { - - int count = 5_000_000; - CountDownLatch countDownLatch = new CountDownLatch(count); - - CountDownLatch sync = new CountDownLatch(2); - Aeron.Context ctx = new Aeron.Context(); - - // ctx.publicationConnectionTimeout(TimeUnit.MINUTES.toNanos(5)); - - ctx.availableImageHandler( - image -> { - System.out.println( - "name image subscription => " - + image.subscription().channel() - + " streamid => " - + image.subscription().streamId() - + " registrationid => " - + image.subscription().registrationId()); - sync.countDown(); - }); - - ctx.unavailableImageHandler( - image -> { - System.out.println( - "=== unavailable image name image subscription => " - + image.subscription().channel() - + " streamid => " - + image.subscription().streamId() - + " registrationid => " - + image.subscription().registrationId()); - }); - /*ctx.errorHandler(t -> { - /* StringWriter writer = new StringWriter(); - PrintWriter w = new PrintWriter(writer); - t.printStackTrace(w); - - w.flush();* - - // System.out.println("\nGOT AERON ERROR => \n [" + writer.toString() + "]\n\n"); - });*/ - - ctx.driverTimeoutMs(Integer.MAX_VALUE); - Aeron aeron = Aeron.connect(ctx); - /* - Subscription serverSubscription = aeron.addSubscription("aeron:ipc", Constants.SERVER_STREAM_ID); - Publication serverPublication = aeron.addPublication("aeron:ipc", Constants.CLIENT_STREAM_ID); - - Subscription clientSubscription = aeron.addSubscription("aeron:ipc", Constants.CLIENT_STREAM_ID); - Publication clientPublication = aeron.addPublication("aeron:ipc", Constants.SERVER_STREAM_ID); - */ - - Subscription serverSubscription = - aeron.addSubscription("aeron:udp?endpoint=localhost:39791", Constants.SERVER_STREAM_ID); - System.out.println( - "serverSubscription registration id => " + serverSubscription.registrationId()); - - Publication serverPublication = - aeron.addPublication("aeron:udp?endpoint=localhost:39790", Constants.CLIENT_STREAM_ID); - - Subscription clientSubscription = - aeron.addSubscription("aeron:udp?endpoint=localhost:39790", Constants.CLIENT_STREAM_ID); - - System.out.println( - "clientSubscription registration id => " + clientSubscription.registrationId()); - Publication clientPublication = - aeron.addPublication("aeron:udp?endpoint=localhost:39791", Constants.SERVER_STREAM_ID); - - try { - sync.await(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - EventLoop serverLoop = new SingleThreadedEventLoop("server"); - - AeronOutPublisher publisher = - new AeronOutPublisher( - "server", clientPublication.sessionId(), serverSubscription, serverLoop); - publisher - .doOnNext(i -> countDownLatch.countDown()) - .doOnError(Throwable::printStackTrace) - .subscribe(); - - AeronInSubscriber aeronInSubscriber = new AeronInSubscriber("client", clientPublication); - - Flux unsafeBufferObservable = - Flux.range(1, count) - //.doOnNext(i -> LockSupport.parkNanos(TimeUnit.MICROSECONDS.toNanos(50))) - // .doOnNext(i -> System.out.println(Thread.currentThread() + " => client sending => " + i)) - .map( - i -> { - UnsafeBuffer buffer = new UnsafeBuffer(new byte[BitUtil.SIZE_OF_INT]); - buffer.putInt(0, i); - return buffer; - }) - // .doOnRequest(l -> System.out.println("Client reuqested => " + l)) - .doOnError(Throwable::printStackTrace) - .doOnComplete(() -> System.out.println("Im done")); - - unsafeBufferObservable.subscribe(aeronInSubscriber); - - try { - countDownLatch.await(); - } catch (InterruptedException e) { - LangUtil.rethrowUnchecked(e); - } - System.out.println("HERE!!!!"); - } - - @Test(timeout = 2_000) - public void testPingPong_10() { - pingPong(10); - } - - @Test(timeout = 2_000) - public void testPingPong_100() { - pingPong(100); - } - - @Test(timeout = 5_000) - public void testPingPong_300() { - pingPong(300); - } - - @Test(timeout = 5_000) - public void testPingPong_1_000() { - pingPong(1_000); - } - - @Test(timeout = 15_000) - public void testPingPong_10_000() { - pingPong(10_000); - } - - @Ignore - @Test(timeout = 5_000) - public void testPingPong_100_000() { - pingPong(100_000); - } - - @Ignore - @Test(timeout = 15_000) - public void testPingPong_1_000_000() { - pingPong(1_000_000); - } - - @Test(timeout = 50_000) - @Ignore - public void testPingPong_10_000_000() { - pingPong(10_000_000); - } - - @Test - @Ignore - public void testPingPongAlot() { - pingPong(100_000_000); - } - - private void pingPong(int count) { - - CountDownLatch sync = new CountDownLatch(2); - Aeron.Context ctx = new Aeron.Context(); - ctx.availableImageHandler( - image -> { - System.out.println( - "name image subscription => " - + image.subscription().channel() - + " streamid => " - + image.subscription().streamId() - + " registrationid => " - + image.subscription().registrationId()); - sync.countDown(); - }); - - ctx.unavailableImageHandler( - image -> { - System.out.println( - "=== unavailable image name image subscription => " - + image.subscription().channel() - + " streamid => " - + image.subscription().streamId() - + " registrationid => " - + image.subscription().registrationId()); - }); - - /*ctx.errorHandler(t -> { - /* StringWriter writer = new StringWriter(); - PrintWriter w = new PrintWriter(writer); - t.printStackTrace(w); - - w.flush();* - - // System.out.println("\nGOT AERON ERROR => \n [" + writer.toString() + "]\n\n"); - });*/ - - //ctx.driverTimeoutMs(Integer.MAX_VALUE); - Aeron aeron = Aeron.connect(ctx); - - Subscription serverSubscription = - aeron.addSubscription("aeron:ipc", Constants.SERVER_STREAM_ID); - Publication serverPublication = aeron.addPublication("aeron:ipc", Constants.CLIENT_STREAM_ID); - - Subscription clientSubscription = - aeron.addSubscription("aeron:ipc", Constants.CLIENT_STREAM_ID); - Publication clientPublication = aeron.addPublication("aeron:ipc", Constants.SERVER_STREAM_ID); - - /* - Subscription serverSubscription = aeron.addSubscription("udp://localhost:39791", Constants.SERVER_STREAM_ID); - System.out.println("serverSubscription registration id => " + serverSubscription.registrationId()); - - Publication serverPublication = aeron.addPublication("udp://localhost:39790", Constants.CLIENT_STREAM_ID); - - Subscription clientSubscription = aeron.addSubscription("udp://localhost:39790", Constants.CLIENT_STREAM_ID); - - System.out.println("clientSubscription registration id => " + clientSubscription.registrationId()); - Publication clientPublication = aeron.addPublication("udp://localhost:39791", Constants.SERVER_STREAM_ID); - */ - try { - sync.await(); - } catch (InterruptedException e) { - LangUtil.rethrowUnchecked(e); - } - - SingleThreadedEventLoop serverLoop = new SingleThreadedEventLoop("server"); - SingleThreadedEventLoop clientLoop = new SingleThreadedEventLoop("client"); - - AeronChannel serverChannel = - new AeronChannel( - "server", - serverPublication, - serverSubscription, - serverLoop, - clientPublication.sessionId()); - - System.out.println("created server channel"); - - CountDownLatch latch = new CountDownLatch(count); - - serverChannel - .receive() - .flatMap( - f -> { - // latch.countDown(); - //System.out.println("received -> " + f.getInt(0)); - return serverChannel.send(f); - }, - 32) - .doOnError(Throwable::printStackTrace) - .subscribe(); - - AeronChannel clientChannel = - new AeronChannel( - "client", - clientPublication, - clientSubscription, - clientLoop, - serverPublication.sessionId()); - - clientChannel - .receive() - .doOnNext( - l -> { - synchronized (latch) { - latch.countDown(); - if (latch.getCount() % 10_000 == 0) { - System.out.println("mod of client got back -> " + latch.getCount()); - } - // if (latch.getCount() < 10_000) { - // System.out.println("client got back -> " + latch.getCount()); - // } - } - }) - .doOnError(Throwable::printStackTrace) - .subscribe(); - - byte[] bytes = new byte[8]; - ThreadLocalRandom.current().nextBytes(bytes); - - Flux.range(1, count) - //.doOnRequest(l -> System.out.println("requested => " + l)) - .flatMap( - i -> { - //System.out.println("Sending -> " + i); - - //UnsafeBuffer b = new UnsafeBuffer(new byte[BitUtil.SIZE_OF_INT]); - UnsafeBuffer b = new UnsafeBuffer(bytes); - b.putInt(0, i); - - return clientChannel.send(b); - }, - 8) - .doOnError(Throwable::printStackTrace) - .subscribe(); - - try { - latch.await(); - } catch (Exception t) { - LangUtil.rethrowUnchecked(t); - } - } -} diff --git a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronClientServerChannelTest.java b/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronClientServerChannelTest.java deleted file mode 100644 index 1a0dd3bfc..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronClientServerChannelTest.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.aeron.internal.reactivestreams; - -import io.rsocket.aeron.MediaDriverHolder; -import io.rsocket.aeron.internal.AeronWrapper; -import io.rsocket.aeron.internal.DefaultAeronWrapper; -import io.rsocket.aeron.internal.EventLoop; -import io.rsocket.aeron.internal.SingleThreadedEventLoop; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ThreadLocalRandom; -import org.agrona.BitUtil; -import org.agrona.DirectBuffer; -import org.agrona.concurrent.UnsafeBuffer; -import org.junit.Assert; -import org.junit.Ignore; -import org.junit.Test; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** */ -@Ignore("travis does not like me") -public class AeronClientServerChannelTest { - static { - MediaDriverHolder.getInstance(); - } - - @Test(timeout = 5_000) - public void testConnect() throws Exception { - int clientId = ThreadLocalRandom.current().nextInt(0, 1_000); - int serverId = clientId + 1; - - System.out.println("test client stream id => " + clientId); - System.out.println("test server stream id => " + serverId); - - AeronWrapper aeronWrapper = new DefaultAeronWrapper(); - - // Create Client Connector - AeronSocketAddress clientManagementSocketAddress = - AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - EventLoop clientEventLoop = new SingleThreadedEventLoop("client"); - - AeronSocketAddress receiveAddress = AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - AeronSocketAddress sendAddress = AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - - AeronClientChannelConnector.AeronClientConfig config = - AeronClientChannelConnector.AeronClientConfig.create( - receiveAddress, sendAddress, clientId, serverId, clientEventLoop); - - AeronClientChannelConnector connector = - AeronClientChannelConnector.create( - aeronWrapper, clientManagementSocketAddress, clientEventLoop); - - // Create Server - CountDownLatch latch = new CountDownLatch(2); - - AeronChannelServer.AeronChannelConsumer consumer = - (AeronChannel aeronChannel) -> { - Assert.assertNotNull(aeronChannel); - latch.countDown(); - }; - - AeronSocketAddress serverManagementSocketAddress = - AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - EventLoop serverEventLoop = new SingleThreadedEventLoop("server"); - AeronChannelServer aeronChannelServer = - AeronChannelServer.create( - consumer, aeronWrapper, serverManagementSocketAddress, serverEventLoop); - - aeronChannelServer.start(); - - Publisher publisher = connector.apply(config); - Flux.from(publisher) - .doOnNext(Assert::assertNotNull) - .doOnNext(c -> latch.countDown()) - .doOnError( - t -> { - throw new RuntimeException(t); - }) - .subscribe(); - - latch.await(); - } - - @Test(timeout = 5_000) - public void testPingPong() throws Exception { - int clientId = ThreadLocalRandom.current().nextInt(2_000, 3_000); - int serverId = clientId + 1; - - System.out.println("test client stream id => " + clientId); - System.out.println("test server stream id => " + serverId); - - AeronWrapper aeronWrapper = new DefaultAeronWrapper(); - - // Create Client Connector - AeronSocketAddress clientManagementSocketAddress = - AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - EventLoop clientEventLoop = new SingleThreadedEventLoop("client"); - - AeronSocketAddress receiveAddress = AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - AeronSocketAddress sendAddress = AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - - AeronClientChannelConnector.AeronClientConfig config = - AeronClientChannelConnector.AeronClientConfig.create( - receiveAddress, sendAddress, clientId, serverId, clientEventLoop); - - AeronClientChannelConnector connector = - AeronClientChannelConnector.create( - aeronWrapper, clientManagementSocketAddress, clientEventLoop); - - // Create Server - - AeronChannelServer.AeronChannelConsumer consumer = - (AeronChannel aeronChannel) -> { - Assert.assertNotNull(aeronChannel); - - Flux receive = aeronChannel.receive(); - - Flux data = - receive.doOnNext(b -> System.out.println("server received => " + b.getInt(0))); - - aeronChannel.send(data).subscribe(); - }; - - AeronSocketAddress serverManagementSocketAddress = - AeronSocketAddress.create("aeron:udp", "127.0.0.1", 39790); - EventLoop serverEventLoop = new SingleThreadedEventLoop("server"); - AeronChannelServer aeronChannelServer = - AeronChannelServer.create( - consumer, aeronWrapper, serverManagementSocketAddress, serverEventLoop); - - aeronChannelServer.start(); - - Publisher publisher = connector.apply(config); - - int count = 10; - CountDownLatch latch = new CountDownLatch(count); - - Mono.from(publisher) - .flatMap( - aeronChannel -> - Mono.create( - callback -> { - Flux data = - Flux.range(1, count) - .map( - i -> { - byte[] b = new byte[BitUtil.SIZE_OF_INT]; - UnsafeBuffer buffer = new UnsafeBuffer(b); - buffer.putInt(0, i); - return buffer; - }); - - aeronChannel - .receive() - .doOnNext(b -> latch.countDown()) - .doOnNext(callback::success) - .subscribe(); - aeronChannel.send(data).subscribe(); - })) - .subscribe(); - - latch.await(); - } -} diff --git a/rsocket-transport-aeron/src/test/resources/log4j.properties b/rsocket-transport-aeron/src/test/resources/log4j.properties deleted file mode 100644 index 6477d125f..000000000 --- a/rsocket-transport-aeron/src/test/resources/log4j.properties +++ /dev/null @@ -1,33 +0,0 @@ -# -# Copyright 2016 Netflix, Inc. -#

-# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -#

-# http://www.apache.org/licenses/LICENSE-2.0 -#

-# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on -# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -# - - -# -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -log4j.rootLogger=INFO, stdout - -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] (%F:%L) - %m%n \ No newline at end of file diff --git a/rsocket-transport-local/build.gradle b/rsocket-transport-local/build.gradle index 859702f6f..8c855f26c 100644 --- a/rsocket-transport-local/build.gradle +++ b/rsocket-transport-local/build.gradle @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,8 +14,22 @@ * limitations under the License. */ +plugins { + id 'java-library' + id 'maven-publish' + id 'signing' +} + dependencies { - compile project(':rsocket-core') + api project(':rsocket-core') - testCompile project(':rsocket-test') + testImplementation project(':rsocket-test') + testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.junit.jupiter:junit-jupiter-api' + + testRuntimeOnly 'ch.qos.logback:logback-classic' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' } + +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 687a1ebb7..b80fc2337 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,54 +1,91 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; -import io.rsocket.Frame; +import io.rsocket.internal.UnboundedProcessor; import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.local.LocalServerTransport.ServerDuplexConnectionAcceptor; +import io.rsocket.transport.ServerTransport; +import java.util.Objects; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; -import reactor.core.publisher.UnicastProcessor; -public class LocalClientTransport implements ClientTransport { +/** + * An implementation of {@link ClientTransport} that connects to a {@link ServerTransport} in the + * same JVM. + */ +public final class LocalClientTransport implements ClientTransport { + private final String name; - private LocalClientTransport(String name) { + private final ByteBufAllocator allocator; + + private LocalClientTransport(String name, ByteBufAllocator allocator) { this.name = name; + this.allocator = allocator; } + /** + * Creates a new instance. + * + * @param name the name of the {@link ClientTransport} instance to connect to + * @return a new instance + * @throws NullPointerException if {@code name} is {@code null} + */ public static LocalClientTransport create(String name) { - return new LocalClientTransport(name); + Objects.requireNonNull(name, "name must not be null"); + + return create(name, ByteBufAllocator.DEFAULT); + } + + /** + * Creates a new instance. + * + * @param name the name of the {@link ClientTransport} instance to connect to + * @param allocator the allocator used by {@link ClientTransport} instance + * @return a new instance + * @throws NullPointerException if {@code name} is {@code null} + */ + public static LocalClientTransport create(String name, ByteBufAllocator allocator) { + Objects.requireNonNull(name, "name must not be null"); + Objects.requireNonNull(allocator, "allocator must not be null"); + + return new LocalClientTransport(name, allocator); } @Override public Mono connect() { return Mono.defer( () -> { - ServerDuplexConnectionAcceptor server = LocalServerTransport.findServer(name); - if (server != null) { - final UnicastProcessor in = UnicastProcessor.create(); - final UnicastProcessor out = UnicastProcessor.create(); - final MonoProcessor closeNotifier = MonoProcessor.create(); - server.accept(new LocalDuplexConnection(out, in, closeNotifier)); - DuplexConnection client = new LocalDuplexConnection(in, out, closeNotifier); - return Mono.just(client); + ServerTransport.ConnectionAcceptor server = LocalServerTransport.findServer(name); + if (server == null) { + return Mono.error(new IllegalArgumentException("Could not find server: " + name)); } - return Mono.error(new IllegalArgumentException("Could not find server: " + name)); + + UnboundedProcessor in = new UnboundedProcessor<>(); + UnboundedProcessor out = new UnboundedProcessor<>(); + MonoProcessor closeNotifier = MonoProcessor.create(); + + server.apply(new LocalDuplexConnection(allocator, out, in, closeNotifier)).subscribe(); + + return Mono.just( + (DuplexConnection) new LocalDuplexConnection(allocator, in, out, closeNotifier)); }); } } diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java index af251ae4e..afaa14f95 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java @@ -1,73 +1,97 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; -import io.rsocket.Frame; +import java.util.Objects; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; -public class LocalDuplexConnection implements DuplexConnection { - private final Flux in; - private final Subscriber out; - private final MonoProcessor closeNotifier; +/** An implementation of {@link DuplexConnection} that connects inside the same JVM. */ +final class LocalDuplexConnection implements DuplexConnection { - public LocalDuplexConnection( - Flux in, Subscriber out, MonoProcessor closeNotifier) { - this.in = in; - this.out = out; - this.closeNotifier = closeNotifier; + private final ByteBufAllocator allocator; + private final Flux in; + + private final MonoProcessor onClose; + + private final Subscriber out; + + /** + * Creates a new instance. + * + * @param in the inbound {@link ByteBuf}s + * @param out the outbound {@link ByteBuf}s + * @param onClose the closing notifier + * @throws NullPointerException if {@code in}, {@code out}, or {@code onClose} are {@code null} + */ + LocalDuplexConnection( + ByteBufAllocator allocator, + Flux in, + Subscriber out, + MonoProcessor onClose) { + this.allocator = Objects.requireNonNull(allocator, "allocator must not be null"); + this.in = Objects.requireNonNull(in, "in must not be null"); + this.out = Objects.requireNonNull(out, "out must not be null"); + this.onClose = Objects.requireNonNull(onClose, "onClose must not be null"); } @Override - public Mono send(Publisher frames) { - return Flux.from(frames).concatMap(this::sendOne).then(); + public void dispose() { + out.onComplete(); + onClose.onComplete(); } @Override - public Mono sendOne(Frame frame) { - return Mono.fromRunnable(() -> out.onNext(frame)); + public boolean isDisposed() { + return onClose.isDisposed(); } @Override - public Flux receive() { + public Mono onClose() { + return onClose; + } + + @Override + public Flux receive() { return in; } @Override - public Mono close() { - return Mono.defer( - () -> { - out.onComplete(); - closeNotifier.onComplete(); - return closeNotifier; - }); + public Mono send(Publisher frames) { + Objects.requireNonNull(frames, "frames must not be null"); + + return Flux.from(frames).doOnNext(out::onNext).then(); } @Override - public Mono onClose() { - return closeNotifier; + public Mono sendOne(ByteBuf frame) { + Objects.requireNonNull(frame, "frame must not be null"); + out.onNext(frame); + return Mono.empty(); } @Override - public double availability() { - return closeNotifier.isDisposed() ? 0.0 : 1.0; + public ByteBufAllocator alloc() { + return allocator; } } diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java index baeb9faa3..c07713cb3 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,37 +1,40 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.Closeable; -import io.rsocket.DuplexConnection; +import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; +import java.util.Objects; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.function.Consumer; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; +import reactor.util.annotation.Nullable; -public class LocalServerTransport implements ServerTransport { - private static final ConcurrentMap registry = - new ConcurrentHashMap<>(); +/** + * An implementation of {@link ServerTransport} that connects to a {@link ClientTransport} in the + * same JVM. + */ +public final class LocalServerTransport implements ServerTransport { - static ServerDuplexConnectionAcceptor findServer(String name) { - return registry.get(name); - } + private static final ConcurrentMap registry = + new ConcurrentHashMap<>(); private final String name; @@ -39,54 +42,108 @@ private LocalServerTransport(String name) { this.name = name; } + /** + * Creates an instance. + * + * @param name the name of this {@link ServerTransport} that clients will connect to + * @return a new instance + * @throws NullPointerException if {@code name} is {@code null} + */ public static LocalServerTransport create(String name) { + Objects.requireNonNull(name, "name must not be null"); return new LocalServerTransport(name); } + /** + * Creates an instance with a random name. + * + * @return a new instance with a random name + */ + public static LocalServerTransport createEphemeral() { + return create(UUID.randomUUID().toString()); + } + + /** + * Remove an instance from the JVM registry. + * + * @param name the local transport instance to free + * @throws NullPointerException if {@code name} is {@code null} + */ + public static void dispose(String name) { + Objects.requireNonNull(name, "name must not be null"); + registry.remove(name); + } + + /** + * Retrieves an instance of {@link ConnectionAcceptor} based on the name of its {@code + * LocalServerTransport}. Returns {@code null} if that server is not registered. + * + * @param name the name of the server to retrieve + * @return the server if it has been registered, {@code null} otherwise + * @throws NullPointerException if {@code name} is {@code null} + */ + static @Nullable ConnectionAcceptor findServer(String name) { + Objects.requireNonNull(name, "name must not be null"); + + return registry.get(name); + } + + /** Return the name associated with this local server instance. */ + String getName() { + return name; + } + + /** + * Return a new {@link LocalClientTransport} connected to this {@code LocalServerTransport} + * through its {@link #getName()}. + */ + public LocalClientTransport clientTransport() { + return LocalClientTransport.create(name); + } + @Override public Mono start(ConnectionAcceptor acceptor) { + Objects.requireNonNull(acceptor, "acceptor must not be null"); return Mono.create( sink -> { - ServerDuplexConnectionAcceptor serverDuplexConnectionAcceptor = - new ServerDuplexConnectionAcceptor(name, acceptor); - if (registry.putIfAbsent(name, serverDuplexConnectionAcceptor) != null) { + ServerCloseable closeable = new ServerCloseable(name, acceptor); + if (registry.putIfAbsent(name, acceptor) != null) { throw new IllegalStateException("name already registered: " + name); } - sink.success(serverDuplexConnectionAcceptor); + sink.success(closeable); }); } - static class ServerDuplexConnectionAcceptor implements Consumer, Closeable { + static class ServerCloseable implements Closeable { + private final LocalSocketAddress address; + private final ConnectionAcceptor acceptor; - private final MonoProcessor closeNotifier = MonoProcessor.create(); - public ServerDuplexConnectionAcceptor(String name, ConnectionAcceptor acceptor) { + private final MonoProcessor onClose = MonoProcessor.create(); + + ServerCloseable(String name, ConnectionAcceptor acceptor) { + Objects.requireNonNull(name, "name must not be null"); this.address = new LocalSocketAddress(name); this.acceptor = acceptor; } @Override - public void accept(DuplexConnection duplexConnection) { - acceptor.apply(duplexConnection).subscribe(); + public void dispose() { + if (!registry.remove(address.getName(), acceptor)) { + throw new AssertionError(); + } + onClose.onComplete(); } @Override - public Mono close() { - return Mono.defer( - () -> { - if (!registry.remove(address.getName(), this)) { - throw new AssertionError(); - } - - closeNotifier.onComplete(); - return Mono.empty(); - }); + public boolean isDisposed() { + return onClose.isDisposed(); } @Override public Mono onClose() { - return closeNotifier; + return onClose; } } } diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalSocketAddress.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalSocketAddress.java index 48aece758..d04fd482e 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalSocketAddress.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalSocketAddress.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -17,22 +17,31 @@ package io.rsocket.transport.local; import java.net.SocketAddress; +import java.util.Objects; -public class LocalSocketAddress extends SocketAddress { +/** An implementation of {@link SocketAddress} representing a local connection. */ +final class LocalSocketAddress extends SocketAddress { - private static final long serialVersionUID = -5974652906020342524L; - private final String name; + private static final long serialVersionUID = -7513338854585475473L; - public LocalSocketAddress(String name) { - this.name = name; - } + private final String name; - public String getName() { - return name; + /** + * Creates a new instance. + * + * @param name the name representing the address + * @throws NullPointerException if {@code name} is {@code null} + */ + LocalSocketAddress(String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); } @Override public String toString() { return "[local server] " + name; } + + String getName() { + return name; + } } diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalUriHandler.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalUriHandler.java deleted file mode 100644 index a24842982..000000000 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalUriHandler.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.local; - -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import io.rsocket.uri.UriHandler; -import java.net.URI; -import java.util.Optional; - -public class LocalUriHandler implements UriHandler { - @Override - public Optional buildClient(URI uri) { - if ("local".equals(uri.getScheme())) { - return Optional.of(LocalClientTransport.create(uri.getSchemeSpecificPart())); - } - - return UriHandler.super.buildClient(uri); - } - - @Override - public Optional buildServer(URI uri) { - if ("local".equals(uri.getScheme())) { - return Optional.of(LocalServerTransport.create(uri.getSchemeSpecificPart())); - } - - return UriHandler.super.buildServer(uri); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/CancelException.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/package-info.java similarity index 67% rename from rsocket-core/src/main/java/io/rsocket/exceptions/CancelException.java rename to rsocket-transport-local/src/main/java/io/rsocket/transport/local/package-info.java index 1c2797b61..6a67f6af4 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/CancelException.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/package-info.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.exceptions; -public class CancelException extends Throwable { - public CancelException(String message) { - super(message); - } -} +/** The local RSocket transport implementation. */ +@NonNullApi +package io.rsocket.transport.local; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-transport-local/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler b/rsocket-transport-local/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler deleted file mode 100644 index 6b72bbd84..000000000 --- a/rsocket-transport-local/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2016 Netflix, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -io.rsocket.transport.local.LocalUriHandler diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientSetupRule.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientSetupRule.java deleted file mode 100644 index 0c172587c..000000000 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientSetupRule.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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.Closeable; -import io.rsocket.test.ClientSetupRule; -import java.util.concurrent.atomic.AtomicInteger; - -public class LocalClientSetupRule extends ClientSetupRule { - private static final AtomicInteger uniqueNameGenerator = new AtomicInteger(); - - public LocalClientSetupRule() { - super( - () -> "test" + uniqueNameGenerator.incrementAndGet(), - (address, server) -> LocalClientTransport.create(address), - address -> LocalServerTransport.create(address)); - } -} 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 new file mode 100644 index 000000000..ac4c13efe --- /dev/null +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientTransportTest.java @@ -0,0 +1,64 @@ +/* + * 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.transport.local; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +final class LocalClientTransportTest { + + @DisplayName("connects to server") + @Test + void connect() { + LocalServerTransport serverTransport = LocalServerTransport.createEphemeral(); + + serverTransport + .start(duplexConnection -> Mono.empty()) + .flatMap(closeable -> LocalClientTransport.create(serverTransport.getName()).connect()) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } + + @DisplayName("generates error if server not started") + @Test + void connectNoServer() { + LocalClientTransport.create("test-name") + .connect() + .as(StepVerifier::create) + .verifyErrorMessage("Could not find server: test-name"); + } + + @DisplayName("creates client") + @Test + void create() { + assertThat(LocalClientTransport.create("test-name")).isNotNull(); + } + + @DisplayName("throws NullPointerException with null name") + @Test + void createNullName() { + assertThatNullPointerException() + .isThrownBy(() -> LocalClientTransport.create(null)) + .withMessage("name must not be null"); + } +} diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalPingPong.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalPingPong.java index e6c186524..9228e2d05 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalPingPong.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalPingPong.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.transport.local; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.test.PingClient; import io.rsocket.test.PingHandler; import java.time.Duration; @@ -25,28 +28,26 @@ public final class LocalPingPong { - public static void main(String... args) throws Exception { - RSocketFactory.receive() - .acceptor(new PingHandler()) - .transport(LocalServerTransport.create("test-local-server")) - .start() + public static void main(String... args) { + RSocketServer.create(new PingHandler()) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(LocalServerTransport.create("test-local-server")) .block(); - Mono rSocketMono = - RSocketFactory.connect() - .transport(LocalClientTransport.create("test-local-server")) - .start(); + Mono client = + RSocketConnector.create() + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(LocalClientTransport.create("test-local-server")); - PingClient pingClient = new PingClient(rSocketMono); + PingClient pingClient = new PingClient(client); Recorder recorder = pingClient.startTracker(Duration.ofSeconds(1)); - final int count = 1_000_000_000; + + int count = 1_000_000_000; + pingClient - .startPingPong(count, recorder) - .doOnTerminate( - () -> { - System.out.println("Sent " + count + " messages."); - }) + .requestResponsePingPong(count, recorder) + .doOnTerminate(() -> System.out.println("Sent " + count + " messages.")) .blockLast(); } } 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 new file mode 100644 index 000000000..ed906f65b --- /dev/null +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalServerTransportTest.java @@ -0,0 +1,113 @@ +/* + * 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.transport.local; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +final class LocalServerTransportTest { + + @DisplayName("create throws NullPointerException with null name") + @Test + void createNullName() { + assertThatNullPointerException() + .isThrownBy(() -> LocalServerTransport.create(null)) + .withMessage("name must not be null"); + } + + @DisplayName("dispose removes name from registry") + @Test + void dispose() { + LocalServerTransport.dispose("test-name"); + } + + @DisplayName("dispose throws NullPointerException with null name") + @Test + void disposeNullName() { + assertThatNullPointerException() + .isThrownBy(() -> LocalServerTransport.dispose(null)) + .withMessage("name must not be null"); + } + + @DisplayName("creates transports with ephemeral names") + @Test + void ephemeral() { + LocalServerTransport serverTransport1 = LocalServerTransport.createEphemeral(); + LocalServerTransport serverTransport2 = LocalServerTransport.createEphemeral(); + + assertThat(serverTransport1.getName()).isNotEqualTo(serverTransport2.getName()); + } + + @DisplayName("returns the server by name") + @Test + void findServer() { + LocalServerTransport serverTransport = LocalServerTransport.createEphemeral(); + + serverTransport + .start(duplexConnection -> Mono.empty()) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + + assertThat(LocalServerTransport.findServer(serverTransport.getName())).isNotNull(); + } + + @DisplayName("returns null if server hasn't been started") + @Test + void findServerMissingName() { + assertThat(LocalServerTransport.findServer("test-name")).isNull(); + } + + @DisplayName("findServer throws NullPointerException with null name") + @Test + void findServerNullName() { + assertThatNullPointerException() + .isThrownBy(() -> LocalServerTransport.findServer(null)) + .withMessage("name must not be null"); + } + + @DisplayName("creates transport with name") + @Test + void named() { + LocalServerTransport serverTransport = LocalServerTransport.create("test-name"); + + assertThat(serverTransport.getName()).isEqualTo("test-name"); + } + + @DisplayName("starts local server transport") + @Test + void start() { + LocalServerTransport.createEphemeral() + .start(duplexConnection -> Mono.empty()) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } + + @DisplayName("start throws NullPointerException with null acceptor") + @Test + void startNullAcceptor() { + assertThatNullPointerException() + .isThrownBy(() -> LocalServerTransport.createEphemeral().start(null)) + .withMessage("acceptor must not be null"); + } +} diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalSocketAddressTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalSocketAddressTest.java new file mode 100644 index 000000000..8ad7b70ce --- /dev/null +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalSocketAddressTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.transport.local; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +final class LocalSocketAddressTest { + + @DisplayName("constructor throws NullPointerException with null name") + @Test + void constructorNullName() { + assertThatNullPointerException() + .isThrownBy(() -> new LocalSocketAddress(null)) + .withMessage("name must not be null"); + } + + @DisplayName("returns the configured name") + @Test + void name() { + assertThat(new LocalSocketAddress("test-name").getName()).isEqualTo("test-name"); + } +} 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 new file mode 100644 index 000000000..7184dd645 --- /dev/null +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.transport.local; + +final class LocalTransportTest { // implements TransportTest { + /* + TODO // think this has a memory leak or something in the local connection now that needs to be checked into. the test + TODO // isn't very happy when run from commandline i the command line + private static final AtomicInteger UNIQUE_NAME_GENERATOR = new AtomicInteger(); + + private final TransportPair transportPair = + new TransportPair<>( + () -> "test" + UNIQUE_NAME_GENERATOR.incrementAndGet(), + (address, server) -> LocalClientTransport.create(address), + LocalServerTransport::create); + + @Override + @Test + public void requestChannel512() { + + } + + @Override + public Duration getTimeout() { + return Duration.ofSeconds(10); + } + + @Override + public TransportPair getTransportPair() { + return transportPair; + }*/ +} diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriTransportRegistryTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriTransportRegistryTest.java deleted file mode 100644 index a7cbb8f4b..000000000 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriTransportRegistryTest.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.local; - -import static org.junit.Assert.assertTrue; - -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import io.rsocket.uri.UriTransportRegistry; -import org.junit.Test; - -public class LocalUriTransportRegistryTest { - @Test - public void testLocalClient() { - ClientTransport transport = UriTransportRegistry.clientForUri("local:test1"); - - assertTrue(transport instanceof LocalClientTransport); - } - - @Test - public void testLocalServer() { - ServerTransport transport = UriTransportRegistry.serverForUri("local:test1"); - - assertTrue(transport instanceof LocalServerTransport); - } -} diff --git a/rsocket-transport-local/src/test/resources/log4j.properties b/rsocket-transport-local/src/test/resources/log4j.properties deleted file mode 100644 index e1edb1274..000000000 --- a/rsocket-transport-local/src/test/resources/log4j.properties +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2016 Netflix, Inc. -#

-# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -#

-# http://www.apache.org/licenses/LICENSE-2.0 -#

-# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on -# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -# -log4j.rootLogger=INFO, stdout - -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] (%F:%L) - %m%n \ No newline at end of file diff --git a/rsocket-transport-local/src/test/resources/logback-test.xml b/rsocket-transport-local/src/test/resources/logback-test.xml new file mode 100644 index 000000000..01a7fa4cd --- /dev/null +++ b/rsocket-transport-local/src/test/resources/logback-test.xml @@ -0,0 +1,32 @@ + + + + + + + + %date{HH:mm:ss.SSS} %-10thread %-42logger %msg%n + + + + + + + + + + diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 7dea79ec4..201d56bbf 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,13 +14,34 @@ * limitations under the License. */ +plugins { + id 'java-library' + id 'maven-publish' + 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"]) { + os_suffix = "::" + osdetector.classifier +} + dependencies { - compile project(':rsocket-core') - compile 'io.projectreactor.ipc:reactor-netty:0.6.4.RELEASE' - compile "io.netty:netty-handler:4.1.12.Final" - compile "io.netty:netty-handler-proxy:4.1.12.Final" - compile "io.netty:netty-codec-http:4.1.12.Final" - compile "io.netty:netty-transport-native-epoll:4.1.12.Final" + api project(':rsocket-core') + api 'io.projectreactor.netty:reactor-netty' + api 'org.slf4j:slf4j-api' + + testImplementation project(':rsocket-test') + testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.mockito:mockito-core' + testImplementation 'org.mockito:mockito-junit-jupiter' + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'org.junit.jupiter:junit-jupiter-params' - testCompile project(':rsocket-test') + testRuntimeOnly 'ch.qos.logback:logback-classic' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + testRuntimeOnly 'io.netty:netty-tcnative-boringssl-static' + os_suffix } + +description = 'Reactor Netty RSocket transport implementations (TCP, Websocket)' diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/NettyDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/NettyDuplexConnection.java deleted file mode 100644 index 2afd85160..000000000 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/NettyDuplexConnection.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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.DuplexConnection; -import io.rsocket.Frame; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.NettyContext; -import reactor.ipc.netty.NettyInbound; -import reactor.ipc.netty.NettyOutbound; - -public class NettyDuplexConnection implements DuplexConnection { - private final NettyInbound in; - private final NettyOutbound out; - private final NettyContext context; - - public NettyDuplexConnection(NettyInbound in, NettyOutbound out, NettyContext context) { - this.in = in; - this.out = out; - this.context = context; - } - - @Override - public Mono send(Publisher frames) { - return Flux.from(frames).concatMap(this::sendOne).then(); - } - - @Override - public Mono sendOne(Frame frame) { - return out.sendObject(frame.content()).then(); - } - - @Override - public Flux receive() { - return in.receive().map(buf -> Frame.from(buf.retain())); - } - - @Override - public Mono close() { - return Mono.fromRunnable( - () -> { - context.dispose(); - context.channel().close(); - }); - } - - @Override - public Mono onClose() { - return context.onClose(); - } - - @Override - public double availability() { - return context.isDisposed() ? 0.0 : 1.0; - } -} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/RSocketLengthCodec.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/RSocketLengthCodec.java index f01ef363a..d7b368a3e 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/RSocketLengthCodec.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/RSocketLengthCodec.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,14 +16,42 @@ package io.rsocket.transport.netty; -import static io.rsocket.frame.FrameHeaderFlyweight.FRAME_LENGTH_MASK; -import static io.rsocket.frame.FrameHeaderFlyweight.FRAME_LENGTH_SIZE; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_SIZE; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.LengthFieldBasedFrameDecoder; -public class RSocketLengthCodec extends LengthFieldBasedFrameDecoder { +/** + * An extension to the Netty {@link LengthFieldBasedFrameDecoder} that encapsulates the + * RSocket-specific frame length header details. + */ +public final class RSocketLengthCodec extends LengthFieldBasedFrameDecoder { + /** Creates a new instance of the decoder, specifying the RSocket frame length header size. */ public RSocketLengthCodec() { - super(FRAME_LENGTH_MASK, 0, FRAME_LENGTH_SIZE, 0, 0); + this(FRAME_LENGTH_MASK); + } + + /** + * Creates a new instance of the decoder, specifying the RSocket frame length header size. + * + * @param maxFrameLength maximum allowed frame length for incoming rsocket frames + */ + public RSocketLengthCodec(int maxFrameLength) { + super(maxFrameLength, 0, FRAME_LENGTH_SIZE, 0, 0); + } + + /** + * Simplified non-netty focused decode usage. + * + * @param in the input buffer to read data from. + * @return decoded buffer or null is none available. + * @see #decode(ChannelHandlerContext, ByteBuf) + * @throws Exception if any error happens. + */ + public Object decode(ByteBuf in) throws Exception { + return decode(null, in); } } 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 new file mode 100644 index 000000000..618708bf0 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java @@ -0,0 +1,107 @@ +/* + * 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.transport.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.frame.FrameLengthCodec; +import io.rsocket.internal.BaseDuplexConnection; +import java.util.Objects; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; + +/** An implementation of {@link DuplexConnection} that connects via TCP. */ +public final class TcpDuplexConnection extends BaseDuplexConnection { + + private final Connection connection; + private final boolean encodeLength; + + /** + * Creates a new instance + * + * @param connection the {@link Connection} for managing the server + */ + public TcpDuplexConnection(Connection connection) { + this(connection, true); + } + + /** + * Creates a new instance + * + * @param encodeLength indicates if this connection should encode the length or not. + * @param connection the {@link Connection} to for managing the server + * @deprecated as of 1.0.1 in favor of using {@link #TcpDuplexConnection(Connection)} and hence + * {@code encodeLength} should always be true. + */ + @Deprecated + public TcpDuplexConnection(Connection connection, boolean encodeLength) { + this.encodeLength = encodeLength; + this.connection = Objects.requireNonNull(connection, "connection must not be null"); + + connection + .channel() + .closeFuture() + .addListener( + future -> { + if (!isDisposed()) dispose(); + }); + } + + @Override + public ByteBufAllocator alloc() { + return connection.channel().alloc(); + } + + @Override + protected void doOnClose() { + if (!connection.isDisposed()) { + connection.dispose(); + } + } + + @Override + public Flux receive() { + return connection.inbound().receive().map(this::decode); + } + + @Override + public Mono send(Publisher frames) { + if (frames instanceof Mono) { + return connection.outbound().sendObject(((Mono) frames).map(this::encode)).then(); + } + return connection.outbound().send(Flux.from(frames).map(this::encode)).then(); + } + + private ByteBuf encode(ByteBuf frame) { + if (encodeLength) { + return FrameLengthCodec.encode(alloc(), frame.readableBytes(), frame); + } else { + return frame; + } + } + + private ByteBuf decode(ByteBuf frame) { + if (encodeLength) { + return FrameLengthCodec.frame(frame).retain(); + } else { + return frame; + } + } +} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpUriHandler.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpUriHandler.java deleted file mode 100644 index bc7e24269..000000000 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpUriHandler.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.netty; - -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import io.rsocket.transport.netty.client.TcpClientTransport; -import io.rsocket.transport.netty.server.TcpServerTransport; -import io.rsocket.uri.UriHandler; -import java.net.URI; -import java.util.Optional; -import reactor.ipc.netty.tcp.TcpServer; - -public class TcpUriHandler implements UriHandler { - @Override - public Optional buildClient(URI uri) { - if ("tcp".equals(uri.getScheme())) { - return Optional.of(TcpClientTransport.create(uri.getHost(), uri.getPort())); - } - - return UriHandler.super.buildClient(uri); - } - - @Override - public Optional buildServer(URI uri) { - if ("tcp".equals(uri.getScheme())) { - return Optional.of(TcpServerTransport.create(TcpServer.create(uri.getHost(), uri.getPort()))); - } - - return UriHandler.super.buildServer(uri); - } -} 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 b98130139..0183ef19d 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,76 +16,72 @@ package io.rsocket.transport.netty; import io.netty.buffer.ByteBuf; -import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.rsocket.DuplexConnection; -import io.rsocket.Frame; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.internal.BaseDuplexConnection; +import java.util.Objects; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.ipc.netty.NettyContext; -import reactor.ipc.netty.NettyInbound; -import reactor.ipc.netty.NettyOutbound; - -import static io.netty.buffer.Unpooled.wrappedBuffer; -import static io.rsocket.frame.FrameHeaderFlyweight.FRAME_LENGTH_SIZE; +import reactor.netty.Connection; /** - * Implementation of a DuplexConnection for Websocket. + * An implementation of {@link DuplexConnection} that connects via a Websocket. * - * rsocket-java strongly assumes that each Frame is encoded with the length. This is not true for message oriented - * transports so this must be specifically dropped from Frames sent and stitched back on for frames received. + *

rsocket-java strongly assumes that each ByteBuf is encoded with the length. This is not true + * for message oriented transports so this must be specifically dropped from Frames sent and + * stitched back on for frames received. */ -public class WebsocketDuplexConnection implements DuplexConnection { - private final NettyInbound in; - private final NettyOutbound out; - private final NettyContext context; +public final class WebsocketDuplexConnection extends BaseDuplexConnection { - public WebsocketDuplexConnection(NettyInbound in, NettyOutbound out, NettyContext context) { - this.in = in; - this.out = out; - this.context = context; - } + private final Connection connection; - @Override - public Mono send(Publisher frames) { - return Flux.from(frames).concatMap(this::sendOne).then(); - } + /** + * Creates a new instance + * + * @param connection the {@link Connection} to for managing the server + */ + public WebsocketDuplexConnection(Connection connection) { + this.connection = Objects.requireNonNull(connection, "connection must not be null"); - @Override - public Mono sendOne(Frame frame) { - return out.sendObject(new BinaryWebSocketFrame(frame.content().skipBytes(FRAME_LENGTH_SIZE))).then(); + connection + .channel() + .closeFuture() + .addListener( + future -> { + if (!isDisposed()) dispose(); + }); } @Override - public Flux receive() { - return in.receive().map(buf -> { - CompositeByteBuf composite = context.channel().alloc().compositeBuffer(); - ByteBuf length = wrappedBuffer(new byte[FRAME_LENGTH_SIZE]); - FrameHeaderFlyweight.encodeLength(length, 0, buf.readableBytes()); - composite.addComponents(true, length, buf.retain()); - return Frame.from(composite); - }); + public ByteBufAllocator alloc() { + return connection.channel().alloc(); } @Override - public Mono close() { - return Mono.fromRunnable( - () -> { - if (!context.isDisposed()) { - context.channel().close(); - } - }); + protected void doOnClose() { + if (!connection.isDisposed()) { + connection.dispose(); + } } @Override - public Mono onClose() { - return context.onClose(); + public Flux receive() { + return connection.inbound().receive().map(ByteBuf::retain); } @Override - public double availability() { - return context.isDisposed() ? 0.0 : 1.0; + public Mono send(Publisher frames) { + if (frames instanceof Mono) { + return connection + .outbound() + .sendObject(((Mono) frames).map(BinaryWebSocketFrame::new)) + .then(); + } + return connection + .outbound() + .sendObject(Flux.from(frames).map(BinaryWebSocketFrame::new)) + .then(); } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketUriHandler.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketUriHandler.java deleted file mode 100644 index dcb3f295b..000000000 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketUriHandler.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.netty; - -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import io.rsocket.transport.netty.client.WebsocketClientTransport; -import io.rsocket.transport.netty.server.WebsocketServerTransport; -import io.rsocket.uri.UriHandler; -import java.net.URI; -import java.util.Optional; - -public class WebsocketUriHandler implements UriHandler { - @Override - public Optional buildClient(URI uri) { - if ("ws".equals(uri.getScheme()) || "wss".equals(uri.getScheme())) { - return Optional.of(WebsocketClientTransport.create(uri)); - } - - return UriHandler.super.buildClient(uri); - } - - @Override - public Optional buildServer(URI uri) { - if ("ws".equals(uri.getScheme())) { - return Optional.of( - WebsocketServerTransport.create( - uri.getHost(), WebsocketClientTransport.getPort(uri, 80))); - } - - return UriHandler.super.buildServer(uri); - } -} 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 8a17b9b09..f64c6063c 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 @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,57 +16,106 @@ package io.rsocket.transport.netty.client; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + import io.rsocket.DuplexConnection; import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.netty.NettyDuplexConnection; +import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.RSocketLengthCodec; +import io.rsocket.transport.netty.TcpDuplexConnection; import java.net.InetSocketAddress; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.Objects; import reactor.core.publisher.Mono; -import reactor.ipc.netty.tcp.TcpClient; +import reactor.netty.tcp.TcpClient; + +/** + * An implementation of {@link ClientTransport} that connects to a {@link ServerTransport} via TCP. + */ +public final class TcpClientTransport implements ClientTransport { -public class TcpClientTransport implements ClientTransport { - private final Logger logger = LoggerFactory.getLogger(TcpClientTransport.class); private final TcpClient client; + private final int maxFrameLength; - private TcpClientTransport(TcpClient client) { + private TcpClientTransport(TcpClient client, int maxFrameLength) { this.client = client; + this.maxFrameLength = maxFrameLength; } + /** + * Creates a new instance connecting to localhost + * + * @param port the port to connect to + * @return a new instance + */ public static TcpClientTransport create(int port) { - TcpClient tcpClient = TcpClient.create(port); + TcpClient tcpClient = TcpClient.create().port(port); return create(tcpClient); } + /** + * Creates a new instance + * + * @param bindAddress the address to connect to + * @param port the port to connect to + * @return a new instance + * @throws NullPointerException if {@code bindAddress} is {@code null} + */ public static TcpClientTransport create(String bindAddress, int port) { - TcpClient tcpClient = TcpClient.create(bindAddress, port); + Objects.requireNonNull(bindAddress, "bindAddress must not be null"); + + TcpClient tcpClient = TcpClient.create().host(bindAddress).port(port); return create(tcpClient); } + /** + * Creates a new instance + * + * @param address the address to connect to + * @return a new instance + * @throws NullPointerException if {@code address} is {@code null} + */ public static TcpClientTransport create(InetSocketAddress address) { - TcpClient tcpClient = TcpClient.create(address.getHostString(), address.getPort()); + Objects.requireNonNull(address, "address must not be null"); + + TcpClient tcpClient = TcpClient.create().remoteAddress(() -> address); return create(tcpClient); } + /** + * Creates a new instance + * + * @param client the {@link TcpClient} to use + * @return a new instance + * @throws NullPointerException if {@code client} is {@code null} + */ public static TcpClientTransport create(TcpClient client) { - return new TcpClientTransport(client); + return create(client, FRAME_LENGTH_MASK); + } + + /** + * Creates a new instance + * + * @param client the {@link TcpClient} to use + * @param maxFrameLength max frame length being sent over the connection + * @return a new instance + * @throws NullPointerException if {@code client} is {@code null} + */ + public static TcpClientTransport create(TcpClient client, int maxFrameLength) { + Objects.requireNonNull(client, "client must not be null"); + + return new TcpClientTransport(client, maxFrameLength); + } + + @Override + public int maxFrameLength() { + return maxFrameLength; } @Override public Mono connect() { - return Mono.create( - sink -> - client - .newHandler( - (in, out) -> { - in.context().addHandler("client-length-codec", new RSocketLengthCodec()); - NettyDuplexConnection connection = - new NettyDuplexConnection(in, out, in.context()); - sink.success(connection); - return connection.onClose(); - }) - .doOnError(sink::error) - .subscribe()); + return client + .doOnConnected(c -> c.addHandlerLast(new RSocketLengthCodec(maxFrameLength))) + .connect() + .map(TcpDuplexConnection::new); } } 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 49551eb42..dd6c535db 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 @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,83 +16,173 @@ package io.rsocket.transport.netty.client; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpHeaders; import io.rsocket.DuplexConnection; import io.rsocket.transport.ClientTransport; +import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.WebsocketDuplexConnection; import java.net.InetSocketAddress; import java.net.URI; +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.WebsocketClientSpec; +import reactor.netty.tcp.TcpClient; + +/** + * An implementation of {@link ClientTransport} that connects to a {@link ServerTransport} over + * WebSocket. + */ +@SuppressWarnings("deprecation") +public final class WebsocketClientTransport + implements ClientTransport, io.rsocket.transport.TransportHeaderAware { + + private static final String DEFAULT_PATH = "/"; -public class WebsocketClientTransport implements ClientTransport { private final HttpClient client; - private String path; + + private final String path; + + private HttpHeaders headers = new DefaultHttpHeaders(); + + private final WebsocketClientSpec.Builder specBuilder = + WebsocketClientSpec.builder().maxFramePayloadLength(FRAME_LENGTH_MASK); private WebsocketClientTransport(HttpClient client, String path) { + Objects.requireNonNull(client, "HttpClient must not be null"); + Objects.requireNonNull(path, "path must not be null"); this.client = client; - this.path = path; + this.path = path.startsWith("/") ? path : "/" + path; } + /** + * Creates a new instance connecting to localhost + * + * @param port the port to connect to + * @return a new instance + */ public static WebsocketClientTransport create(int port) { - HttpClient httpClient = HttpClient.create(port); - return create(httpClient, "/"); + return create(TcpClient.create().port(port)); } + /** + * Creates a new instance + * + * @param bindAddress the address to connect to + * @param port the port to connect to + * @return a new instance + * @throws NullPointerException if {@code bindAddress} is {@code null} + */ public static WebsocketClientTransport create(String bindAddress, int port) { - HttpClient httpClient = HttpClient.create(bindAddress, port); - return create(httpClient, "/"); + return create(TcpClient.create().host(bindAddress).port(port)); } + /** + * Creates a new instance + * + * @param address the address to connect to + * @return a new instance + * @throws NullPointerException if {@code address} is {@code null} + */ public static WebsocketClientTransport create(InetSocketAddress address) { - return create(address.getHostName(), address.getPort()); + Objects.requireNonNull(address, "address must not be null"); + return create(TcpClient.create().remoteAddress(() -> address)); + } + + /** + * Creates a new instance + * + * @param client the {@link TcpClient} to use + * @return a new instance + * @throws NullPointerException if {@code client} or {@code path} is {@code null} + */ + public static WebsocketClientTransport create(TcpClient client) { + return new WebsocketClientTransport(HttpClient.from(client), DEFAULT_PATH); } + /** + * Creates a new instance + * + * @param uri the URI to connect to + * @return a new instance + * @throws NullPointerException if {@code uri} is {@code null} + */ public static WebsocketClientTransport create(URI uri) { - HttpClient httpClient = createClient(uri); - return create(httpClient, uri.toString()); + Objects.requireNonNull(uri, "uri must not be null"); + boolean isSecure = uri.getScheme().equals("wss") || uri.getScheme().equals("https"); + TcpClient client = + (isSecure ? TcpClient.create().secure() : TcpClient.create()) + .host(uri.getHost()) + .port(uri.getPort() == -1 ? (isSecure ? 443 : 80) : uri.getPort()); + return new WebsocketClientTransport(HttpClient.from(client), uri.getPath()); } - private static HttpClient createClient(URI uri) { - if (isSecureWebsocket(uri)) { - return HttpClient.create( - options -> options.sslSupport().connect(uri.getHost(), getPort(uri, 443))); - } else { - return HttpClient.create(uri.getHost(), getPort(uri, 80)); - } + /** + * Creates a new instance + * + * @param client the {@link HttpClient} to use + * @param path the path to request + * @return a new instance + * @throws NullPointerException if {@code client} or {@code path} is {@code null} + */ + public static WebsocketClientTransport create(HttpClient client, String path) { + return new WebsocketClientTransport(client, path); } - public static int getPort(URI uri, int defaultPort) { - return uri.getPort() == -1 ? defaultPort : uri.getPort(); + /** + * Add a header and value(s) to use for the WebSocket handshake request. + * + * @param name the header name + * @param values the header value(s) + * @return the same instance for method chaining + * @since 1.0.1 + */ + public WebsocketClientTransport header(String name, String... values) { + if (values != null) { + Arrays.stream(values).forEach(value -> headers.add(name, value)); + } + return this; } - public static boolean isSecureWebsocket(URI uri) { - return uri.getScheme().equals("wss") || uri.getScheme().equals("https"); + /** + * Provide a consumer to customize properties of the {@link WebsocketClientSpec} to use for + * WebSocket upgrades. The consumer is invoked immediately. + * + * @param configurer the configurer to apply to the spec + * @return the same instance for method chaining + * @since 1.0.1 + */ + public WebsocketClientTransport webSocketSpec(Consumer configurer) { + configurer.accept(specBuilder); + return this; } - public static boolean isPlaintextWebsocket(URI uri) { - return uri.getScheme().equals("ws") || uri.getScheme().equals("http"); + @Override + public void setTransportHeaders(Supplier> transportHeaders) { + if (transportHeaders != null) { + transportHeaders.get().forEach((name, value) -> headers.add(name, value)); + } } - public static WebsocketClientTransport create(HttpClient client, String path) { - return new WebsocketClientTransport(client, path); + @Override + public int maxFrameLength() { + return specBuilder.build().maxFramePayloadLength(); } @Override public Mono connect() { - return Mono.create( - sink -> - client - .ws(path) - .flatMap( - response -> - response.receiveWebsocket( - (in, out) -> { - WebsocketDuplexConnection connection = - new WebsocketDuplexConnection(in, out, in.context()); - sink.success(connection); - return connection.onClose(); - })) - .doOnError(sink::error) - .subscribe()); + return client + .headers(headers -> headers.add(this.headers)) + .websocket(specBuilder.build()) + .uri(path) + .connect() + .map(WebsocketDuplexConnection::new); } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/package-info.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/package-info.java new file mode 100644 index 000000000..4567f2012 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** The Netty-based RSocket client transport implementations. */ +@NonNullApi +package io.rsocket.transport.netty.client; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidRequestException.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/package-info.java similarity index 66% rename from rsocket-core/src/main/java/io/rsocket/exceptions/InvalidRequestException.java rename to rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/package-info.java index 9d4ae7567..599500cff 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidRequestException.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/package-info.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket.exceptions; -public class InvalidRequestException extends RuntimeException { - public InvalidRequestException(String message) { - super(message); - } -} +/** The Netty-based RSocket transport implementations. */ +@NonNullApi +package io.rsocket.transport.netty; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/BaseWebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/BaseWebsocketServerTransport.java new file mode 100644 index 000000000..5f04eb575 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/BaseWebsocketServerTransport.java @@ -0,0 +1,67 @@ +package io.rsocket.transport.netty.server; + +import static io.netty.channel.ChannelHandler.*; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import io.netty.util.ReferenceCountUtil; +import io.rsocket.Closeable; +import io.rsocket.transport.ServerTransport; +import java.util.function.Consumer; +import java.util.function.Function; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.netty.http.server.HttpServer; +import reactor.netty.http.server.WebsocketServerSpec; + +abstract class BaseWebsocketServerTransport< + SELF extends BaseWebsocketServerTransport, T extends Closeable> + implements ServerTransport { + private static final Logger logger = LoggerFactory.getLogger(BaseWebsocketServerTransport.class); + private static final ChannelHandler pongHandler = new PongHandler(); + + static Function serverConfigurer = + server -> + server.tcpConfiguration( + tcpServer -> + tcpServer.doOnConnection(connection -> connection.addHandlerLast(pongHandler))); + + final WebsocketServerSpec.Builder specBuilder = + WebsocketServerSpec.builder().maxFramePayloadLength(FRAME_LENGTH_MASK); + + /** + * Provide a consumer to customize properties of the {@link WebsocketServerSpec} to use for + * WebSocket upgrades. The consumer is invoked immediately. + * + * @param configurer the configurer to apply to the spec + * @return the same instance for method chaining + * @since 1.0.1 + */ + @SuppressWarnings("unchecked") + public SELF webSocketSpec(Consumer configurer) { + configurer.accept(specBuilder); + return (SELF) this; + } + + @Override + public int maxFrameLength() { + return specBuilder.build().maxFramePayloadLength(); + } + + @Sharable + private static class PongHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof PongWebSocketFrame) { + logger.debug("received WebSocket Pong Frame"); + ReferenceCountUtil.safeRelease(msg); + ctx.read(); + } else { + ctx.fireChannelRead(msg); + } + } + } +} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/CloseableChannel.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/CloseableChannel.java new file mode 100644 index 000000000..c4a257f76 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/CloseableChannel.java @@ -0,0 +1,87 @@ +/* + * 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.transport.netty.server; + +import io.rsocket.Closeable; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.util.Objects; +import reactor.core.publisher.Mono; +import reactor.netty.DisposableChannel; + +/** + * An implementation of {@link Closeable} that wraps a {@link DisposableChannel}, enabling + * close-ability and exposing the {@link DisposableChannel}'s address. + */ +public final class CloseableChannel implements Closeable { + + /** For forward compatibility: remove when RSocket compiles against Reactor 1.0. */ + private static final Method channelAddressMethod; + + static { + try { + channelAddressMethod = DisposableChannel.class.getMethod("address"); + } catch (NoSuchMethodException ex) { + throw new IllegalStateException("Expected address method", ex); + } + } + + private final DisposableChannel channel; + + /** + * Creates a new instance + * + * @param channel the {@link DisposableChannel} to wrap + * @throws NullPointerException if {@code context} is {@code null} + */ + CloseableChannel(DisposableChannel channel) { + this.channel = Objects.requireNonNull(channel, "channel must not be null"); + } + + /** + * Return local server selector channel address. + * + * @return local {@link InetSocketAddress} + * @see DisposableChannel#address() + */ + public InetSocketAddress address() { + try { + return channel.address(); + } catch (NoSuchMethodError e) { + try { + return (InetSocketAddress) channelAddressMethod.invoke(this.channel); + } catch (Exception ex) { + throw new IllegalStateException("Unable to obtain address", ex); + } + } + } + + @Override + public void dispose() { + channel.dispose(); + } + + @Override + public boolean isDisposed() { + return channel.isDisposed(); + } + + @Override + public Mono onClose() { + return channel.onDispose(); + } +} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/NettyContextCloseable.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/NettyContextCloseable.java deleted file mode 100644 index 114a1ca68..000000000 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/NettyContextCloseable.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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.server; - -import io.rsocket.Closeable; -import java.net.InetSocketAddress; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; -import reactor.ipc.netty.NettyContext; - -/** - * A {@link Closeable} wrapping a {@link NettyContext}, allowing for close and aware of its address. - */ -public class NettyContextCloseable implements Closeable { - private NettyContext context; - - private MonoProcessor onClose; - - NettyContextCloseable(NettyContext context) { - this.onClose = MonoProcessor.create(); - this.context = context; - } - - @Override - public Mono close() { - return Mono.empty() - .doFinally( - s -> { - context.dispose(); - onClose.onComplete(); - }) - .then(); - } - - @Override - public Mono onClose() { - return onClose; - } - - /** @see NettyContext#address() */ - public InetSocketAddress address() { - return context.address(); - } -} 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 36943691f..effc7bed5 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 @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,50 +16,109 @@ package io.rsocket.transport.netty.server; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; + +import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; -import io.rsocket.transport.netty.NettyDuplexConnection; import io.rsocket.transport.netty.RSocketLengthCodec; +import io.rsocket.transport.netty.TcpDuplexConnection; import java.net.InetSocketAddress; +import java.util.Objects; import reactor.core.publisher.Mono; -import reactor.ipc.netty.tcp.TcpServer; +import reactor.netty.tcp.TcpServer; + +/** + * An implementation of {@link ServerTransport} that connects to a {@link ClientTransport} via TCP. + */ +public final class TcpServerTransport implements ServerTransport { -public class TcpServerTransport implements ServerTransport { - TcpServer server; + private final TcpServer server; + private final int maxFrameLength; - private TcpServerTransport(TcpServer server) { + private TcpServerTransport(TcpServer server, int maxFrameLength) { this.server = server; + this.maxFrameLength = maxFrameLength; } - public static TcpServerTransport create(InetSocketAddress address) { - TcpServer server = TcpServer.create(address.getHostName(), address.getPort()); + /** + * Creates a new instance binding to localhost + * + * @param port the port to bind to + * @return a new instance + */ + public static TcpServerTransport create(int port) { + TcpServer server = TcpServer.create().port(port); return create(server); } + /** + * Creates a new instance + * + * @param bindAddress the address to bind to + * @param port the port to bind to + * @return a new instance + * @throws NullPointerException if {@code bindAddress} is {@code null} + */ public static TcpServerTransport create(String bindAddress, int port) { - TcpServer server = TcpServer.create(bindAddress, port); + Objects.requireNonNull(bindAddress, "bindAddress must not be null"); + TcpServer server = TcpServer.create().host(bindAddress).port(port); return create(server); } - public static TcpServerTransport create(int port) { - TcpServer server = TcpServer.create(port); - return create(server); + /** + * Creates a new instance + * + * @param address the address to bind to + * @return a new instance + * @throws NullPointerException if {@code address} is {@code null} + */ + public static TcpServerTransport create(InetSocketAddress address) { + Objects.requireNonNull(address, "address must not be null"); + return create(address.getHostName(), address.getPort()); } + /** + * Creates a new instance + * + * @param server the {@link TcpServer} to use + * @return a new instance + * @throws NullPointerException if {@code server} is {@code null} + */ public static TcpServerTransport create(TcpServer server) { - return new TcpServerTransport(server); + return create(server, FRAME_LENGTH_MASK); + } + + /** + * Creates a new instance + * + * @param server the {@link TcpServer} to use + * @param maxFrameLength max frame length being sent over the connection + * @return a new instance + * @throws NullPointerException if {@code server} is {@code null} + */ + public static TcpServerTransport create(TcpServer server, int maxFrameLength) { + Objects.requireNonNull(server, "server must not be null"); + return new TcpServerTransport(server, maxFrameLength); } @Override - public Mono start(ConnectionAcceptor acceptor) { - return server - .newHandler( - (in, out) -> { - in.context().addHandler("server-length-codec", new RSocketLengthCodec()); - NettyDuplexConnection connection = new NettyDuplexConnection(in, out, in.context()); - acceptor.apply(connection).subscribe(); + public int maxFrameLength() { + return maxFrameLength; + } - return out.neverComplete(); + @Override + public Mono start(ConnectionAcceptor acceptor) { + Objects.requireNonNull(acceptor, "acceptor must not be null"); + return server + .doOnConnection( + c -> { + c.addHandlerLast(new RSocketLengthCodec(maxFrameLength)); + acceptor + .apply(new TcpDuplexConnection(c)) + .then(Mono.never()) + .subscribe(c.disposeSubscriber()); }) - .map(NettyContextCloseable::new); + .bind() + .map(CloseableChannel::new); } } 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 new file mode 100644 index 000000000..38344c472 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java @@ -0,0 +1,85 @@ +/* + * 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.transport.netty.server; + +import io.rsocket.Closeable; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.WebsocketDuplexConnection; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; +import reactor.netty.http.server.HttpServer; +import reactor.netty.http.server.HttpServerRoutes; +import reactor.netty.http.websocket.WebsocketInbound; +import reactor.netty.http.websocket.WebsocketOutbound; + +/** + * An implementation of {@link ServerTransport} that connects via Websocket and listens on specified + * routes. + */ +public final class WebsocketRouteTransport + extends BaseWebsocketServerTransport { + + private final String path; + + private final Consumer routesBuilder; + + private final HttpServer server; + + /** + * Creates a new instance + * + * @param server the {@link HttpServer} to use + * @param routesBuilder the builder for the routes that will be listened on + * @param path the path foe each route + */ + public WebsocketRouteTransport( + HttpServer server, Consumer routesBuilder, String path) { + this.server = serverConfigurer.apply(Objects.requireNonNull(server, "server must not be null")); + this.routesBuilder = Objects.requireNonNull(routesBuilder, "routesBuilder must not be null"); + this.path = Objects.requireNonNull(path, "path must not be null"); + } + + @Override + public Mono start(ConnectionAcceptor acceptor) { + Objects.requireNonNull(acceptor, "acceptor must not be null"); + return server + .route( + routes -> { + routesBuilder.accept(routes); + routes.ws(path, newHandler(acceptor), specBuilder.build()); + }) + .bind() + .map(CloseableChannel::new); + } + + /** + * Creates a new Websocket handler + * + * @param acceptor the {@link ConnectionAcceptor} to use with the handler + * @return a new Websocket handler + * @throws NullPointerException if {@code acceptor} is {@code null} + */ + public static BiFunction> newHandler( + ConnectionAcceptor acceptor) { + return (in, out) -> + acceptor.apply(new WebsocketDuplexConnection((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 95a7a7fb9..4fb6417c9 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 @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,45 +16,123 @@ package io.rsocket.transport.netty.server; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpHeaders; +import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.WebsocketDuplexConnection; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.server.HttpServer; +import reactor.netty.Connection; +import reactor.netty.http.server.HttpServer; -public class WebsocketServerTransport implements ServerTransport { - HttpServer server; +/** + * An implementation of {@link ServerTransport} that connects to a {@link ClientTransport} via a + * Websocket. + */ +@SuppressWarnings("deprecation") +public final class WebsocketServerTransport + extends BaseWebsocketServerTransport + implements io.rsocket.transport.TransportHeaderAware { + + private final HttpServer server; + + private HttpHeaders headers = new DefaultHttpHeaders(); private WebsocketServerTransport(HttpServer server) { - this.server = server; + this.server = serverConfigurer.apply(Objects.requireNonNull(server, "server must not be null")); } - public static WebsocketServerTransport create(String bindAddress, int port) { - HttpServer httpServer = HttpServer.create(bindAddress, port); + /** + * Creates a new instance binding to localhost + * + * @param port the port to bind to + * @return a new instance + */ + public static WebsocketServerTransport create(int port) { + HttpServer httpServer = HttpServer.create().port(port); return create(httpServer); } - public static WebsocketServerTransport create(int port) { - HttpServer httpServer = HttpServer.create(port); + /** + * Creates a new instance + * + * @param bindAddress the address to bind to + * @param port the port to bind to + * @return a new instance + * @throws NullPointerException if {@code bindAddress} is {@code null} + */ + public static WebsocketServerTransport create(String bindAddress, int port) { + Objects.requireNonNull(bindAddress, "bindAddress must not be null"); + HttpServer httpServer = HttpServer.create().host(bindAddress).port(port); return create(httpServer); } - public static WebsocketServerTransport create(HttpServer server) { + /** + * Creates a new instance + * + * @param address the address to bind to + * @return a new instance + * @throws NullPointerException if {@code address} is {@code null} + */ + public static WebsocketServerTransport create(InetSocketAddress address) { + Objects.requireNonNull(address, "address must not be null"); + return create(address.getHostName(), address.getPort()); + } + + /** + * Creates a new instance + * + * @param server the {@link HttpServer} to use + * @return a new instance + * @throws NullPointerException if {@code server} is {@code null} + */ + public static WebsocketServerTransport create(final HttpServer server) { + Objects.requireNonNull(server, "server must not be null"); return new WebsocketServerTransport(server); } + /** + * Add a header and value(s) to set on the response of WebSocket handshakes. + * + * @param name the header name + * @param values the header value(s) + * @return the same instance for method chaining + * @since 1.0.1 + */ + public WebsocketServerTransport header(String name, String... values) { + if (values != null) { + Arrays.stream(values).forEach(value -> headers.add(name, value)); + } + return this; + } + + @Override + public void setTransportHeaders(Supplier> transportHeaders) { + if (transportHeaders != null) { + transportHeaders.get().forEach((name, value) -> headers.add(name, value)); + } + } + @Override - public Mono start(ServerTransport.ConnectionAcceptor acceptor) { + public Mono start(ConnectionAcceptor acceptor) { + Objects.requireNonNull(acceptor, "acceptor must not be null"); return server - .newHandler( - (request, response) -> - response.sendWebsocket( - (in, out) -> { - WebsocketDuplexConnection connection = - new WebsocketDuplexConnection(in, out, in.context()); - acceptor.apply(connection).subscribe(); - - return out.neverComplete(); - })) - .map(NettyContextCloseable::new); + .handle( + (request, response) -> { + response.headers(headers); + return response.sendWebsocket( + (in, out) -> + acceptor + .apply(new WebsocketDuplexConnection((Connection) in)) + .then(out.neverComplete()), + specBuilder.build()); + }) + .bind() + .map(CloseableChannel::new); } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/package-info.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/package-info.java new file mode 100644 index 000000000..031844d06 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** The Netty-based RSocket server transport implementations. */ +@NonNullApi +package io.rsocket.transport.netty.server; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-transport-netty/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler b/rsocket-transport-netty/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler deleted file mode 100644 index d762aba6a..000000000 --- a/rsocket-transport-netty/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright 2016 Netflix, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -io.rsocket.transport.netty.TcpUriHandler -io.rsocket.transport.netty.WebsocketUriHandler diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java new file mode 100644 index 000000000..23041ec65 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java @@ -0,0 +1,184 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.DefaultPayload; +import io.rsocket.util.RSocketProxy; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class FragmentTest { + private RSocket handler; + private CloseableChannel server; + private String message = null; + private String metaData = null; + private String responseMessage = null; + + private static Stream cases() { + return Stream.of(Arguments.of(0, 64), Arguments.of(64, 0), Arguments.of(64, 64)); + } + + public void startup(int frameSize) { + int randomPort = ThreadLocalRandom.current().nextInt(10_000, 20_000); + StringBuilder message = new StringBuilder(); + StringBuilder responseMessage = new StringBuilder(); + StringBuilder metaData = new StringBuilder(); + for (int i = 0; i < 100; i++) { + message.append("REQUEST "); + responseMessage.append("RESPONSE "); + metaData.append("METADATA "); + } + this.message = message.toString(); + this.responseMessage = responseMessage.toString(); + this.metaData = metaData.toString(); + + TcpServerTransport serverTransport = TcpServerTransport.create("localhost", randomPort); + server = + RSocketServer.create((setup, sendingSocket) -> Mono.just(new RSocketProxy(handler))) + .fragment(frameSize) + .bind(serverTransport) + .block(); + } + + private RSocket buildClient(int frameSize) { + return RSocketConnector.create() + .fragment(frameSize) + .connect(TcpClientTransport.create(server.address())) + .block(); + } + + @AfterEach + public void cleanup() { + server.dispose(); + } + + @ParameterizedTest + @MethodSource("cases") + void testFragmentNoMetaData(int clientFrameSize, int serverFrameSize) { + startup(serverFrameSize); + System.out.println( + "-------------------------------------------------testFragmentNoMetaData-------------------------------------------------"); + handler = + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + String request = payload.getDataUtf8(); + String metaData = payload.getMetadataUtf8(); + System.out.println("request message: " + request); + System.out.println("request metadata: " + metaData); + + return Flux.just(DefaultPayload.create(responseMessage)); + } + }; + + RSocket client = buildClient(clientFrameSize); + + System.out.println("original message: " + message); + System.out.println("original metadata: " + metaData); + Payload payload = client.requestStream(DefaultPayload.create(message)).blockLast(); + System.out.println("response message: " + payload.getDataUtf8()); + System.out.println("response metadata: " + payload.getMetadataUtf8()); + + assertThat(responseMessage).isEqualTo(payload.getDataUtf8()); + } + + @ParameterizedTest + @MethodSource("cases") + void testFragmentRequestMetaDataOnly(int clientFrameSize, int serverFrameSize) { + startup(serverFrameSize); + System.out.println( + "-------------------------------------------------testFragmentRequestMetaDataOnly-------------------------------------------------"); + handler = + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + String request = payload.getDataUtf8(); + String metaData = payload.getMetadataUtf8(); + System.out.println("request message: " + request); + System.out.println("request metadata: " + metaData); + + return Flux.just(DefaultPayload.create(responseMessage)); + } + }; + + RSocket client = buildClient(clientFrameSize); + + System.out.println("original message: " + message); + System.out.println("original metadata: " + metaData); + Payload payload = client.requestStream(DefaultPayload.create(message, metaData)).blockLast(); + System.out.println("response message: " + payload.getDataUtf8()); + System.out.println("response metadata: " + payload.getMetadataUtf8()); + + assertThat(responseMessage).isEqualTo(payload.getDataUtf8()); + } + + @ParameterizedTest + @MethodSource("cases") + void testFragmentBothMetaData(int clientFrameSize, int serverFrameSize) { + startup(serverFrameSize); + Payload responsePayload = DefaultPayload.create(responseMessage); + System.out.println( + "-------------------------------------------------testFragmentBothMetaData-------------------------------------------------"); + handler = + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + String request = payload.getDataUtf8(); + String metaData = payload.getMetadataUtf8(); + System.out.println("request message: " + request); + System.out.println("request metadata: " + metaData); + + return Flux.just(DefaultPayload.create(responseMessage, metaData)); + } + + @Override + public Mono requestResponse(Payload payload) { + String request = payload.getDataUtf8(); + String metaData = payload.getMetadataUtf8(); + System.out.println("request message: " + request); + System.out.println("request metadata: " + metaData); + + return Mono.just(DefaultPayload.create(responseMessage, metaData)); + } + }; + + RSocket client = buildClient(clientFrameSize); + + System.out.println("original message: " + message); + System.out.println("original metadata: " + metaData); + Payload payload = client.requestStream(DefaultPayload.create(message, metaData)).blockLast(); + System.out.println("response message: " + payload.getDataUtf8()); + System.out.println("response metadata: " + payload.getMetadataUtf8()); + + assertThat(responseMessage).isEqualTo(payload.getDataUtf8()); + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/NettyUriTransportRegistryTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/NettyUriTransportRegistryTest.java deleted file mode 100644 index 209236420..000000000 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/NettyUriTransportRegistryTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.netty; - -import static org.junit.Assert.assertTrue; - -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import io.rsocket.transport.netty.client.TcpClientTransport; -import io.rsocket.transport.netty.client.WebsocketClientTransport; -import io.rsocket.transport.netty.server.TcpServerTransport; -import io.rsocket.transport.netty.server.WebsocketServerTransport; -import io.rsocket.uri.UriTransportRegistry; -import org.junit.Test; - -public class NettyUriTransportRegistryTest { - @Test - public void testTcpClient() { - ClientTransport transport = UriTransportRegistry.clientForUri("tcp://localhost:9898"); - - assertTrue(transport instanceof TcpClientTransport); - } - - @Test - public void testTcpServer() { - ServerTransport transport = UriTransportRegistry.serverForUri("tcp://localhost:9898"); - - assertTrue(transport instanceof TcpServerTransport); - } - - @Test - public void testWsClient() { - ClientTransport transport = UriTransportRegistry.clientForUri("ws://localhost:9898"); - - assertTrue(transport instanceof WebsocketClientTransport); - } - - @Test - public void testWsServer() { - ServerTransport transport = UriTransportRegistry.serverForUri("ws://localhost:9898"); - - assertTrue(transport instanceof WebsocketServerTransport); - } -} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/RSocketFactoryNettyTransportFragmentationTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/RSocketFactoryNettyTransportFragmentationTest.java new file mode 100644 index 000000000..b9c0d4f60 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/RSocketFactoryNettyTransportFragmentationTest.java @@ -0,0 +1,80 @@ +package io.rsocket.transport.netty; + +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.transport.netty.server.WebsocketServerTransport; +import java.time.Duration; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class RSocketFactoryNettyTransportFragmentationTest { + + static Stream> arguments() { + return Stream.of(TcpServerTransport.create(0), WebsocketServerTransport.create(0)); + } + + @ParameterizedTest + @MethodSource("arguments") + void serverSucceedsWithEnabledFragmentationOnSufficientMtu( + ServerTransport serverTransport) { + Mono server = + RSocketServer.create(mockAcceptor()) + .fragment(100) + .bind(serverTransport) + .doOnNext(CloseableChannel::dispose); + StepVerifier.create(server).expectNextCount(1).expectComplete().verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("arguments") + void serverSucceedsWithDisabledFragmentation(ServerTransport serverTransport) { + Mono server = + RSocketServer.create(mockAcceptor()) + .bind(serverTransport) + .doOnNext(CloseableChannel::dispose); + StepVerifier.create(server).expectNextCount(1).expectComplete().verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("arguments") + void clientSucceedsWithEnabledFragmentationOnSufficientMtu( + ServerTransport serverTransport) { + CloseableChannel server = + RSocketServer.create(mockAcceptor()).fragment(100).bind(serverTransport).block(); + + Mono rSocket = + RSocketConnector.create() + .fragment(100) + .connect(TcpClientTransport.create(server.address())) + .doFinally(s -> server.dispose()); + StepVerifier.create(rSocket).expectNextCount(1).expectComplete().verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("arguments") + void clientSucceedsWithDisabledFragmentation(ServerTransport serverTransport) { + CloseableChannel server = RSocketServer.create(mockAcceptor()).bind(serverTransport).block(); + + Mono rSocket = + RSocketConnector.connectWith(TcpClientTransport.create(server.address())) + .doFinally(s -> server.dispose()); + StepVerifier.create(rSocket).expectNextCount(1).expectComplete().verify(Duration.ofSeconds(5)); + } + + private SocketAcceptor mockAcceptor() { + SocketAcceptor mock = Mockito.mock(SocketAcceptor.class); + Mockito.when(mock.accept(Mockito.any(), Mockito.any())) + .thenReturn(Mono.just(Mockito.mock(RSocket.class))); + return mock; + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/SecureWebsocketClientSetupRule.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/SecureWebsocketClientSetupRule.java deleted file mode 100644 index 7048445de..000000000 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/SecureWebsocketClientSetupRule.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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.handler.ssl.util.InsecureTrustManagerFactory; -import io.rsocket.test.ClientSetupRule; -import io.rsocket.transport.netty.client.WebsocketClientTransport; -import io.rsocket.transport.netty.server.NettyContextCloseable; -import io.rsocket.transport.netty.server.WebsocketServerTransport; -import java.net.InetSocketAddress; -import reactor.ipc.netty.http.client.HttpClient; -import reactor.ipc.netty.http.server.HttpServer; - -public class SecureWebsocketClientSetupRule - extends ClientSetupRule { - - public SecureWebsocketClientSetupRule() { - super( - () -> new InetSocketAddress("localhost", 0), - (address, server) -> - WebsocketClientTransport.create( - HttpClient.create( - options -> - options - .connect(server.address()) - .sslSupport(c -> c.trustManager(InsecureTrustManagerFactory.INSTANCE))), - "https://" - + server.address().getHostName() - + ":" - + server.address().getPort() - + "/"), - address -> - WebsocketServerTransport.create( - HttpServer.create(options -> options.listen(address).sslSelfSigned()))); - } -} 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 new file mode 100644 index 000000000..6fd3de791 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/SetupRejectionTest.java @@ -0,0 +1,118 @@ +package io.rsocket.transport.netty; + +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.exceptions.RejectedSetupException; +import io.rsocket.transport.ClientTransport; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.client.WebsocketClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.transport.netty.server.WebsocketServerTransport; +import io.rsocket.util.DefaultPayload; +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.function.Consumer; +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.test.StepVerifier; + +public class SetupRejectionTest { + + /* + TODO Fix this test + @DisplayName( + "Rejecting setup by server causes requester RSocket disposal and RejectedSetupException") + @ParameterizedTest + @MethodSource(value = "transports")*/ + void rejectSetupTcp( + Function> serverTransport, + Function clientTransport) { + + String errorMessage = "error"; + RejectingAcceptor acceptor = new RejectingAcceptor(errorMessage); + Mono serverRequester = acceptor.requesterRSocket(); + + CloseableChannel channel = + RSocketServer.create(acceptor) + .bind(serverTransport.apply(new InetSocketAddress("localhost", 0))) + .block(Duration.ofSeconds(5)); + + ErrorConsumer errorConsumer = new ErrorConsumer(); + + RSocket clientRequester = + RSocketConnector.connectWith(clientTransport.apply(channel.address())) + .doOnError(errorConsumer) + .block(Duration.ofSeconds(5)); + + StepVerifier.create(errorConsumer.errors().next()) + .expectNextMatches( + err -> err instanceof RejectedSetupException && errorMessage.equals(err.getMessage())) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + StepVerifier.create(clientRequester.onClose()).expectComplete().verify(Duration.ofSeconds(5)); + + StepVerifier.create(serverRequester.flatMap(socket -> socket.onClose())) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + StepVerifier.create(clientRequester.requestResponse(DefaultPayload.create("test"))) + .expectErrorMatches( + err -> err instanceof RejectedSetupException && errorMessage.equals(err.getMessage())) + .verify(Duration.ofSeconds(5)); + + channel.dispose(); + } + + static Stream transports() { + Function> tcpServer = + TcpServerTransport::create; + Function> wsServer = + WebsocketServerTransport::create; + Function tcpClient = TcpClientTransport::create; + Function wsClient = WebsocketClientTransport::create; + + return Stream.of(Arguments.of(tcpServer, tcpClient), Arguments.of(wsServer, wsClient)); + } + + static class ErrorConsumer implements Consumer { + private final EmitterProcessor errors = EmitterProcessor.create(); + + @Override + public void accept(Throwable t) { + errors.onNext(t); + } + + Flux errors() { + return errors; + } + } + + private static class RejectingAcceptor implements SocketAcceptor { + private final String msg; + private final EmitterProcessor requesters = EmitterProcessor.create(); + + public RejectingAcceptor(String msg) { + this.msg = msg; + } + + @Override + public Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket) { + requesters.onNext(sendingSocket); + return Mono.error(new RuntimeException(msg)); + } + + public Mono requesterRSocket() { + return requesters.next(); + } + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpClientServerTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpClientServerTest.java deleted file mode 100644 index 475dabf0c..000000000 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpClientServerTest.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.transport.netty; - -import io.rsocket.test.BaseClientServerTest; - -public class TcpClientServerTest extends BaseClientServerTest { - @Override - protected TcpClientSetupRule createClientServer() { - return new TcpClientSetupRule(); - } -} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPing.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPing.java index ac94b1a96..88c64648c 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPing.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPing.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,31 +13,85 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.transport.netty; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.Resume; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.test.PerfTest; import io.rsocket.test.PingClient; import io.rsocket.transport.netty.client.TcpClientTransport; import java.time.Duration; import org.HdrHistogram.Recorder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; +@PerfTest public final class TcpPing { + private static final int INTERACTIONS_COUNT = 1_000_000_000; + private static final int port = Integer.valueOf(System.getProperty("RSOCKET_TEST_PORT", "7878")); + + @BeforeEach + void setUp() { + System.out.println("Starting ping-pong test (TCP transport)"); + System.out.println("port: " + port); + } + + @Test + void requestResponseTest() { + PingClient pingClient = newPingClient(); + Recorder recorder = pingClient.startTracker(Duration.ofSeconds(1)); + + pingClient + .requestResponsePingPong(INTERACTIONS_COUNT, recorder) + .doOnTerminate(() -> System.out.println("Sent " + INTERACTIONS_COUNT + " messages.")) + .blockLast(); + } + + @Test + void requestStreamTest() { + PingClient pingClient = newPingClient(); + Recorder recorder = pingClient.startTracker(Duration.ofSeconds(1)); - public static void main(String... args) throws Exception { - Mono client = - RSocketFactory.connect().transport(TcpClientTransport.create(7878)).start(); + pingClient + .requestStreamPingPong(INTERACTIONS_COUNT, recorder) + .doOnTerminate(() -> System.out.println("Sent " + INTERACTIONS_COUNT + " messages.")) + .blockLast(); + } - PingClient pingClient = new PingClient(client); + @Test + void requestStreamResumableTest() { + PingClient pingClient = newResumablePingClient(); Recorder recorder = pingClient.startTracker(Duration.ofSeconds(1)); - final int count = 1_000_000_000; + pingClient - .startPingPong(count, recorder) - .doOnTerminate( - () -> { - System.out.println("Sent " + count + " messages."); - }) + .requestStreamPingPong(INTERACTIONS_COUNT, recorder) + .doOnTerminate(() -> System.out.println("Sent " + INTERACTIONS_COUNT + " messages.")) .blockLast(); } + + private static PingClient newPingClient() { + return newPingClient(false); + } + + private static PingClient newResumablePingClient() { + return newPingClient(true); + } + + private static PingClient newPingClient(boolean isResumable) { + RSocketConnector connector = RSocketConnector.create(); + if (isResumable) { + connector.resume(new Resume()); + } + Mono rSocket = + connector + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .keepAlive(Duration.ofMinutes(1), Duration.ofMinutes(30)) + .connect(TcpClientTransport.create(port)); + + return new PingClient(rSocket); + } } diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java index 35f54b5ba..338868470 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,19 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.transport.netty; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketServer; +import io.rsocket.core.Resume; +import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.test.PingHandler; import io.rsocket.transport.netty.server.TcpServerTransport; public final class TcpPongServer { + private static final boolean isResume = + Boolean.valueOf(System.getProperty("RSOCKET_TEST_RESUME", "false")); + private static final int port = Integer.valueOf(System.getProperty("RSOCKET_TEST_PORT", "7878")); + + public static void main(String... args) { + System.out.println("Starting TCP ping-pong server"); + System.out.println("port: " + port); + System.out.println("resume enabled: " + isResume); - public static void main(String... args) throws Exception { - RSocketFactory.receive() - .acceptor(new PingHandler()) - .transport(TcpServerTransport.create(7878)) - .start() + RSocketServer server = RSocketServer.create(new PingHandler()); + if (isResume) { + server.resume(new Resume()); + } + server + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(TcpServerTransport.create("localhost", port)) .block() .onClose() .block(); diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java new file mode 100644 index 000000000..95bebd6aa --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java @@ -0,0 +1,55 @@ +package io.rsocket.transport.netty; + +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.rsocket.test.TransportTest; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.TcpServerTransport; +import java.net.InetSocketAddress; +import java.security.cert.CertificateException; +import java.time.Duration; +import reactor.core.Exceptions; +import reactor.netty.tcp.TcpClient; +import reactor.netty.tcp.TcpServer; + +public class TcpSecureTransportTest implements TransportTest { + private final TransportPair transportPair = + new TransportPair<>( + () -> new InetSocketAddress("localhost", 0), + (address, server) -> + TcpClientTransport.create( + TcpClient.create() + .remoteAddress(server::address) + .secure( + ssl -> + ssl.sslContext( + SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE)))), + address -> { + try { + SelfSignedCertificate ssc = new SelfSignedCertificate(); + TcpServer server = + TcpServer.create() + .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() { + return Duration.ofMinutes(10); + } + + @Override + public TransportPair getTransportPair() { + return transportPair; + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpClientSetupRule.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpTransportTest.java similarity index 52% rename from rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpClientSetupRule.java rename to rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpTransportTest.java index 1c64ace87..182be1d91 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpClientSetupRule.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpTransportTest.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,18 +16,27 @@ package io.rsocket.transport.netty; -import io.rsocket.test.ClientSetupRule; +import io.rsocket.test.TransportTest; import io.rsocket.transport.netty.client.TcpClientTransport; -import io.rsocket.transport.netty.server.NettyContextCloseable; import io.rsocket.transport.netty.server.TcpServerTransport; import java.net.InetSocketAddress; +import java.time.Duration; -public class TcpClientSetupRule extends ClientSetupRule { +final class TcpTransportTest implements TransportTest { - public TcpClientSetupRule() { - super( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server) -> TcpClientTransport.create(server.address()), - address -> TcpServerTransport.create(address)); + private final TransportPair transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server) -> TcpClientTransport.create(server.address()), + TcpServerTransport::create); + + @Override + public Duration getTimeout() { + return Duration.ofMinutes(2); + } + + @Override + public TransportPair getTransportPair() { + return transportPair; } } diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketClient.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketClient.java new file mode 100644 index 000000000..2deb4a4a8 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketClient.java @@ -0,0 +1,128 @@ +package io.rsocket.transport.netty; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.websocketx.*; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.URI; + +/** + * This is an example of a WebSocket client. + * + *

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

The client will attempt to connect to the URI passed to it as the first argument. You don't + * have to specify any arguments if you want to connect to the example WebSocket server, as this is + * the default. + */ +public final class WebSocketClient { + + static final String URL = System.getProperty("url", "ws://127.0.0.1:7878/websocket"); + + public static void main(String[] args) throws Exception { + URI uri = new URI(URL); + String scheme = uri.getScheme() == null ? "ws" : uri.getScheme(); + final String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost(); + final int port; + if (uri.getPort() == -1) { + if ("ws".equalsIgnoreCase(scheme)) { + port = 80; + } else if ("wss".equalsIgnoreCase(scheme)) { + port = 443; + } else { + port = -1; + } + } else { + port = uri.getPort(); + } + + if (!"ws".equalsIgnoreCase(scheme) && !"wss".equalsIgnoreCase(scheme)) { + System.err.println("Only WS(S) is supported."); + return; + } + + final boolean ssl = "wss".equalsIgnoreCase(scheme); + final SslContext sslCtx; + if (ssl) { + sslCtx = + SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build(); + } else { + sslCtx = null; + } + + EventLoopGroup group = new NioEventLoopGroup(); + try { + // Connect with V13 (RFC 6455 aka HyBi-17). You can change it to V08 or V00. + // If you change it to V00, ping is not supported and remember to change + // HttpResponseDecoder to WebSocketHttpResponseDecoder in the pipeline. + final WebSocketClientHandler handler = + new WebSocketClientHandler( + WebSocketClientHandshakerFactory.newHandshaker( + uri, WebSocketVersion.V13, null, true, new DefaultHttpHeaders())); + + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + if (sslCtx != null) { + p.addLast(sslCtx.newHandler(ch.alloc(), host, port)); + } + p.addLast( + new HttpClientCodec(), + new HttpObjectAggregator(8192), + WebSocketClientCompressionHandler.INSTANCE, + handler); + } + }); + + Channel ch = b.connect(uri.getHost(), port).sync().channel(); + handler.handshakeFuture().sync(); + + BufferedReader console = new BufferedReader(new InputStreamReader(System.in)); + while (true) { + String msg = console.readLine(); + if (msg == null) { + break; + } else if ("bye".equals(msg.toLowerCase())) { + ch.writeAndFlush(new CloseWebSocketFrame()); + ch.closeFuture().sync(); + break; + } else if ("ping".equals(msg.toLowerCase())) { + WebSocketFrame frame = + new PingWebSocketFrame(Unpooled.wrappedBuffer(new byte[] {8, 1, 8, 1})); + ch.writeAndFlush(frame); + } else if ("pong".equals(msg.toLowerCase())) { + WebSocketFrame frame = + new PongWebSocketFrame(Unpooled.wrappedBuffer(new byte[] {8, 1, 8, 1})); + ch.writeAndFlush(frame); + } else { + WebSocketFrame frame = new TextWebSocketFrame(msg); + ch.writeAndFlush(frame); + } + } + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketClientHandler.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketClientHandler.java new file mode 100644 index 000000000..092cad2c7 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketClientHandler.java @@ -0,0 +1,90 @@ +package io.rsocket.transport.netty; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; +import io.netty.util.CharsetUtil; + +public class WebSocketClientHandler extends SimpleChannelInboundHandler { + + private final WebSocketClientHandshaker handshaker; + private ChannelPromise handshakeFuture; + + public WebSocketClientHandler(WebSocketClientHandshaker handshaker) { + this.handshaker = handshaker; + } + + public ChannelFuture handshakeFuture() { + return handshakeFuture; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + handshakeFuture = ctx.newPromise(); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + handshaker.handshake(ctx.channel()); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + System.out.println("WebSocket Client disconnected!"); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + Channel ch = ctx.channel(); + if (!handshaker.isHandshakeComplete()) { + try { + handshaker.finishHandshake(ch, (FullHttpResponse) msg); + System.out.println("WebSocket Client connected!"); + handshakeFuture.setSuccess(); + } catch (WebSocketHandshakeException e) { + System.out.println("WebSocket Client failed to connect"); + handshakeFuture.setFailure(e); + } + return; + } + + if (msg instanceof FullHttpResponse) { + FullHttpResponse response = (FullHttpResponse) msg; + throw new IllegalStateException( + "Unexpected FullHttpResponse (getStatus=" + + response.status() + + ", content=" + + response.content().toString(CharsetUtil.UTF_8) + + ')'); + } + + WebSocketFrame frame = (WebSocketFrame) msg; + if (frame instanceof TextWebSocketFrame) { + TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; + System.out.println("WebSocket Client received message: " + textFrame.text()); + } else if (frame instanceof PongWebSocketFrame) { + System.out.println("WebSocket Client received pong"); + } else if (frame instanceof CloseWebSocketFrame) { + System.out.println("WebSocket Client received closing"); + ch.close(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + if (!handshakeFuture.isDone()) { + handshakeFuture.setFailure(cause); + } + ctx.close(); + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java new file mode 100644 index 000000000..c418dea0f --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java @@ -0,0 +1,49 @@ +package io.rsocket.transport.netty; + +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.client.WebsocketClientTransport; +import io.rsocket.transport.netty.server.WebsocketRouteTransport; +import io.rsocket.util.DefaultPayload; +import io.rsocket.util.EmptyPayload; +import java.net.URI; +import java.time.Duration; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.netty.DisposableServer; +import reactor.netty.http.server.HttpServer; +import reactor.test.StepVerifier; + +public class WebSocketTransportIntegrationTest { + + @Test + public void sendStreamOfDataWithExternalHttpServerTest() { + ServerTransport.ConnectionAcceptor acceptor = + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> + Flux.range(0, 10).map(i -> DefaultPayload.create(String.valueOf(i))))) + .asConnectionAcceptor(); + + DisposableServer server = + HttpServer.create() + .host("localhost") + .route(router -> router.ws("/test", WebsocketRouteTransport.newHandler(acceptor))) + .bindNow(); + + RSocket rsocket = + RSocketConnector.connectWith( + WebsocketClientTransport.create( + URI.create("ws://" + server.host() + ":" + server.port() + "/test"))) + .block(); + + StepVerifier.create(rsocket.requestStream(EmptyPayload.INSTANCE)) + .expectSubscription() + .expectNextCount(10) + .expectComplete() + .verify(Duration.ofMillis(1000)); + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketClientServerTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketClientServerTest.java deleted file mode 100644 index 91542049d..000000000 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketClientServerTest.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.transport.netty; - -import io.rsocket.test.BaseClientServerTest; - -public class WebsocketClientServerTest extends BaseClientServerTest { - @Override - protected WebsocketClientSetupRule createClientServer() { - return new WebsocketClientSetupRule(); - } -} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java index e79c4771b..a784a43c0 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.transport.netty; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.test.PingClient; import io.rsocket.transport.netty.client.WebsocketClientTransport; import java.time.Duration; @@ -25,19 +27,21 @@ public final class WebsocketPing { - public static void main(String... args) throws Exception { + public static void main(String... args) { Mono client = - RSocketFactory.connect().transport(WebsocketClientTransport.create(7878)).start(); + RSocketConnector.create() + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(WebsocketClientTransport.create(7878)); PingClient pingClient = new PingClient(client); + Recorder recorder = pingClient.startTracker(Duration.ofSeconds(1)); - final int count = 1_000_000_000; + + int count = 1_000_000_000; + pingClient - .startPingPong(count, recorder) - .doOnTerminate( - () -> { - System.out.println("Sent " + count + " messages."); - }) + .requestResponsePingPong(count, recorder) + .doOnTerminate(() -> System.out.println("Sent " + count + " messages.")) .blockLast(); } } diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java new file mode 100644 index 000000000..e2ee9e521 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java @@ -0,0 +1,152 @@ +package io.rsocket.transport.netty; + +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.util.ReferenceCountUtil; +import io.rsocket.Closeable; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.client.WebsocketClientTransport; +import io.rsocket.transport.netty.server.WebsocketRouteTransport; +import io.rsocket.transport.netty.server.WebsocketServerTransport; +import io.rsocket.util.DefaultPayload; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.server.HttpServer; +import reactor.test.StepVerifier; + +public class WebsocketPingPongIntegrationTest { + private static final String host = "localhost"; + private static final int port = 8088; + + private Closeable server; + + @AfterEach + void tearDown() { + server.dispose(); + } + + @ParameterizedTest + @MethodSource("provideServerTransport") + void webSocketPingPong(ServerTransport serverTransport) { + server = + RSocketServer.create(SocketAcceptor.forRequestResponse(Mono::just)) + .bind(serverTransport) + .block(); + + String expectedData = "data"; + String expectedPing = "ping"; + + PingSender pingSender = new PingSender(); + + HttpClient httpClient = + HttpClient.create() + .tcpConfiguration( + tcpClient -> + tcpClient + .doOnConnected(b -> b.addHandlerLast(pingSender)) + .host(host) + .port(port)); + + RSocket rSocket = + RSocketConnector.connectWith(WebsocketClientTransport.create(httpClient, "/")).block(); + + rSocket + .requestResponse(DefaultPayload.create(expectedData)) + .delaySubscription(pingSender.sendPing(expectedPing)) + .as(StepVerifier::create) + .expectNextMatches(p -> expectedData.equals(p.getDataUtf8())) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + pingSender + .receivePong() + .as(StepVerifier::create) + .expectNextMatches(expectedPing::equals) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + rSocket + .requestResponse(DefaultPayload.create(expectedData)) + .delaySubscription(pingSender.sendPong()) + .as(StepVerifier::create) + .expectNextMatches(p -> expectedData.equals(p.getDataUtf8())) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + private static Stream provideServerTransport() { + return Stream.of( + Arguments.of(WebsocketServerTransport.create(host, port)), + Arguments.of( + new WebsocketRouteTransport( + HttpServer.create().host(host).port(port), routes -> {}, "/"))); + } + + private static class PingSender extends ChannelInboundHandlerAdapter { + private final MonoProcessor channel = MonoProcessor.create(); + private final MonoProcessor pong = MonoProcessor.create(); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof PongWebSocketFrame) { + pong.onNext(((PongWebSocketFrame) msg).content().toString(StandardCharsets.UTF_8)); + ReferenceCountUtil.safeRelease(msg); + ctx.read(); + } else { + super.channelRead(ctx, msg); + } + } + + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { + Channel ch = ctx.channel(); + if (!channel.isTerminated() && ch.isWritable()) { + channel.onNext(ctx.channel()); + } + super.channelWritabilityChanged(ctx); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + Channel ch = ctx.channel(); + if (ch.isWritable()) { + channel.onNext(ch); + } + super.handlerAdded(ctx); + } + + public Mono sendPing(String data) { + return send( + new PingWebSocketFrame(Unpooled.wrappedBuffer(data.getBytes(StandardCharsets.UTF_8)))); + } + + public Mono sendPong() { + return send(new PongWebSocketFrame()); + } + + public Mono receivePong() { + return pong; + } + + private Mono send(WebSocketFrame webSocketFrame) { + return channel.doOnNext(ch -> ch.writeAndFlush(webSocketFrame)).then(); + } + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPongServer.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPongServer.java index 06780c9fc..84dc816be 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPongServer.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPongServer.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,19 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket.transport.netty; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketServer; +import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.test.PingHandler; import io.rsocket.transport.netty.server.WebsocketServerTransport; public final class WebsocketPongServer { - public static void main(String... args) throws Exception { - RSocketFactory.receive() - .acceptor(new PingHandler()) - .transport(WebsocketServerTransport.create(7878)) - .start() + public static void main(String... args) { + RSocketServer.create(new PingHandler()) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(WebsocketServerTransport.create(7878)) .block() .onClose() .block(); 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 new file mode 100644 index 000000000..ec33060b2 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketSecureTransportTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.transport.netty; + +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.rsocket.test.TransportTest; +import io.rsocket.transport.netty.client.WebsocketClientTransport; +import io.rsocket.transport.netty.server.WebsocketServerTransport; +import java.net.InetSocketAddress; +import java.security.cert.CertificateException; +import java.time.Duration; +import reactor.core.Exceptions; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.server.HttpServer; +import reactor.netty.tcp.TcpServer; + +final class WebsocketSecureTransportTest implements TransportTest { + + private final TransportPair transportPair = + new TransportPair<>( + () -> new InetSocketAddress("localhost", 0), + (address, server) -> + WebsocketClientTransport.create( + HttpClient.create() + .remoteAddress(server::address) + .secure( + ssl -> + ssl.sslContext( + SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE))), + String.format( + "https://%s:%d/", + server.address().getHostName(), server.address().getPort())), + address -> { + try { + SelfSignedCertificate ssc = new SelfSignedCertificate(); + HttpServer server = + HttpServer.from( + TcpServer.create() + .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() { + return Duration.ofMinutes(3); + } + + @Override + public TransportPair getTransportPair() { + return transportPair; + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketClientSetupRule.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketTransportTest.java similarity index 50% rename from rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketClientSetupRule.java rename to rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketTransportTest.java index a8c90872e..10d27daeb 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketClientSetupRule.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketTransportTest.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,19 +16,27 @@ package io.rsocket.transport.netty; -import io.rsocket.test.ClientSetupRule; +import io.rsocket.test.TransportTest; import io.rsocket.transport.netty.client.WebsocketClientTransport; -import io.rsocket.transport.netty.server.NettyContextCloseable; import io.rsocket.transport.netty.server.WebsocketServerTransport; import java.net.InetSocketAddress; +import java.time.Duration; -public class WebsocketClientSetupRule - extends ClientSetupRule { +final class WebsocketTransportTest implements TransportTest { - public WebsocketClientSetupRule() { - super( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server) -> WebsocketClientTransport.create(server.address()), - address -> WebsocketServerTransport.create(address.getHostName(), address.getPort())); + private final TransportPair transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server) -> WebsocketClientTransport.create(server.address()), + address -> WebsocketServerTransport.create(address.getHostName(), address.getPort())); + + @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/client/TcpClientTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/TcpClientTransportTest.java new file mode 100644 index 000000000..ac4c6044b --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/TcpClientTransportTest.java @@ -0,0 +1,103 @@ +/* + * 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.transport.netty.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.rsocket.transport.netty.server.TcpServerTransport; +import java.net.InetSocketAddress; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.netty.tcp.TcpClient; +import reactor.test.StepVerifier; + +final class TcpClientTransportTest { + + @DisplayName("connects to server") + @Test + void connect() { + InetSocketAddress address = InetSocketAddress.createUnresolved("localhost", 0); + + TcpServerTransport serverTransport = TcpServerTransport.create(address); + + serverTransport + .start(duplexConnection -> Mono.empty()) + .flatMap(context -> TcpClientTransport.create(context.address()).connect()) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } + + @DisplayName("create generates error if server not started") + @Test + void connectNoServer() { + TcpClientTransport.create(8000).connect().as(StepVerifier::create).verifyError(); + } + + @DisplayName("creates client with BindAddress") + @Test + void createBindAddress() { + assertThat(TcpClientTransport.create("test-bind-address", 8000)).isNotNull(); + } + + @DisplayName("creates client with InetSocketAddress") + @Test + void createInetSocketAddress() { + assertThat( + TcpClientTransport.create( + InetSocketAddress.createUnresolved("test-bind-address", 8000))) + .isNotNull(); + } + + @DisplayName("create throws NullPointerException with null bindAddress") + @Test + void createNullBindAddress() { + assertThatNullPointerException() + .isThrownBy(() -> TcpClientTransport.create((String) null, 8000)) + .withMessage("bindAddress must not be null"); + } + + @DisplayName("create throws NullPointerException with null address") + @Test + void createNullInetSocketAddress() { + assertThatNullPointerException() + .isThrownBy(() -> TcpClientTransport.create((InetSocketAddress) null)) + .withMessage("address must not be null"); + } + + @DisplayName("create throws NullPointerException with null client") + @Test + void createNullTcpClient() { + assertThatNullPointerException() + .isThrownBy(() -> TcpClientTransport.create((TcpClient) null)) + .withMessage("client must not be null"); + } + + @DisplayName("creates client with port") + @Test + void createPort() { + assertThat(TcpClientTransport.create(8000)).isNotNull(); + } + + @DisplayName("creates client with TcpClient") + @Test + void createTcpClient() { + assertThat(TcpClientTransport.create(TcpClient.create())).isNotNull(); + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java new file mode 100644 index 000000000..944d20313 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java @@ -0,0 +1,159 @@ +/* + * 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.transport.netty.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.rsocket.transport.netty.server.WebsocketServerTransport; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Collections; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.test.StepVerifier; + +@ExtendWith(MockitoExtension.class) +final class WebsocketClientTransportTest { + + @DisplayName("connects to server") + @Test + void connect() { + InetSocketAddress address = InetSocketAddress.createUnresolved("localhost", 0); + + WebsocketServerTransport serverTransport = WebsocketServerTransport.create(address); + + serverTransport + .start(duplexConnection -> Mono.empty()) + .flatMap(context -> WebsocketClientTransport.create(context.address()).connect()) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } + + @DisplayName("create generates error if server not started") + @Test + void connectNoServer() { + WebsocketClientTransport.create(8000).connect().as(StepVerifier::create).verifyError(); + } + + @DisplayName("creates client with BindAddress") + @Test + void createBindAddress() { + assertThat(WebsocketClientTransport.create("test-bind-address", 8000)) + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/"); + } + + @DisplayName("creates client with HttpClient") + @Test + void createHttpClient() { + assertThat(WebsocketClientTransport.create(HttpClient.create(), "/")) + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/"); + } + + @DisplayName("creates client with HttpClient and path without root") + @Test + void createHttpClientWithPathWithoutRoot() { + assertThat(WebsocketClientTransport.create(HttpClient.create(), "test")) + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/test"); + } + + @DisplayName("creates client with InetSocketAddress") + @Test + void createInetSocketAddress() { + assertThat( + WebsocketClientTransport.create( + InetSocketAddress.createUnresolved("test-bind-address", 8000))) + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/"); + } + + @DisplayName("create throws NullPointerException with null bindAddress") + @Test + void createNullBindAddress() { + assertThatNullPointerException() + .isThrownBy(() -> WebsocketClientTransport.create(null, 8000)) + .withMessage("host"); + } + + @DisplayName("create throws NullPointerException with null client") + @Test + void createNullHttpClient() { + assertThatNullPointerException() + .isThrownBy(() -> WebsocketClientTransport.create(null, "/test-path")) + .withMessage("HttpClient must not be null"); + } + + @DisplayName("create throws NullPointerException with null address") + @Test + void createNullInetSocketAddress() { + assertThatNullPointerException() + .isThrownBy(() -> WebsocketClientTransport.create((InetSocketAddress) null)) + .withMessage("address must not be null"); + } + + @DisplayName("create throws NullPointerException with null path") + @Test + void createNullPath() { + assertThatNullPointerException() + .isThrownBy(() -> WebsocketClientTransport.create(HttpClient.create(), null)) + .withMessage("path must not be null"); + } + + @DisplayName("create throws NullPointerException with null URI") + @Test + void createNullUri() { + assertThatNullPointerException() + .isThrownBy(() -> WebsocketClientTransport.create((URI) null)) + .withMessage("uri must not be null"); + } + + @DisplayName("creates client with port") + @Test + void createPort() { + assertThat(WebsocketClientTransport.create(8000)).isNotNull(); + } + + @DisplayName("creates client with URI") + @Test + void createUri() { + assertThat(WebsocketClientTransport.create(URI.create("ws://test-host"))) + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/"); + } + + @DisplayName("creates client with URI path") + @Test + void createUriPath() { + assertThat(WebsocketClientTransport.create(URI.create("ws://test-host/test"))) + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/test"); + } + + @DisplayName("sets transport headers") + @Test + void setTransportHeader() { + WebsocketClientTransport.create(8000).setTransportHeaders(Collections::emptyMap); + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/CloseableChannelTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/CloseableChannelTest.java new file mode 100644 index 000000000..308118955 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/CloseableChannelTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.transport.netty.server; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.netty.DisposableChannel; +import reactor.netty.tcp.TcpServer; +import reactor.test.StepVerifier; + +final class CloseableChannelTest { + + private final Mono channel = + TcpServer.create().handle((in, out) -> Mono.empty()).bind(); + + @DisplayName("returns the address of the context") + @Test + void address() { + channel + .map(CloseableChannel::new) + .map(CloseableChannel::address) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } + + @DisplayName("creates instance") + @Test + void constructor() { + channel.map(CloseableChannel::new).as(StepVerifier::create).expectNextCount(1).verifyComplete(); + } + + @DisplayName("constructor throws NullPointerException with null context") + @Test + void constructorNullContext() { + assertThatNullPointerException() + .isThrownBy(() -> new CloseableChannel(null)) + .withMessage("channel must not be null"); + } + + @Disabled( + "NettyContext isDisposed() is not accurate\n" + + "https://github.com/reactor/reactor-netty/issues/360") + @DisplayName("disposes context") + @Test + void dispose() { + channel + .map(CloseableChannel::new) + .delayUntil( + closeable -> { + closeable.dispose(); + return closeable.onClose().log(); + }) + .as(StepVerifier::create) + .assertNext(closeable -> assertThat(closeable.isDisposed()).isTrue()) + .verifyComplete(); + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/TcpServerTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/TcpServerTransportTest.java new file mode 100644 index 000000000..0e14d8f1d --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/TcpServerTransportTest.java @@ -0,0 +1,103 @@ +/* + * 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.transport.netty.server; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import java.net.InetSocketAddress; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.netty.tcp.TcpServer; +import reactor.test.StepVerifier; + +final class TcpServerTransportTest { + + @DisplayName("creates server with BindAddress") + @Test + void createBindAddress() { + assertThat(TcpServerTransport.create("test-bind-address", 8000)).isNotNull(); + } + + @DisplayName("creates server with InetSocketAddress") + @Test + void createInetSocketAddress() { + assertThat( + TcpServerTransport.create( + InetSocketAddress.createUnresolved("test-bind-address", 8000))) + .isNotNull(); + } + + @DisplayName("create throws NullPointerException with null bindAddress") + @Test + void createNullBindAddress() { + assertThatNullPointerException() + .isThrownBy(() -> TcpServerTransport.create((String) null, 8000)) + .withMessage("bindAddress must not be null"); + } + + @DisplayName("create throws NullPointerException with null address") + @Test + void createNullInetSocketAddress() { + assertThatNullPointerException() + .isThrownBy(() -> TcpServerTransport.create((InetSocketAddress) null)) + .withMessage("address must not be null"); + } + + @DisplayName("create throws NullPointerException with null server") + @Test + void createNullTcpClient() { + assertThatNullPointerException() + .isThrownBy(() -> TcpServerTransport.create((TcpServer) null)) + .withMessage("server must not be null"); + } + + @DisplayName("creates server with port") + @Test + void createPort() { + assertThat(TcpServerTransport.create("localhost", 8000)).isNotNull(); + } + + @DisplayName("creates client with TcpServer") + @Test + void createTcpClient() { + assertThat(TcpServerTransport.create(TcpServer.create())).isNotNull(); + } + + @DisplayName("starts server") + @Test + void start() { + InetSocketAddress address = InetSocketAddress.createUnresolved("localhost", 0); + + TcpServerTransport serverTransport = TcpServerTransport.create(address); + + serverTransport + .start(duplexConnection -> Mono.empty()) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } + + @DisplayName("start throws NullPointerException with null acceptor") + @Test + void startNullAcceptor() { + assertThatNullPointerException() + .isThrownBy(() -> TcpServerTransport.create("localhost", 8000).start(null)) + .withMessage("acceptor must not be null"); + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java new file mode 100644 index 000000000..2670b4a4b --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.transport.netty.server; + +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.netty.http.server.HttpServer; +import reactor.test.StepVerifier; + +final class WebsocketRouteTransportTest { + + @DisplayName("creates server") + @Test + void constructor() { + new WebsocketRouteTransport(HttpServer.create(), routes -> {}, "/test-path"); + } + + @DisplayName("constructor throw NullPointer with null path") + @Test + void constructorNullPath() { + assertThatNullPointerException() + .isThrownBy(() -> new WebsocketRouteTransport(HttpServer.create(), routes -> {}, null)) + .withMessage("path must not be null"); + } + + @DisplayName("constructor throw NullPointer with null routesBuilder") + @Test + void constructorNullRoutesBuilder() { + assertThatNullPointerException() + .isThrownBy(() -> new WebsocketRouteTransport(HttpServer.create(), null, "/test-path")) + .withMessage("routesBuilder must not be null"); + } + + @DisplayName("constructor throw NullPointer with null server") + @Test + void constructorNullServer() { + assertThatNullPointerException() + .isThrownBy(() -> new WebsocketRouteTransport(null, routes -> {}, "/test-path")) + .withMessage("server must not be null"); + } + + @DisplayName("starts server") + @Test + void start() { + WebsocketRouteTransport serverTransport = + new WebsocketRouteTransport(HttpServer.create(), routes -> {}, "/test-path"); + + serverTransport + .start(duplexConnection -> Mono.empty()) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } + + @DisplayName("start throw NullPointerException with null acceptor") + @Test + void startNullAcceptor() { + assertThatNullPointerException() + .isThrownBy( + () -> + new WebsocketRouteTransport(HttpServer.create(), routes -> {}, "/test-path") + .start(null)) + .withMessage("acceptor must not be null"); + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java new file mode 100644 index 000000000..7f7567dc8 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java @@ -0,0 +1,137 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.transport.netty.server; + +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.function.BiFunction; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import reactor.core.publisher.Mono; +import reactor.netty.http.server.HttpServer; +import reactor.netty.http.server.HttpServerRequest; +import reactor.netty.http.server.HttpServerResponse; +import reactor.test.StepVerifier; + +final class WebsocketServerTransportTest { + + // @Test + public void testThatSetupWithUnSpecifiedFrameSizeShouldSetMaxFrameSize() { + ArgumentCaptor captor = ArgumentCaptor.forClass(BiFunction.class); + HttpServer httpServer = Mockito.spy(HttpServer.create()); + Mockito.doAnswer(a -> httpServer).when(httpServer).handle(captor.capture()); + Mockito.doAnswer(a -> Mono.empty()).when(httpServer).bind(); + + WebsocketServerTransport serverTransport = WebsocketServerTransport.create(httpServer); + + serverTransport.start(c -> Mono.empty()).subscribe(); + + HttpServerRequest httpServerRequest = Mockito.mock(HttpServerRequest.class); + HttpServerResponse httpServerResponse = Mockito.mock(HttpServerResponse.class); + + captor.getValue().apply(httpServerRequest, httpServerResponse); + + Mockito.verify(httpServerResponse) + .sendWebsocket( + Mockito.nullable(String.class), Mockito.eq(FRAME_LENGTH_MASK), Mockito.any()); + } + + @DisplayName("creates server with BindAddress") + @Test + void createBindAddress() { + assertThat(WebsocketServerTransport.create("test-bind-address", 8000)).isNotNull(); + } + + @DisplayName("creates server with HttpClient") + @Test + void createHttpClient() { + assertThat(WebsocketServerTransport.create(HttpServer.create())).isNotNull(); + } + + @DisplayName("creates server with InetSocketAddress") + @Test + void createInetSocketAddress() { + assertThat( + WebsocketServerTransport.create( + InetSocketAddress.createUnresolved("test-bind-address", 8000))) + .isNotNull(); + } + + @DisplayName("create throws NullPointerException with null bindAddress") + @Test + void createNullBindAddress() { + assertThatNullPointerException() + .isThrownBy(() -> WebsocketServerTransport.create(null, 8000)) + .withMessage("bindAddress must not be null"); + } + + @DisplayName("create throws NullPointerException with null client") + @Test + void createNullHttpClient() { + assertThatNullPointerException() + .isThrownBy(() -> WebsocketServerTransport.create((HttpServer) null)) + .withMessage("server must not be null"); + } + + @DisplayName("create throws NullPointerException with null address") + @Test + void createNullInetSocketAddress() { + assertThatNullPointerException() + .isThrownBy(() -> WebsocketServerTransport.create((InetSocketAddress) null)) + .withMessage("address must not be null"); + } + + @DisplayName("creates server with port") + @Test + void createPort() { + assertThat(WebsocketServerTransport.create(8000)).isNotNull(); + } + + @DisplayName("sets transport headers") + @Test + void setTransportHeader() { + WebsocketServerTransport.create(8000).setTransportHeaders(Collections::emptyMap); + } + + @DisplayName("starts server") + @Test + void start() { + InetSocketAddress address = InetSocketAddress.createUnresolved("localhost", 0); + + WebsocketServerTransport serverTransport = WebsocketServerTransport.create(address); + + serverTransport + .start(duplexConnection -> Mono.empty()) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } + + @DisplayName("start throws NullPointerException with null acceptor") + @Test + void startNullAcceptor() { + assertThatNullPointerException() + .isThrownBy(() -> WebsocketServerTransport.create(8000).start(null)) + .withMessage("acceptor must not be null"); + } +} diff --git a/rsocket-transport-netty/src/test/resources/log4j.properties b/rsocket-transport-netty/src/test/resources/log4j.properties deleted file mode 100644 index 04bbed8ae..000000000 --- a/rsocket-transport-netty/src/test/resources/log4j.properties +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright 2016 Netflix, Inc. -#

-# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -#

-# http://www.apache.org/licenses/LICENSE-2.0 -#

-# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on -# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -# -log4j.rootLogger=INFO, stdout - -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %5p [%t] (%F) - %m%n -log4j.logger.io.rsocketogger=Debug \ No newline at end of file diff --git a/rsocket-transport-netty/src/test/resources/logback-test.xml b/rsocket-transport-netty/src/test/resources/logback-test.xml new file mode 100644 index 000000000..f9dec2bbe --- /dev/null +++ b/rsocket-transport-netty/src/test/resources/logback-test.xml @@ -0,0 +1,34 @@ + + + + + + + + %date{HH:mm:ss.SSS} %-10thread %-42logger %msg%n + + + + + + + + + + + + diff --git a/rsocket-transport-netty/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/rsocket-transport-netty/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..ca6ee9cea --- /dev/null +++ b/rsocket-transport-netty/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 99ca454bc..25c3feee5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,11 +1,11 @@ /* - * Copyright 2016 Netflix, Inc. + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,14 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +plugins { + id 'com.gradle.enterprise' version '3.1' +} + +rootProject.name = 'rsocket-java' -rootProject.name='rsocket' -include 'rsocket-load-balancer' include 'rsocket-core' -include 'rsocket-examples' -include 'rsocket-spectator' +include 'rsocket-load-balancer' +include 'rsocket-micrometer' include 'rsocket-test' -include 'rsocket-transport-aeron' include 'rsocket-transport-local' include 'rsocket-transport-netty' -include 'rsocket-tck-drivers' +include 'rsocket-bom' + +include 'rsocket-examples' +include 'benchmarks' + + + +gradleEnterprise { + buildScan { + termsOfServiceUrl = 'https://gradle.com/terms-of-service' + termsOfServiceAgree = 'yes' + } +} +