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 2743ae0bc..000000000 --- a/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -# -# Copyright 2015-2018 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---- -language: java - -jdk: -- oraclejdk8 -# - oraclejdk9 - -env: - global: - - secure: "WBCy0hsF96Xybj4n0AUrGY2m5FWCUa30XR+aVElSOO8d7v7BMypAT8mAd+yC2Y+j8WUGpIv59CqgeK1JrYdR9b3qRKhJmoE1Q92TotrxXMTIC9OKuU51LaaOqGYqx4SqiA2AyaikTFPd8um7KZfUpW/dG4IXySsiJ2OKT1jMUq6TmbWHnAYtjbl3u3WdjBQTIZNMtqG1+H1vIpsWyZrvbB4TWlNzhKBAu/YnlzMtvStrDaF7XrCJ2BQdMomQO18NH2gWxUEvLbQb6ip3wFl9CRe6vID7K1dmFwm08RPt9hRPC9yDahlIy8VvuNcWrP42TV+BVYy8V/hfaIo1pPsDBrtmVyc7YZjXSUM68orDFOkRB35qGkNIaAhy5Yt6G9QfwLXJkDFofW5KMKtDFUzf+j4DwS0CiDMF4k6Qq7YN1tYFXE9R8xa6Gv+wTNHqs4RURbYMS9IlbkhKxNbtyuema2sIUbsIfDezIzLI5BnfH2uli7O6/z0/G0Vfmf6A4q5Olm+7uhzMTI0GKheUIKr16SOxABlrwJtLJftzoKz9hYd3b7C9t61vYzccC3rWYobplwIcK2w50gFHQS8HLeiCjo8yjCx+IRSvAGaZIBPQdHCktrEYCVDUTXOxdaD6k6Ef+ppm8Nn+M+iC8x/G1wYE4x1lDqHw3GfhKsEQmiHL/98=" - - secure: "mbB+rv9eWUFQ9/yr2REH2ztH6r/Uq7cq/OJ5WK6yFp0TmPzlJ8jbEVwe/sdAMW2E4qrfMu1c2h3qsVm41pNx0MwEsIW/lTIZRiRmNYon32n+SHlRWyTn8dJeY/p1HoHs450OjLgB4X4jmRmfSt8IQ/w9ZCjF6HVcgR4ctt+myECTNcRidEIOahljnSJmnFFDsKbt2UJN96AfvvhbxcarEKgKLXLd9tQT2GlvEOM+hVOY9hKD5FvIoRp9heyCEAsSBXe+MIWQlh4jx+B4zCajZJ+8KN6M8KIt40lV8z4Zbc11jgq/xULJwkQIuVZvkJ3huIfUrxwLPgYWeai/TR/m3+2jy1hFajt96pnhJzFEz0IBL0wFALwAY1n2R/6uugEUYnDsFcGQGTsO5OeeOixiRPH5HNgfOhInqJoFh/887f+gq7OLXjlRCTsw+S9KknZ3iBpHX/+khurfAUC9khiMvufEq6Wyu0TvxhmGERFrs7uugeJ1VA85SDVQ6Au9MV831PeBGqzHpYG7w2kJj1EiFjBRUhCthxyDfX2b04egozlKF8JEifZ9EVj7pNMQUvVG2c9Wj6M0fG84NusnlZlA16XxAmfLevc9b/BOSSrqc2r9Z1ZvxFnBPP9H94Uqt9ZninhW/T49jRF+lQzD45MTVogzVk77XtdpzUemf4t5mHc=" - - secure: "GcPu3U4o2Dp7QLCqaAo3mGMJTl9yd+w+elXqqt7WDjrjm5p8mrzvQfyiJA7mRJVDTGpgib8fLctL1X1+QOX4fNKElrDUFhE3bWAqwVwHGPK4D3HCb6THD5XVqE4qcPmdLWPkvJ9ZY5nSIfuRVASjZTcc4XSXISK2jUSGar0PNYlo62/OFGvNvMz/qINU9RU7iYdDlL19yd72TKDfuK0UOKhQEGypamEHam3SMNCw/p8Q5K1vQe+Oba3ILCvYHJvqWc2NLjRXJjXfIaOq/NpCK6Lx2U9etdpkb5lyW5Cx1lkzIcRUq8ZUCwbkHog9LJoZGrZFh5AzlZ6kRuejBqu7AISmZy4s9HVAb7AQmNxvXkK9EIt8lavcaHnLYUIfuxvBqK/ptcUN5P/KXCs1DsbpADjB7YbUu/EQ2OAWncV31Z+O4uMHV29eGTtaz9LoK28+mHRfFHqoazWyuUejor6iSSkrCeqsLEvU8o6rH4oenKz7hLlZsJqHGACYtYNYi2CXYlTu0bMX+Hb1EtTu6Awm9Gn04TqVdmNexgF5CdqW4A696i6jlkPpVCt4B4nq4VPs2RMTkjVl3B7uOkDm18u35dncuhgsnMfVmo9cWX5COeyefdh6kdnKsUf0+IPbV/hix/OCP72dpuhxgcyzN+DvaVLzX7YOx7TpJTzPSKNEQZc=" - - secure: "UFJEzDEv6H2Qscg9UgZFVJq5oFvq7nQkVoSuGfh5Y4ZhL9PCK5f3Ft9oYEZOQwXaxWD1qivtJjQV3DdBiqsHkrnPrJ0hi3iYVDJo26xLNtu3welFw5Veqmgu2NuwjaDn6cjRFCJRLzpszMUWO1DvfLJTs3LuJDuXEyAKDw9eQgfOakqO4xeloyXgM7xnoXz11rgqtJNU6snjVPHftXNPTHGsNDlTR7SAIbjYwLMbdIKM2qjzrXkg+a94QOz2stnTDz9V5iYNH+3XXCcYxD9nb1Ol1XGWvtDnNGEhtGmylLdjHXwGLHiW2HOXskLzSkm7ASie1WdyHVHZb4X8LjxCy62S0FPevBgat1a443Khx5HCMYR/8dQrlOI82GYTr8n9U6QQE4Li8XLw64DVP9HGs9jdbsfEdlIsiPWqB6ujlwiO6pyfmQGQCgjALA+oD87uDQLcgh+SDYgE0ZwmwGzbjeynZpoCrEE8A1GHhSwkM9khx6EJFacm9XzqoUGK0wB1f8su+51fqPglF1zye80IFA4wOMMAY+KUc9du/vQ98f0lfjsNSOC02CKYxbA5RaakQMAYjirsZraA57xLmCSIGMhhW4wClQdJBww6LLz463yZU4WPwyqU+ZW12aV5dVLb5RWXIbZKmdT74DfZajHvqgTYpb05L5cJl7ApMspUkKk=" - -script: ci/travis.sh - -addons: - apt: - packages: - - oracle-java8-installer - # - oracle-java9-installer - -before_cache: -- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock -- rm -fr $HOME/.gradle/caches/*/plugin-resolution/ - -cache: - directories: - - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ - diff --git a/AUTHORS b/AUTHORS index 89f6e3696..ef7dd9dda 100644 --- a/AUTHORS +++ b/AUTHORS @@ -18,3 +18,4 @@ somasun = somasun stevegury = Steve Gury tmontgomery = Todd L. Montgomery yschimke = Yuri Schimke +OlegDokuka = Oleh Dokuka diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 934796246..56a5a7b69 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ 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. diff --git a/README.md b/README.md index 4e670aa63..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/ReactiveSocket/reactivesocket-java) +[![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,16 +15,35 @@ Learn more at http://rsocket.io ## Build and Binaries -[![Build Status](https://travis-ci.org/rsocket/rsocket-java.svg?branch=1.0.x)](https://travis-ci.org/rsocket/rsocket-java) +[![Main Branches Java CI](https://github.com/rsocket/rsocket-java/actions/workflows/gradle-main.yml/badge.svg?branch=1.0.x)](https://github.com/rsocket/rsocket-java/actions/workflows/gradle-main.yml) -Releases are available via Maven Central. +Releases and milestones are available via Maven Central. Example: ```groovy +repositories { + mavenCentral() + maven { url 'https://repo.spring.io/milestone' } // Reactor milestones (if needed) +} dependencies { - implementation 'io.rsocket:rsocket-core:0.11.5' - implementation 'io.rsocket:rsocket-transport-netty:0.11.5' + 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 { + implementation 'io.rsocket:rsocket-core:1.0.6-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-netty:1.0.6-SNAPSHOT' } ``` @@ -50,12 +69,12 @@ Frames can be printed out to help debugging. Set the logger `io.rsocket.FrameLog ## Trivial Client -``` +```java package io.rsocket.transport.netty; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; import io.rsocket.transport.netty.client.WebsocketClientTransport; import io.rsocket.util.DefaultPayload; import reactor.core.publisher.Flux; @@ -65,19 +84,47 @@ 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 client = RSocketFactory.connect().keepAlive().transport(ws).start().block(); + RSocket clientRSocket = RSocketConnector.connectWith(ws).block(); try { - Flux s = client.requestStream(DefaultPayload.create("peace")); + Flux s = clientRSocket.requestStream(DefaultPayload.create("peace")); s.take(10).doOnNext(p -> System.out.println(p.getDataUtf8())).blockLast(); } finally { - client.dispose(); + 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). diff --git a/artifactory.gradle b/artifactory.gradle deleted file mode 100644 index 6e622a610..000000000 --- a/artifactory.gradle +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -if (project.hasProperty('bintrayUser') && project.hasProperty('bintrayKey')) { - - subprojects { - plugins.withId('com.jfrog.artifactory') { - artifactory { - publish { - contextUrl = 'https://oss.jfrog.org' - - repository { - repoKey = 'oss-snapshot-local' - - // Credentials for oss.jfrog.org are a user's Bintray credentials - username = project.property('bintrayUser') - password = project.property('bintrayKey') - } - - defaults { - publications('maven') - } - } - } - } - } -} 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/bintray.gradle b/bintray.gradle deleted file mode 100644 index fd3207af7..000000000 --- a/bintray.gradle +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -if (project.hasProperty('bintrayUser') && project.hasProperty('bintrayKey') && - project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword')) { - - subprojects { - plugins.withId('com.jfrog.bintray') { - bintray { - user = project.property('bintrayUser') - key = project.property('bintrayKey') - - publications = ['maven'] - publish = true - - pkg { - repo = 'RSocket' - name = 'rsocket-java' - licenses = ['Apache-2.0'] - - issueTrackerUrl = 'https://github.com/rsocket/rsocket-java/issues' - websiteUrl = 'https://github.com/rsocket/rsocket-java' - vcsUrl = 'https://github.com/rsocket/rsocket-java.git' - - githubRepo = 'rsocket/rsocket-java' //Optional Github repository - githubReleaseNotesFile = 'README.md' //Optional Github readme file - - version { - name = project.version - released = new Date() - vcsTag = project.version - - gpg { - sign = true - } - - mavenCentralSync { - user = project.property('sonatypeUsername') - password = project.property('sonatypePassword') - } - } - } - } - } - } -} diff --git a/build.gradle b/build.gradle index 342a94954..00c5d2b9d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,59 +15,96 @@ */ plugins { - id 'com.gradle.build-scan' version '1.12.1' - - id 'com.github.sherter.google-java-format' version '0.6' - id 'com.jfrog.artifactory' version '4.7.0' - id 'com.jfrog.bintray' version '1.8.0' - id 'me.champeau.gradle.jmh' version '0.4.5' apply false - id 'io.spring.dependency-management' version '1.0.4.RELEASE' apply false - id 'io.morethan.jmhreport' version '0.7.0' apply false + 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 +} + +boolean isCiServer = ["CI", "CONTINUOUS_INTEGRATION", "TRAVIS", "CIRCLECI", "bamboo_planKey", "GITHUB_ACTION"].with { + retainAll(System.getenv().keySet()) + return !isEmpty() } subprojects { apply plugin: 'io.spring.dependency-management' + apply plugin: 'com.github.sherter.google-java-format' + + 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' + + group = "io.rsocket" + + googleJavaFormat { + toolVersion = '1.6' + } + + ext { + if (project.hasProperty('versionSuffix')) { + project.version += project.getProperty('versionSuffix') + } + } dependencyManagement { imports { - mavenBom 'io.projectreactor:reactor-bom:Californium-RELEASE' + 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:1.2.3' - dependency 'com.google.code.findbugs:jsr305:3.0.2' - dependency 'io.netty:netty-buffer:4.1.29.Final' - dependency 'io.aeron:aeron-all:1.4.1' - dependency 'io.micrometer:micrometer-core:1.0.6' - dependency 'org.assertj:assertj-core:3.9.1' - dependency 'org.hdrhistogram:HdrHistogram:2.1.10' - dependency 'org.jctools:jctools-core:2.1.2' - dependency 'org.mockito:mockito-core:2.16.0' - dependency 'org.openjdk.jmh:jmh-core:1.20' - dependency 'org.slf4j:slf4j-api:1.7.25' - - dependencySet(group: 'org.junit.jupiter', version: '5.1.0') { - entry 'junit-jupiter-api' - entry 'junit-jupiter-engine' - entry 'junit-jupiter-params' + 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:1.3' - dependencySet(group: 'org.junit.vintage', version: '5.1.0') { - entry 'junit-vintage-engine' - } - - dependencySet(group: 'org.openjdk.jmh', version: '1.20') { + 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 { 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 } plugins.withType(JavaPlugin) { @@ -79,17 +116,82 @@ subprojects { } 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 'https://docs.oracle.com/javase/8/docs/api/' + 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 + } - systemProperty "io.netty.leakDetection.level", "ADVANCED" + //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)") + } + } } } @@ -103,58 +205,14 @@ subprojects { classifier 'javadoc' from javadoc.destinationDir } - } - plugins.withType(MavenPublishPlugin) { - publishing { - publications { - maven(MavenPublication) { - groupId 'io.rsocket' - - from components.java - - artifact sourcesJar - artifact javadocJar - - pom.withXml { - asNode().':version' + { - resolveStrategy = DELEGATE_FIRST - - name project.name - description project.description - url 'http://rsocket.io' - - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/license/LICENSE-2.0.txt' - } - } - - 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' - } - } - - 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' - } - } + plugins.withType(MavenPublishPlugin) { + publishing { + publications { + maven(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar } } } @@ -162,8 +220,7 @@ subprojects { } } -apply from: 'artifactory.gradle' -apply from: 'bintray.gradle' +apply from: "${rootDir}/gradle/publications.gradle" buildScan { termsOfServiceUrl = 'https://gradle.com/terms-of-service' @@ -172,10 +229,6 @@ buildScan { description = 'RSocket: Stream Oriented Messaging Passing with Reactive Stream Semantics.' -googleJavaFormat { - toolVersion = '1.5' -} - repositories { mavenCentral() } diff --git a/ci/travis.sh b/ci/travis.sh deleted file mode 100755 index 372c01070..000000000 --- a/ci/travis.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash - -if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then - - echo -e "Building PR #$TRAVIS_PULL_REQUEST [$TRAVIS_PULL_REQUEST_SLUG/$TRAVIS_PULL_REQUEST_BRANCH => $TRAVIS_REPO_SLUG/$TRAVIS_BRANCH]" - ./gradlew build - -elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ] && [ "$bintrayUser" != "" ] ; then - - echo -e "Building Snapshot $TRAVIS_REPO_SLUG/$TRAVIS_BRANCH" - ./gradlew \ - -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" \ - -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" \ - build artifactoryPublish --stacktrace - -elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ] && [ "$bintrayUser" != "" ] ; then - - echo -e "Building Tag $TRAVIS_REPO_SLUG/$TRAVIS_TAG" - ./gradlew \ - -Pversion="$TRAVIS_TAG" \ - -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" \ - -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" \ - build bintrayUpload --stacktrace - -else - - echo -e "Building $TRAVIS_REPO_SLUG/$TRAVIS_BRANCH" - ./gradlew build - -fi - diff --git a/gradle.properties b/gradle.properties index 34dbb3a6c..4f89f4d88 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=0.11.0.BUILD-SNAPSHOT +version=1.0.6 +perfBaselineVersion=1.0.5 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 ed88a042a..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 9a4163a4f..8f6e03af5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#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 -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index cccdd3d51..83f2acfdc 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/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. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ 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="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -109,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"` diff --git a/gradlew.bat b/gradlew.bat index e95643d6a..24467a141 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@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 @@ -14,7 +30,7 @@ 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= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/rsocket-transport-aeron/build.gradle b/rsocket-bom/build.gradle old mode 100644 new mode 100755 similarity index 53% rename from rsocket-transport-aeron/build.gradle rename to rsocket-bom/build.gradle index cf983a9de..a75ab3bc8 --- a/rsocket-transport-aeron/build.gradle +++ b/rsocket-bom/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,26 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - plugins { - id 'java-library' + id 'java-platform' id 'maven-publish' - id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' + id 'signing' } -dependencies { - api project(':rsocket-core') - api 'io.aeron:aeron-all' - - implementation 'org.slf4j:slf4j-api' +description = 'RSocket Java Bill of materials.' - testImplementation project(':rsocket-test') - testImplementation 'org.junit.jupiter:junit-jupiter-api' +def excluded = ["rsocket-examples", "benchmarks"] - // TODO: Remove after JUnit5 migration - testCompileOnly 'junit:junit' - testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' +dependencies { + constraints { + parent.subprojects.findAll { it.name != project.name && !excluded.contains(it.name) } .sort { "$it.name" }.each { + api it + } + } } -description = 'Aeron RSocket transport implementation' +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 7743715a3..fbeee37ce 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -17,8 +17,7 @@ plugins { id 'java-library' id 'maven-publish' - id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' + id 'signing' id 'io.morethan.jmhreport' id 'me.champeau.gradle.jmh' } @@ -27,11 +26,8 @@ dependencies { api 'io.netty:netty-buffer' api 'io.projectreactor:reactor-core' - implementation 'org.jctools:jctools-core' implementation 'org.slf4j:slf4j-api' - compileOnly 'com.google.code.findbugs:jsr305' - testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' testImplementation 'org.junit.jupiter:junit-jupiter-api' @@ -39,7 +35,6 @@ dependencies { testImplementation 'org.mockito:mockito-core' testRuntimeOnly 'ch.qos.logback:logback-classic' - testRuntimeOnly 'org.jctools:jctools-core' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' // TODO: Remove after JUnit5 migration @@ -48,6 +43,4 @@ dependencies { testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' } -description = "Core functionality for the RSocket library" - -apply from: 'jmh.gradle' +description = "Core functionality for the RSocket library" \ No newline at end of file diff --git a/rsocket-core/jmh.gradle b/rsocket-core/jmh.gradle deleted file mode 100644 index b7cd10a13..000000000 --- a/rsocket-core/jmh.gradle +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -dependencies { - jmh configurations.api - jmh configurations.implementation - jmh 'org.openjdk.jmh:jmh-core' - jmh 'org.openjdk.jmh:jmh-generator-annprocess' -} - -jmhCompileGeneratedClasses.enabled = false - -jmh { - includeTests = false - profilers = ['gc'] - resultFormat = 'JSON' - - jvmArgs = ['-XX:+UnlockCommercialFeatures', '-XX:+FlightRecorder'] - // jvmArgsAppend = ['-XX:+UseG1GC', '-Xms4g', '-Xmx4g'] -} - -jmhJar { - from project.configurations.jmh -} - -tasks.jmh.finalizedBy tasks.jmhReport - -jmhReport { - jmhResultPath = project.file('build/reports/jmh/results.json') - jmhReportOutput = project.file('build/reports/jmh') -} 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 14cbee4ce..000000000 --- a/rsocket-core/src/jmh/java/io/rsocket/RSocketPerf.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import io.rsocket.RSocketFactory.Start; -import io.rsocket.perfutil.TestDuplexConnection; -import io.rsocket.util.DefaultPayload; -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 = {"-Dio.netty.leakDetection.level=advanced"} -) -@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.voidSubscriber); - } - - @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 = DefaultPayload.create(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 Start server = - RSocketFactory.receive() - .acceptor( - (setup, 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 void dispose() {} - - @Override - public Mono onClose() { - return Mono.empty(); - } - }; - - return Mono.just(rSocket); - }) - .transport( - acceptor -> { - Closeable closeable = - new Closeable() { - MonoProcessor onClose = MonoProcessor.create(); - - @Override - public void dispose() { - onClose.onComplete(); - } - - @Override - public boolean isDisposed() { - return onClose.isDisposed(); - } - - @Override - public Mono onClose() { - return onClose; - } - }; - - acceptor.apply(serverConnection).subscribe(); - - return Mono.just(closeable); - }); - - Subscriber blackHoleSubscriber; - Subscriber voidSubscriber; - - RSocket client; - - @Setup - public void setup(Blackhole bh) { - blackHoleSubscriber = subscriber(bh); - voidSubscriber = subscriber(bh); - - client = - RSocketFactory.connect().transport(() -> Mono.just(clientConnection)).start().block(); - - this.bh = bh; - } - - private Subscriber subscriber(Blackhole bh) { - return new Subscriber() { - @Override - public void onSubscribe(Subscription s) { - s.request(Long.MAX_VALUE); - } - - @Override - public void onNext(T o) { - bh.consume(o); - } - - @Override - public void onError(Throwable t) { - t.printStackTrace(); - } - - @Override - public void onComplete() {} - }; - } - } -} diff --git a/rsocket-core/src/jmh/java/io/rsocket/fragmentation/FragmentationPerformanceTest.java b/rsocket-core/src/jmh/java/io/rsocket/fragmentation/FragmentationPerformanceTest.java deleted file mode 100644 index 2b38a308f..000000000 --- a/rsocket-core/src/jmh/java/io/rsocket/fragmentation/FragmentationPerformanceTest.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.fragmentation; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.RequestResponseFrame.createRequestResponseFrame; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.rsocket.framing.Frame; -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; -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 reactor.core.publisher.SynchronousSink; -import reactor.util.context.Context; - -@BenchmarkMode(Mode.Throughput) -@Fork( - value = 1 // , jvmArgsAppend = {"-Dio.netty.leakDetection.level=advanced"} -) -@Warmup(iterations = 10) -@Measurement(iterations = 10_000) -@State(Scope.Thread) -public class FragmentationPerformanceTest { - - @Benchmark - public void largeFragmentation(Input input) { - Frame frame = - input.largeFragmenter.fragment(input.largeFrame).doOnNext(Frame::dispose).blockLast(); - - input.bh.consume(frame); - } - - @Benchmark - public void largeReassembly(Input input) { - input.largeFrames.forEach(frame -> input.reassembler.reassemble(frame)); - - input.bh.consume(input.sink.next); - } - - @Benchmark - public void smallFragmentation(Input input) { - Frame frame = - input.smallFragmenter.fragment(input.smallFrame).doOnNext(Frame::dispose).blockLast(); - - input.bh.consume(frame); - } - - @Benchmark - public void smallReassembly(Input input) { - input.smallFrames.forEach(frame -> input.reassembler.reassemble(frame)); - - input.bh.consume(input.sink.next); - } - - @State(Scope.Benchmark) - public static class Input { - - Blackhole bh; - - FrameFragmenter largeFragmenter; - - Frame largeFrame; - - List largeFrames; - - FrameReassembler reassembler = FrameReassembler.createFrameReassembler(DEFAULT); - - MockSynchronousSink sink; - - FrameFragmenter smallFragmenter; - - Frame smallFrame; - - List smallFrames; - - @Setup - public void setup(Blackhole bh) { - this.bh = bh; - - sink = new MockSynchronousSink<>(); - - largeFrame = - createRequestResponseFrame( - DEFAULT, false, getRandomByteBuf(1 << 18), getRandomByteBuf(1 << 18)); - - smallFrame = - createRequestResponseFrame(DEFAULT, false, getRandomByteBuf(16), getRandomByteBuf(16)); - - largeFragmenter = new FrameFragmenter(DEFAULT, 1024); - smallFragmenter = new FrameFragmenter(ByteBufAllocator.DEFAULT, 2); - - largeFrames = largeFragmenter.fragment(largeFrame).collectList().block(); - smallFrames = smallFragmenter.fragment(smallFrame).collectList().block(); - } - - private static ByteBuf getRandomByteBuf(int size) { - byte[] bytes = new byte[size]; - ThreadLocalRandom.current().nextBytes(bytes); - return Unpooled.wrappedBuffer(bytes); - } - } - - static final class MockSynchronousSink implements SynchronousSink { - - Throwable error; - - T next; - - @Override - public void complete() {} - - @Override - public Context currentContext() { - return null; - } - - @Override - public void error(Throwable e) { - this.error = e; - } - - @Override - public void next(T t) { - this.next = t; - } - } -} 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 401420b40..000000000 --- a/rsocket-core/src/jmh/java/io/rsocket/perfutil/TestDuplexConnection.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 void dispose() {} - - @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 c099a3120..7f39956dc 100644 --- a/rsocket-core/src/main/java/io/rsocket/AbstractRSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/AbstractRSocket.java @@ -16,48 +16,21 @@ package io.rsocket; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; /** * An abstract implementation of {@link RSocket}. All request handling methods emit {@link * UnsupportedOperationException} and hence must be overridden to provide a valid implementation. + * + * @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) { - payload.release(); - return Mono.error(new UnsupportedOperationException("Fire and forget not implemented.")); - } - - @Override - public Mono requestResponse(Payload payload) { - payload.release(); - return Mono.error(new UnsupportedOperationException("Request-Response not implemented.")); - } - - @Override - public Flux requestStream(Payload payload) { - payload.release(); - 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) { - payload.release(); - return Mono.error(new UnsupportedOperationException("Metadata-Push not implemented.")); - } - @Override public void dispose() { onClose.onComplete(); diff --git a/rsocket-core/src/main/java/io/rsocket/Closeable.java b/rsocket-core/src/main/java/io/rsocket/Closeable.java index 5eb871e18..2ea9a0371 100644 --- a/rsocket-core/src/main/java/io/rsocket/Closeable.java +++ b/rsocket-core/src/main/java/io/rsocket/Closeable.java @@ -16,17 +16,21 @@ package io.rsocket; +import org.reactivestreams.Subscriber; import reactor.core.Disposable; import reactor.core.publisher.Mono; -/** */ +/** An interface which allows listening to when a specific instance of this interface is closed */ public interface Closeable extends Disposable { /** - * Returns a {@code Publisher} that completes when this {@code RSocket} is closed. A {@code - * RSocket} can be closed by explicitly calling {@link RSocket#dispose()} or when the underlying - * transport connection is closed. + * 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. * - * @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 d88cfe445..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 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * 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, @@ -16,42 +16,32 @@ package io.rsocket; -import static io.rsocket.frame.FrameHeaderFlyweight.FLAGS_M; - import io.netty.buffer.ByteBuf; import io.netty.util.AbstractReferenceCounted; -import io.rsocket.Frame.Setup; -import io.rsocket.frame.SetupFrameFlyweight; -import io.rsocket.framing.FrameType; +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 extends AbstractReferenceCounted implements Payload { - public static ConnectionSetupPayload create(final Frame setupFrame) { - Frame.ensureFrameType(FrameType.SETUP, setupFrame); - return new DefaultConnectionSetupPayload(setupFrame); - } + public abstract String metadataMimeType(); + + public abstract String dataMimeType(); public abstract int keepAliveInterval(); public abstract int keepAliveMaxLifetime(); - public abstract String metadataMimeType(); - - public abstract String dataMimeType(); - public abstract int getFlags(); - public boolean willClientHonorLease() { - return Frame.isFlagSet(getFlags(), SetupFrameFlyweight.FLAGS_WILL_HONOR_LEASE); - } + public abstract boolean willClientHonorLease(); - @Override - public boolean hasMetadata() { - return Frame.isFlagSet(getFlags(), FLAGS_M); - } + public abstract boolean isResumeEnabled(); + + @Nullable + public abstract ByteBuf resumeToken(); @Override public ConnectionSetupPayload retain() { @@ -65,68 +55,18 @@ public ConnectionSetupPayload retain(int increment) { return this; } + @Override public abstract ConnectionSetupPayload touch(); - public abstract ConnectionSetupPayload touch(Object hint); - - private static final class DefaultConnectionSetupPayload extends ConnectionSetupPayload { - - private final Frame setupFrame; - - public DefaultConnectionSetupPayload(final Frame setupFrame) { - this.setupFrame = setupFrame; - } - - @Override - public int keepAliveInterval() { - return SetupFrameFlyweight.keepaliveInterval(setupFrame.content()); - } - - @Override - public int keepAliveMaxLifetime() { - return SetupFrameFlyweight.maxLifetime(setupFrame.content()); - } - - @Override - public String metadataMimeType() { - return Setup.metadataMimeType(setupFrame); - } - - @Override - public String dataMimeType() { - return Setup.dataMimeType(setupFrame); - } - - @Override - public ByteBuf sliceData() { - return setupFrame.sliceData(); - } - - @Override - public ByteBuf sliceMetadata() { - return setupFrame.sliceMetadata(); - } - - @Override - public int getFlags() { - return Setup.getFlags(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(); - } + /** + * 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 1e0f9f566..6190d24e3 100644 --- a/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java @@ -16,6 +16,8 @@ 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; @@ -26,19 +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 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 frames); + Mono send(Publisher frames); /** * Sends a single {@code Frame} on this connection and returns the {@code Publisher} representing @@ -48,34 +50,41 @@ 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() { 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 a8e5cf980..000000000 --- a/rsocket-core/src/main/java/io/rsocket/Frame.java +++ /dev/null @@ -1,656 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import static io.rsocket.frame.FrameHeaderFlyweight.FLAGS_M; - -import io.netty.buffer.*; -import io.netty.util.AbstractReferenceCounted; -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 io.rsocket.framing.FrameType; -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 extends AbstractReferenceCounted implements Payload, ByteBufHolder { - private static final Recycler RECYCLER = - new Recycler() { - protected Frame newObject(Handle handle) { - return new Frame(handle); - } - }; - - private final Handle handle; - private ByteBuf content; - - private Frame(final Handle handle) { - this.handle = handle; - } - - /** 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); - } - - /** Increases the reference count by {@code 1}. */ - @Override - public Frame retain() { - super.retain(); - return this; - } - - /** Increases the reference count by the specified {@code increment}. */ - @Override - public Frame retain(int increment) { - super.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 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 - public Frame touch(@Nullable Object hint) { - content.touch(hint); - return this; - } - - /** Called once {@link #refCnt()} is equals 0. */ - @Override - protected void deallocate() { - content.release(); - content = null; - handle.recycle(this); - } - - /** - * Return {@link ByteBuf} that is a {@link ByteBuf#slice()} for the frame metadata - * - *

If no metadata is present, the ByteBuf will have 0 capacity. - * - * @return ByteBuf containing the content - */ - public ByteBuf sliceMetadata() { - return hasMetadata() ? FrameHeaderFlyweight.sliceFrameMetadata(content) : Unpooled.EMPTY_BUFFER; - } - - /** - * Return {@link ByteBuf} that is a {@link ByteBuf#slice()} for the frame data - * - *

If no data is present, the ByteBuf will have 0 capacity. - * - * @return ByteBuf containing the data - */ - public ByteBuf sliceData() { - return FrameHeaderFlyweight.sliceFrameData(content); - } - - /** - * 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.setRefCnt(1); - frame.content = content; - - return frame; - } - - public static boolean isFlagSet(int flags, int checkedFlag) { - return (flags & checkedFlag) == checkedFlag; - } - - public static int setFlag(int current, int toSet) { - return current | toSet; - } - - @Override - public boolean hasMetadata() { - return Frame.isFlagSet(this.flags(), FLAGS_M); - } - - /* 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.hasMetadata() ? payload.sliceMetadata() : Unpooled.EMPTY_BUFFER; - final ByteBuf data = payload.sliceData(); - - final Frame frame = RECYCLER.get(); - frame.setRefCnt(1); - 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 dataBuffer) { - if (errorLogger.isDebugEnabled()) { - errorLogger.debug("an error occurred, creating error frame", throwable); - } - - final int code = ErrorFrameFlyweight.errorCodeFromException(throwable); - final Frame frame = RECYCLER.get(); - frame.setRefCnt(1); - frame.content = - ByteBufAllocator.DEFAULT.buffer( - ErrorFrameFlyweight.computeFrameLength(dataBuffer.readableBytes())); - frame.content.writerIndex( - ErrorFrameFlyweight.encode(frame.content, streamId, code, dataBuffer)); - return frame; - } - - public static Frame from(int streamId, final Throwable throwable) { - String data = throwable.getMessage() == null ? "" : throwable.getMessage(); - byte[] bytes = data.getBytes(StandardCharsets.UTF_8); - - return from(streamId, throwable, Unpooled.wrappedBuffer(bytes)); - } - - public static int errorCode(final Frame frame) { - ensureFrameType(FrameType.ERROR, frame); - return ErrorFrameFlyweight.errorCode(frame.content); - } - - public static String message(Frame frame) { - ensureFrameType(FrameType.ERROR, frame); - return ErrorFrameFlyweight.message(frame.content); - } - } - - public static class Lease { - private Lease() {} - - public static Frame from(int ttl, int numberOfRequests, ByteBuf metadata) { - final Frame frame = RECYCLER.get(); - frame.setRefCnt(1); - 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.setRefCnt(1); - 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 @Nullable ByteBuf metadata = payload.hasMetadata() ? payload.sliceMetadata() : null; - final ByteBuf data = payload.sliceData(); - - final Frame frame = RECYCLER.get(); - frame.setRefCnt(1); - frame.content = - ByteBufAllocator.DEFAULT.buffer( - RequestFrameFlyweight.computeFrameLength( - type, metadata != null ? metadata.readableBytes() : null, data.readableBytes())); - - if (type.hasInitialRequestN()) { - frame.content.writerIndex( - RequestFrameFlyweight.encode( - frame.content, - streamId, - metadata != null ? FLAGS_M : 0, - type, - initialRequestN, - metadata, - data)); - } else { - frame.content.writerIndex( - RequestFrameFlyweight.encode( - frame.content, streamId, metadata != null ? FLAGS_M : 0, type, metadata, data)); - } - - return frame; - } - - public static Frame from(int streamId, FrameType type, int flags) { - final Frame frame = RECYCLER.get(); - frame.setRefCnt(1); - frame.content = - ByteBufAllocator.DEFAULT.buffer(RequestFrameFlyweight.computeFrameLength(type, null, 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.setRefCnt(1); - 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 REQUEST_FNF: - case METADATA_PUSH: - 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, null, Unpooled.EMPTY_BUFFER, 0); - } - - public static Frame from(int streamId, FrameType type, Payload payload) { - return from(streamId, type, payload, payload.hasMetadata() ? FLAGS_M : 0); - } - - public static Frame from(int streamId, FrameType type, Payload payload, int flags) { - final ByteBuf metadata = payload.hasMetadata() ? payload.sliceMetadata() : null; - final ByteBuf data = payload.sliceData(); - return from(streamId, type, metadata, data, flags); - } - - public static Frame from( - int streamId, FrameType type, @Nullable ByteBuf metadata, ByteBuf data, int flags) { - final Frame frame = RECYCLER.get(); - frame.setRefCnt(1); - frame.content = - ByteBufAllocator.DEFAULT.buffer( - FrameHeaderFlyweight.computeFrameHeaderLength( - type, metadata != null ? metadata.readableBytes() : null, 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.setRefCnt(1); - frame.content = - ByteBufAllocator.DEFAULT.buffer( - FrameHeaderFlyweight.computeFrameHeaderLength(FrameType.CANCEL, null, 0)); - frame.content.writerIndex( - FrameHeaderFlyweight.encode( - frame.content, streamId, 0, FrameType.CANCEL, null, 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.setRefCnt(1); - 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 boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof Frame)) { - return false; - } - final Frame frame = (Frame) o; - return content.equals(frame.content()); - } - - @Override - public int hashCode() { - return content.hashCode(); - } - - @Override - public String toString() { - FrameType type = FrameHeaderFlyweight.frameType(content); - StringBuilder payload = new StringBuilder(); - @Nullable ByteBuf metadata = FrameHeaderFlyweight.sliceFrameMetadata(content); - - if (metadata != null) { - 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))); - } - - long streamId = FrameHeaderFlyweight.streamId(content); - - String additionalFlags = ""; - 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; - } - - return "Frame => Stream ID: " - + streamId - + " Type: " - + type - + additionalFlags - + " Payload: " - + payload; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/KeepAliveHandler.java b/rsocket-core/src/main/java/io/rsocket/KeepAliveHandler.java deleted file mode 100644 index 5ca1d4c7e..000000000 --- a/rsocket-core/src/main/java/io/rsocket/KeepAliveHandler.java +++ /dev/null @@ -1,116 +0,0 @@ -package io.rsocket; - -import io.netty.buffer.Unpooled; -import java.time.Duration; -import reactor.core.Disposable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; -import reactor.core.publisher.UnicastProcessor; - -abstract class KeepAliveHandler implements Disposable { - private final KeepAlive keepAlive; - private final UnicastProcessor sent = UnicastProcessor.create(); - private final MonoProcessor timeout = MonoProcessor.create(); - private Disposable intervalDisposable; - private volatile long lastReceivedMillis; - - static KeepAliveHandler ofServer(KeepAlive keepAlive) { - return new KeepAliveHandler.Server(keepAlive); - } - - static KeepAliveHandler ofClient(KeepAlive keepAlive) { - return new KeepAliveHandler.Client(keepAlive); - } - - private KeepAliveHandler(KeepAlive keepAlive) { - this.keepAlive = keepAlive; - this.lastReceivedMillis = System.currentTimeMillis(); - this.intervalDisposable = - Flux.interval(Duration.ofMillis(keepAlive.getTickPeriod())) - .subscribe(v -> onIntervalTick()); - } - - @Override - public void dispose() { - sent.onComplete(); - timeout.onComplete(); - intervalDisposable.dispose(); - } - - public void receive(Frame keepAliveFrame) { - this.lastReceivedMillis = System.currentTimeMillis(); - if (Frame.Keepalive.hasRespondFlag(keepAliveFrame)) { - doSend(Frame.Keepalive.from(Unpooled.wrappedBuffer(keepAliveFrame.getData()), false)); - } - } - - public Flux send() { - return sent; - } - - public Mono timeout() { - return timeout; - } - - abstract void onIntervalTick(); - - void doSend(Frame frame) { - sent.onNext(frame); - } - - void doCheckTimeout() { - long now = System.currentTimeMillis(); - if (now - lastReceivedMillis >= keepAlive.getTimeoutMillis()) { - timeout.onNext(keepAlive); - } - } - - private static class Server extends KeepAliveHandler { - - Server(KeepAlive keepAlive) { - super(keepAlive); - } - - @Override - void onIntervalTick() { - doCheckTimeout(); - } - } - - private static final class Client extends KeepAliveHandler { - - Client(KeepAlive keepAlive) { - super(keepAlive); - } - - @Override - void onIntervalTick() { - doCheckTimeout(); - doSend(Frame.Keepalive.from(Unpooled.EMPTY_BUFFER, true)); - } - } - - static final class KeepAlive { - private final long tickPeriod; - private final long timeoutMillis; - - KeepAlive(Duration tickPeriod, Duration timeoutMillis, int maxTicks) { - this.tickPeriod = tickPeriod.toMillis(); - this.timeoutMillis = timeoutMillis.toMillis() + maxTicks * tickPeriod.toMillis(); - } - - KeepAlive(long tickPeriod, long timeoutMillis) { - this.tickPeriod = tickPeriod; - this.timeoutMillis = timeoutMillis; - } - - public long getTickPeriod() { - return tickPeriod; - } - - public long getTimeoutMillis() { - return timeoutMillis; - } - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/Payload.java b/rsocket-core/src/main/java/io/rsocket/Payload.java index 8103d3c11..fc130528e 100644 --- a/rsocket-core/src/main/java/io/rsocket/Payload.java +++ b/rsocket-core/src/main/java/io/rsocket/Payload.java @@ -22,7 +22,7 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -/** Payload of a {@link Frame}. */ +/** 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. @@ -32,8 +32,8 @@ public interface Payload extends ReferenceCounted { boolean hasMetadata(); /** - * Returns the Payload metadata. Always non-null, check {@link #hasMetadata()} to differentiate - * null from "". + * Returns a slice Payload metadata. Always non-null, check {@link #hasMetadata()} to + * differentiate null from "". * * @return payload metadata. */ @@ -46,6 +46,22 @@ public interface Payload extends ReferenceCounted { */ 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(); diff --git a/rsocket-core/src/main/java/io/rsocket/RSocket.java b/rsocket-core/src/main/java/io/rsocket/RSocket.java index 5468b4de8..b05241365 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocket.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -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 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 44460a938..000000000 --- a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java +++ /dev/null @@ -1,544 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import io.rsocket.exceptions.ConnectionErrorException; -import io.rsocket.exceptions.Exceptions; -import io.rsocket.framing.FrameType; -import io.rsocket.internal.LimitableRequestPublisher; -import io.rsocket.internal.UnboundedProcessor; -import java.time.Duration; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import java.util.function.Function; -import org.jctools.maps.NonBlockingHashMapLong; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import reactor.core.publisher.*; - -/** Client Side of a RSocket socket. Sends {@link Frame}s to a {@link RSocketServer} */ -class RSocketClient implements RSocket { - - private final DuplexConnection connection; - private final Function frameDecoder; - private final Consumer errorConsumer; - private final StreamIdSupplier streamIdSupplier; - private final NonBlockingHashMapLong senders; - private final NonBlockingHashMapLong> receivers; - private final UnboundedProcessor sendProcessor; - private KeepAliveHandler keepAliveHandler; - private final Lifecycle lifecycle = new Lifecycle(); - - /*server requester*/ - RSocketClient( - DuplexConnection connection, - Function frameDecoder, - Consumer errorConsumer, - StreamIdSupplier streamIdSupplier) { - this( - connection, frameDecoder, errorConsumer, streamIdSupplier, Duration.ZERO, Duration.ZERO, 0); - } - - /*client requester*/ - RSocketClient( - DuplexConnection connection, - Function frameDecoder, - Consumer errorConsumer, - StreamIdSupplier streamIdSupplier, - Duration tickPeriod, - Duration ackTimeout, - int missedAcks) { - this.connection = connection; - this.frameDecoder = frameDecoder; - this.errorConsumer = errorConsumer; - this.streamIdSupplier = streamIdSupplier; - this.senders = new NonBlockingHashMapLong<>(256); - this.receivers = new NonBlockingHashMapLong<>(256); - - // DO NOT Change the order here. The Send processor must be subscribed to before receiving - this.sendProcessor = new UnboundedProcessor<>(); - - connection.onClose().doFinally(signalType -> cleanup()).subscribe(null, errorConsumer); - - connection - .send(sendProcessor) - .doFinally(this::handleSendProcessorCancel) - .subscribe(null, this::handleSendProcessorError); - - connection.receive().subscribe(this::handleIncomingFrames, errorConsumer); - - if (!Duration.ZERO.equals(tickPeriod)) { - this.keepAliveHandler = - KeepAliveHandler.ofClient( - new KeepAliveHandler.KeepAlive(tickPeriod, ackTimeout, missedAcks)); - - keepAliveHandler - .timeout() - .subscribe( - keepAlive -> { - String message = - String.format("No keep-alive acks for %d ms", keepAlive.getTimeoutMillis()); - errorConsumer.accept(new ConnectionErrorException(message)); - connection.dispose(); - }); - keepAliveHandler.send().subscribe(sendProcessor::onNext); - } else { - keepAliveHandler = null; - } - } - - private void handleSendProcessorError(Throwable t) { - Throwable terminationError = lifecycle.terminationError(); - Throwable err = terminationError != null ? terminationError : t; - for (Subscriber subscriber : receivers.values()) { - try { - subscriber.onError(err); - } catch (Throwable e) { - errorConsumer.accept(e); - } - } - - for (LimitableRequestPublisher p : senders.values()) { - p.cancel(); - } - } - - private void handleSendProcessorCancel(SignalType t) { - if (SignalType.ON_ERROR == t) { - return; - } - - for (Subscriber subscriber : receivers.values()) { - try { - subscriber.onError(new Throwable("closed connection")); - } catch (Throwable e) { - errorConsumer.accept(e); - } - } - - for (LimitableRequestPublisher p : senders.values()) { - p.cancel(); - } - } - - @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 Mono.fromRunnable( - () -> { - final Frame requestFrame = Frame.Request.from(0, FrameType.METADATA_PUSH, payload, 1); - payload.release(); - sendProcessor.onNext(requestFrame); - }); - } - - @Override - public double availability() { - return connection.availability(); - } - - @Override - public void dispose() { - connection.dispose(); - } - - @Override - public boolean isDisposed() { - return connection.isDisposed(); - } - - @Override - public Mono onClose() { - return connection.onClose(); - } - - private Mono handleFireAndForget(Payload payload) { - return lifecycle - .started() - .then( - Mono.fromRunnable( - () -> { - final int streamId = streamIdSupplier.nextStreamId(); - final Frame requestFrame = - Frame.Request.from(streamId, FrameType.REQUEST_FNF, payload, 1); - payload.release(); - sendProcessor.onNext(requestFrame); - })); - } - - private Flux handleRequestStream(final Payload payload) { - return lifecycle - .started() - .thenMany( - Flux.defer( - () -> { - int streamId = streamIdSupplier.nextStreamId(); - - UnicastProcessor receiver = UnicastProcessor.create(); - receivers.put(streamId, receiver); - - AtomicBoolean first = new AtomicBoolean(false); - - return receiver - .doOnRequest( - n -> { - if (first.compareAndSet(false, true) && !receiver.isDisposed()) { - final Frame requestFrame = - Frame.Request.from( - streamId, FrameType.REQUEST_STREAM, payload, n); - payload.release(); - sendProcessor.onNext(requestFrame); - } else if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext(Frame.RequestN.from(streamId, n)); - } - sendProcessor.drain(); - }) - .doOnError( - t -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext(Frame.Error.from(streamId, t)); - } - }) - .doOnCancel( - () -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext(Frame.Cancel.from(streamId)); - } - }) - .doFinally( - s -> { - receivers.remove(streamId); - }); - })); - } - - private Mono handleRequestResponse(final Payload payload) { - return lifecycle - .started() - .then( - Mono.defer( - () -> { - int streamId = streamIdSupplier.nextStreamId(); - final Frame requestFrame = - Frame.Request.from(streamId, FrameType.REQUEST_RESPONSE, payload, 1); - payload.release(); - - UnicastProcessor receiver = UnicastProcessor.create(); - receivers.put(streamId, receiver); - - sendProcessor.onNext(requestFrame); - - return receiver - .singleOrEmpty() - .doOnError(t -> sendProcessor.onNext(Frame.Error.from(streamId, t))) - .doOnCancel(() -> sendProcessor.onNext(Frame.Cancel.from(streamId))) - .doFinally( - s -> { - receivers.remove(streamId); - }); - })); - } - - private Flux handleChannel(Flux request) { - return lifecycle - .started() - .thenMany( - Flux.defer( - () -> { - final UnicastProcessor receiver = UnicastProcessor.create(); - final int streamId = streamIdSupplier.nextStreamId(); - final AtomicBoolean firstRequest = new AtomicBoolean(true); - - return receiver - .doOnRequest( - n -> { - if (firstRequest.compareAndSet(true, false)) { - final AtomicBoolean firstPayload = new AtomicBoolean(true); - final Flux requestFrames = - request - .transform( - f -> { - LimitableRequestPublisher wrapped = - LimitableRequestPublisher.wrap(f); - // Need to set this to one for first the frame - wrapped.increaseRequestLimit(1); - senders.put(streamId, wrapped); - receivers.put(streamId, receiver); - - return wrapped; - }) - .map( - payload -> { - final Frame requestFrame; - if (firstPayload.compareAndSet(true, false)) { - requestFrame = - Frame.Request.from( - streamId, - FrameType.REQUEST_CHANNEL, - payload, - n); - } else { - requestFrame = - Frame.PayloadFrame.from( - streamId, FrameType.NEXT, payload); - } - payload.release(); - return requestFrame; - }) - .doOnComplete( - () -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext( - Frame.PayloadFrame.from( - streamId, FrameType.COMPLETE)); - } - if (firstPayload.get()) { - receiver.onComplete(); - } - }); - - requestFrames.subscribe( - sendProcessor::onNext, - t -> { - errorConsumer.accept(t); - receiver.dispose(); - }); - } else { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext(Frame.RequestN.from(streamId, n)); - } - } - }) - .doOnError( - t -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext(Frame.Error.from(streamId, t)); - } - }) - .doOnCancel( - () -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext(Frame.Cancel.from(streamId)); - } - }) - .doFinally( - s -> { - receivers.remove(streamId); - LimitableRequestPublisher sender = senders.remove(streamId); - if (sender != null) { - sender.cancel(); - } - }); - })); - } - - private boolean contains(int streamId) { - return receivers.containsKey(streamId); - } - - protected void cleanup() { - if (keepAliveHandler != null) { - keepAliveHandler.dispose(); - } - try { - for (UnicastProcessor subscriber : receivers.values()) { - cleanUpSubscriber(subscriber); - } - for (LimitableRequestPublisher p : senders.values()) { - cleanUpLimitableRequestPublisher(p); - } - } finally { - senders.clear(); - receivers.clear(); - sendProcessor.dispose(); - } - } - - private synchronized void cleanUpLimitableRequestPublisher( - LimitableRequestPublisher limitableRequestPublisher) { - try { - limitableRequestPublisher.cancel(); - } catch (Throwable t) { - errorConsumer.accept(t); - } - } - - private synchronized void cleanUpSubscriber(UnicastProcessor subscriber) { - Throwable err = lifecycle.terminationError(); - try { - if (err != null) { - subscriber.onError(err); - } else { - subscriber.cancel(); - } - } catch (Throwable t) { - errorConsumer.accept(t); - } - } - - 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: - RuntimeException error = Exceptions.from(frame); - lifecycle.terminate(error); - errorConsumer.accept(error); - connection.dispose(); - break; - case LEASE: - break; - case KEEPALIVE: - if (keepAliveHandler != null) { - keepAliveHandler.receive(frame); - } - 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())); - } - } - - private void handleFrame(int streamId, FrameType type, Frame frame) { - Subscriber receiver = receivers.get(streamId); - if (receiver == null) { - handleMissingResponseProcessor(streamId, type, frame); - } else { - switch (type) { - case ERROR: - receiver.onError(Exceptions.from(frame)); - receivers.remove(streamId); - break; - case NEXT_COMPLETE: - receiver.onNext(frameDecoder.apply(frame)); - receiver.onComplete(); - break; - case CANCEL: - { - LimitableRequestPublisher sender = senders.remove(streamId); - receivers.remove(streamId); - if (sender != null) { - sender.cancel(); - } - break; - } - case NEXT: - receiver.onNext(frameDecoder.apply(frame)); - break; - case REQUEST_N: - { - LimitableRequestPublisher sender = senders.get(streamId); - if (sender != null) { - int n = Frame.RequestN.requestN(frame); - sender.increaseRequestLimit(n); - sendProcessor.drain(); - } - break; - } - case COMPLETE: - receiver.onComplete(); - 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 = frame.getDataUtf8(); - - 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 static class Lifecycle { - - private volatile Throwable terminationError; - - public Mono started() { - return Mono.create( - sink -> { - Throwable err = terminationError; - if (err == null) { - sink.success(); - } else { - sink.error(err); - } - }); - } - - public void terminate(Throwable err) { - this.terminationError = err; - } - - public Throwable terminationError() { - return terminationError; - } - } -} 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 155170cf3..e23bcceb2 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -1,11 +1,11 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * 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,44 +13,62 @@ * 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.exceptions.RejectedSetupException; -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.DefaultPayload; -import io.rsocket.util.EmptyPayload; 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; + +/** + * 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 { -/** Factory for creating RSocket clients and servers. */ -public class RSocketFactory { /** - * Creates a factory that establishes client connections to other RSockets. + * Create a {@code ClientRSocketFactory} to connect to a remote RSocket endpoint. Internally + * delegates to {@link RSocketConnector}. * - * @return a client factory + * @return the {@code ClientRSocketFactory} instance */ public static ClientRSocketFactory connect() { return new ClientRSocketFactory(); } /** - * Creates a factory that receives server connections from client RSockets. + * Create a {@code ServerRSocketFactory} to accept connections from RSocket clients. Internally + * delegates to {@link RSocketServer}. * - * @return a server factory. + * @return the {@code ClientRSocketFactory} instance */ public static ServerRSocketFactory receive() { return new ServerRSocketFactory(); @@ -69,6 +87,9 @@ default Start transport(ClientTransport transport) { } public interface ServerTransportAcceptor { + + ServerTransport.ConnectionAcceptor toConnectionAcceptor(); + Start transport(Supplier> transport); default Start transport(ServerTransport transport) { @@ -76,312 +97,475 @@ default Start transport(ServerTransport transport) { } } + /** Factory to create and configure an RSocket client, and connect to a server. */ public static class ClientRSocketFactory implements ClientTransportAcceptor { - private Supplier> acceptor = - () -> rSocket -> new AbstractRSocket() {}; + private static final ClientResume CLIENT_RESUME = + new ClientResume(Duration.ofMinutes(2), Unpooled.EMPTY_BUFFER); - private Consumer errorConsumer = Throwable::printStackTrace; - private int mtu = 0; - private PluginRegistry plugins = new PluginRegistry(Plugins.defaultPlugins()); - private int flags = 0; - - private Payload setupPayload = EmptyPayload.INSTANCE; - private Function frameDecoder = DefaultPayload::create; + private final RSocketConnector connector; private Duration tickPeriod = Duration.ofSeconds(20); private Duration ackTimeout = Duration.ofSeconds(30); private int missedAcks = 3; - private String metadataMimeType = "application/binary"; - private String dataMimeType = "application/binary"; + private Resume resume; + + public ClientRSocketFactory() { + this(RSocketConnector.create()); + } + + public ClientRSocketFactory(RSocketConnector connector) { + this.connector = connector; + } + + /** + * @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; + } + + public ClientRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor) { + connector.interceptors(registry -> registry.forSocketAcceptor(interceptor)); return this; } /** - * Deprecated as Keep-Alive is not optional according to spec + * Deprecated without replacement as Keep-Alive is not optional according to spec * * @return this ClientRSocketFactory */ @Deprecated public ClientRSocketFactory keepAlive() { + connector.keepAlive(tickPeriod, ackTimeout.plus(tickPeriod.multipliedBy(missedAcks))); return this; } - public ClientRSocketFactory keepAlive( + public ClientTransportAcceptor keepAlive( Duration tickPeriod, Duration ackTimeout, int missedAcks) { this.tickPeriod = tickPeriod; this.ackTimeout = ackTimeout; this.missedAcks = missedAcks; + keepAlive(); return this; } public ClientRSocketFactory keepAliveTickPeriod(Duration tickPeriod) { this.tickPeriod = tickPeriod; + keepAlive(); return this; } public ClientRSocketFactory keepAliveAckTimeout(Duration ackTimeout) { this.ackTimeout = ackTimeout; + keepAlive(); return this; } public ClientRSocketFactory keepAliveMissedAcks(int missedAcks) { this.missedAcks = missedAcks; + keepAlive(); return this; } public ClientRSocketFactory mimeType(String metadataMimeType, String dataMimeType) { - this.dataMimeType = dataMimeType; - this.metadataMimeType = metadataMimeType; + connector.metadataMimeType(metadataMimeType); + connector.dataMimeType(dataMimeType); return this; } public ClientRSocketFactory dataMimeType(String dataMimeType) { - this.dataMimeType = dataMimeType; + connector.dataMimeType(dataMimeType); return this; } public ClientRSocketFactory metadataMimeType(String metadataMimeType) { - this.metadataMimeType = metadataMimeType; + connector.metadataMimeType(metadataMimeType); return this; } - @Override - public Start transport(Supplier transportClient) { - return new StartClient(transportClient); + public ClientRSocketFactory lease(Supplier> supplier) { + connector.lease(supplier); + return this; } - public ClientTransportAcceptor acceptor(Function acceptor) { - this.acceptor = () -> acceptor; - return StartClient::new; + public ClientRSocketFactory lease() { + connector.lease(Leases::new); + return this; } - public ClientTransportAcceptor acceptor(Supplier> acceptor) { - this.acceptor = acceptor; - return StartClient::new; + /** @deprecated without a replacement and no longer used. */ + @Deprecated + public ClientRSocketFactory singleSubscriberRequester() { + return this; } - public ClientRSocketFactory fragment(int mtu) { - this.mtu = mtu; + /** + * 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 errorConsumer(Consumer errorConsumer) { - this.errorConsumer = errorConsumer; + public ClientRSocketFactory resume() { + resume = resume != null ? resume : new Resume(); + connector.resume(resume); return this; } - public ClientRSocketFactory setupPayload(Payload payload) { - this.setupPayload = payload; + public ClientRSocketFactory resumeToken(Supplier supplier) { + resume(); + resume.token(supplier); return this; } - public ClientRSocketFactory frameDecoder(Function frameDecoder) { - this.frameDecoder = frameDecoder; + public ClientRSocketFactory resumeStore( + Function storeFactory) { + resume(); + resume.storeFactory(storeFactory); return this; } - private class StartClient implements Start { - private final Supplier transportClient; + public ClientRSocketFactory resumeSessionDuration(Duration sessionDuration) { + resume(); + resume.sessionDuration(sessionDuration); + return this; + } - StartClient(Supplier transportClient) { - this.transportClient = transportClient; - } + public ClientRSocketFactory resumeStreamTimeout(Duration streamTimeout) { + resume(); + resume.streamTimeout(streamTimeout); + return this; + } - @Override - public Mono start() { - return transportClient - .get() - .connect() - .flatMap( - connection -> { - Frame setupFrame = - Frame.Setup.from( - flags, - (int) tickPeriod.toMillis(), - (int) (ackTimeout.toMillis() + tickPeriod.toMillis() * missedAcks), - metadataMimeType, - dataMimeType, - setupPayload); + 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; + } - if (mtu > 0) { - connection = new FragmentationDuplexConnection(connection, mtu); - } + public Start transport(Supplier transport) { + return () -> connector.connect(transport); + } - ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(connection, plugins); + public ClientTransportAcceptor acceptor(Function acceptor) { + return acceptor(() -> acceptor); + } - RSocketClient rSocketClient = - new RSocketClient( - multiplexer.asClientConnection(), - frameDecoder, - errorConsumer, - StreamIdSupplier.clientSupplier(), - tickPeriod, - ackTimeout, - missedAcks); + public ClientTransportAcceptor acceptor(Supplier> acceptorSupplier) { + return acceptor( + (setup, sendingSocket) -> { + acceptorSupplier.get().apply(sendingSocket); + return Mono.empty(); + }); + } - RSocket wrappedRSocketClient = plugins.applyClient(rSocketClient); + public ClientTransportAcceptor acceptor(SocketAcceptor acceptor) { + connector.acceptor(acceptor); + return this; + } - RSocket unwrappedServerSocket = acceptor.get().apply(wrappedRSocketClient); + public ClientRSocketFactory fragment(int mtu) { + connector.fragment(mtu); + return this; + } - RSocket wrappedRSocketServer = plugins.applyServer(unwrappedServerSocket); + /** + * @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) { + return this; + } - RSocketServer rSocketServer = - new RSocketServer( - multiplexer.asServerConnection(), - wrappedRSocketServer, - frameDecoder, - errorConsumer); + public ClientRSocketFactory setupPayload(Payload payload) { + connector.setupPayload(payload); + return this; + } - return connection.sendOne(setupFrame).thenReturn(wrappedRSocketClient); - }); - } + public ClientRSocketFactory frameDecoder(PayloadDecoder payloadDecoder) { + connector.payloadDecoder(payloadDecoder); + return this; } } - public static class ServerRSocketFactory { - private SocketAcceptor acceptor; - private Function frameDecoder = DefaultPayload::create; - private Consumer errorConsumer = Throwable::printStackTrace; - private int mtu = 0; - private PluginRegistry plugins = new PluginRegistry(Plugins.defaultPlugins()); + /** Factory to create, configure, and start an RSocket server. */ + public static class ServerRSocketFactory implements ServerTransportAcceptor { + private final RSocketServer server; - private ServerRSocketFactory() {} + private Resume resume; - public ServerRSocketFactory addConnectionPlugin(DuplexConnectionInterceptor interceptor) { - plugins.addConnectionPlugin(interceptor); + public ServerRSocketFactory() { + this(RSocketServer.create()); + } + + 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; + } + + public ServerRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor) { + server.interceptors(registry -> registry.forSocketAcceptor(interceptor)); return this; } public ServerTransportAcceptor acceptor(SocketAcceptor acceptor) { - this.acceptor = acceptor; - return ServerStart::new; + server.acceptor(acceptor); + return this; } - public ServerRSocketFactory frameDecoder(Function frameDecoder) { - this.frameDecoder = frameDecoder; + public ServerRSocketFactory frameDecoder(PayloadDecoder payloadDecoder) { + server.payloadDecoder(payloadDecoder); return this; } public ServerRSocketFactory fragment(int mtu) { - this.mtu = mtu; + server.fragment(mtu); return this; } + /** + * @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 ServerStart implements Start { - private final Supplier> transportServer; - - ServerStart(Supplier> transportServer) { - this.transportServer = transportServer; - } - - @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) { - setupFrame.release(); - InvalidSetupException error = - new InvalidSetupException( - "Unsupported version " + VersionFlyweight.toString(version)); - return multiplexer - .asStreamZeroConnection() - .sendOne(Frame.Error.from(0, error)) - .doFinally(signalType -> multiplexer.dispose()); - } - - ConnectionSetupPayload setupPayload = ConnectionSetupPayload.create(setupFrame); - int keepAliveInterval = setupPayload.keepAliveInterval(); - int keepAliveMaxLifetime = setupPayload.keepAliveMaxLifetime(); - - RSocketClient rSocketClient = - new RSocketClient( - multiplexer.asServerConnection(), - frameDecoder, - errorConsumer, - StreamIdSupplier.serverSupplier()); - - RSocket wrappedRSocketClient = plugins.applyClient(rSocketClient); - - return acceptor - .accept(setupPayload, wrappedRSocketClient) - .onErrorResume( - err -> - multiplexer - .asStreamZeroConnection() - .sendOne(rejectedSetupErrorFrame(err)) - .then(Mono.error(err))) - .doOnNext( - unwrappedServerSocket -> { - RSocket wrappedRSocketServer = plugins.applyServer(unwrappedServerSocket); - - RSocketServer rSocketServer = - new RSocketServer( - multiplexer.asClientConnection(), - wrappedRSocketServer, - frameDecoder, - errorConsumer, - keepAliveInterval, - keepAliveMaxLifetime); - }) - .doFinally(signalType -> setupPayload.release()) - .then(); - } - - private Frame rejectedSetupErrorFrame(Throwable err) { - String msg = err.getMessage(); - return Frame.Error.from( - 0, new RejectedSetupException(msg == null ? "rejected by server acceptor" : msg)); - } + 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 95933c6e2..000000000 --- a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java +++ /dev/null @@ -1,406 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import static io.rsocket.Frame.Request.initialRequestN; -import static io.rsocket.frame.FrameHeaderFlyweight.FLAGS_C; -import static io.rsocket.frame.FrameHeaderFlyweight.FLAGS_M; - -import io.rsocket.exceptions.ApplicationErrorException; -import io.rsocket.exceptions.ConnectionErrorException; -import io.rsocket.framing.FrameType; -import io.rsocket.internal.LimitableRequestPublisher; -import io.rsocket.internal.UnboundedProcessor; -import java.util.function.Consumer; -import java.util.function.Function; -import org.jctools.maps.NonBlockingHashMapLong; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.Disposable; -import reactor.core.publisher.*; - -/** 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 Function frameDecoder; - private final Consumer errorConsumer; - - private final NonBlockingHashMapLong sendingSubscriptions; - private final NonBlockingHashMapLong> channelProcessors; - - private final UnboundedProcessor sendProcessor; - private KeepAliveHandler keepAliveHandler; - - /*client responder*/ - RSocketServer( - DuplexConnection connection, - RSocket requestHandler, - Function frameDecoder, - Consumer errorConsumer) { - this(connection, requestHandler, frameDecoder, errorConsumer, 0, 0); - } - - /*server responder*/ - RSocketServer( - DuplexConnection connection, - RSocket requestHandler, - Function frameDecoder, - Consumer errorConsumer, - long tickPeriod, - long ackTimeout) { - this.connection = connection; - this.requestHandler = requestHandler; - this.frameDecoder = frameDecoder; - this.errorConsumer = errorConsumer; - this.sendingSubscriptions = new NonBlockingHashMapLong<>(); - this.channelProcessors = new NonBlockingHashMapLong<>(); - - // DO NOT Change the order here. The Send processor must be subscribed to before receiving - // connections - this.sendProcessor = new UnboundedProcessor<>(); - - connection - .send(sendProcessor) - .doFinally(this::handleSendProcessorCancel) - .subscribe(null, this::handleSendProcessorError); - - Disposable receiveDisposable = connection.receive().subscribe(this::handleFrame, errorConsumer); - - this.connection - .onClose() - .doFinally( - s -> { - cleanup(); - receiveDisposable.dispose(); - }) - .subscribe(null, errorConsumer); - - if (tickPeriod != 0) { - keepAliveHandler = - KeepAliveHandler.ofServer(new KeepAliveHandler.KeepAlive(tickPeriod, ackTimeout)); - - keepAliveHandler - .timeout() - .subscribe( - keepAlive -> { - String message = - String.format("No keep-alive acks for %d ms", keepAlive.getTimeoutMillis()); - errorConsumer.accept(new ConnectionErrorException(message)); - connection.dispose(); - }); - keepAliveHandler.send().subscribe(sendProcessor::onNext); - } else { - keepAliveHandler = null; - } - } - - private void handleSendProcessorError(Throwable t) { - for (Subscription subscription : sendingSubscriptions.values()) { - try { - subscription.cancel(); - } catch (Throwable e) { - errorConsumer.accept(e); - } - } - - for (UnicastProcessor subscription : channelProcessors.values()) { - try { - subscription.cancel(); - } catch (Throwable e) { - errorConsumer.accept(e); - } - } - } - - private void handleSendProcessorCancel(SignalType t) { - if (SignalType.ON_ERROR == t) { - return; - } - - for (Subscription subscription : sendingSubscriptions.values()) { - try { - subscription.cancel(); - } catch (Throwable e) { - errorConsumer.accept(e); - } - } - - for (UnicastProcessor subscription : channelProcessors.values()) { - try { - subscription.cancel(); - } catch (Throwable e) { - errorConsumer.accept(e); - } - } - } - - @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 void dispose() { - connection.dispose(); - } - - @Override - public boolean isDisposed() { - return connection.isDisposed(); - } - - @Override - public Mono onClose() { - return connection.onClose(); - } - - private void cleanup() { - if (keepAliveHandler != null) { - keepAliveHandler.dispose(); - } - cleanUpSendingSubscriptions(); - cleanUpChannelProcessors(); - - requestHandler.dispose(); - sendProcessor.dispose(); - } - - private synchronized void cleanUpSendingSubscriptions() { - sendingSubscriptions.values().forEach(Subscription::cancel); - sendingSubscriptions.clear(); - } - - private synchronized void cleanUpChannelProcessors() { - channelProcessors.values().forEach(Subscription::cancel); - channelProcessors.clear(); - } - - private void handleFrame(Frame frame) { - try { - int streamId = frame.getStreamId(); - Subscriber receiver; - switch (frame.getType()) { - case REQUEST_FNF: - handleFireAndForget(streamId, fireAndForget(frameDecoder.apply(frame))); - break; - case REQUEST_RESPONSE: - handleRequestResponse(streamId, requestResponse(frameDecoder.apply(frame))); - break; - case CANCEL: - handleCancelFrame(streamId); - break; - case KEEPALIVE: - handleKeepAliveFrame(frame); - break; - case REQUEST_N: - handleRequestN(streamId, frame); - break; - case REQUEST_STREAM: - handleStream(streamId, requestStream(frameDecoder.apply(frame)), initialRequestN(frame)); - break; - case REQUEST_CHANNEL: - handleChannel(streamId, frameDecoder.apply(frame), initialRequestN(frame)); - break; - case METADATA_PUSH: - metadataPush(frameDecoder.apply(frame)); - break; - case PAYLOAD: - // TODO: Hook in receiving socket. - break; - case LEASE: - // Lease must not be received here as this is the server end of the socket which sends - // leases. - break; - case NEXT: - receiver = channelProcessors.get(streamId); - if (receiver != null) { - receiver.onNext(frameDecoder.apply(frame)); - } - break; - case COMPLETE: - receiver = channelProcessors.get(streamId); - if (receiver != null) { - receiver.onComplete(); - } - break; - case ERROR: - receiver = channelProcessors.get(streamId); - if (receiver != null) { - receiver.onError(new ApplicationErrorException(Frame.Error.message(frame))); - } - break; - case NEXT_COMPLETE: - receiver = channelProcessors.get(streamId); - if (receiver != null) { - receiver.onNext(frameDecoder.apply(frame)); - receiver.onComplete(); - } - break; - case SETUP: - handleError(streamId, new IllegalStateException("Setup frame received post setup.")); - break; - default: - handleError( - streamId, - new IllegalStateException( - "ServerRSocket: Unexpected frame type: " + frame.getType())); - break; - } - } finally { - frame.release(); - } - } - - private void handleFireAndForget(int streamId, Mono result) { - result - .doOnSubscribe(subscription -> sendingSubscriptions.put(streamId, subscription)) - .doFinally(signalType -> sendingSubscriptions.remove(streamId)) - .subscribe(null, errorConsumer); - } - - private void handleRequestResponse(int streamId, Mono response) { - response - .doOnSubscribe(subscription -> sendingSubscriptions.put(streamId, subscription)) - .map( - payload -> { - int flags = FLAGS_C; - if (payload.hasMetadata()) { - flags = Frame.setFlag(flags, FLAGS_M); - } - final Frame frame = - Frame.PayloadFrame.from(streamId, FrameType.NEXT_COMPLETE, payload, flags); - payload.release(); - return frame; - }) - .switchIfEmpty( - Mono.fromCallable(() -> Frame.PayloadFrame.from(streamId, FrameType.COMPLETE))) - .doFinally(signalType -> sendingSubscriptions.remove(streamId)) - .subscribe(sendProcessor::onNext, t -> handleError(streamId, t)); - } - - private void handleStream(int streamId, Flux response, int initialRequestN) { - response - .transform( - frameFlux -> { - LimitableRequestPublisher payloads = - LimitableRequestPublisher.wrap(frameFlux); - sendingSubscriptions.put(streamId, payloads); - payloads.increaseRequestLimit(initialRequestN); - return payloads; - }) - .doFinally(signalType -> sendingSubscriptions.remove(streamId)) - .subscribe( - payload -> { - final Frame frame = Frame.PayloadFrame.from(streamId, FrameType.NEXT, payload); - payload.release(); - sendProcessor.onNext(frame); - }, - t -> handleError(streamId, t), - () -> { - final Frame frame = Frame.PayloadFrame.from(streamId, FrameType.COMPLETE); - sendProcessor.onNext(frame); - }); - } - - private void handleChannel(int streamId, Payload payload, int initialRequestN) { - UnicastProcessor frames = UnicastProcessor.create(); - channelProcessors.put(streamId, frames); - - Flux payloads = - frames - .doOnCancel(() -> sendProcessor.onNext(Frame.Cancel.from(streamId))) - .doOnError(t -> sendProcessor.onNext(Frame.Error.from(streamId, t))) - .doOnRequest(l -> sendProcessor.onNext(Frame.RequestN.from(streamId, l))) - .doFinally(signalType -> channelProcessors.remove(streamId)); - - // 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); - - handleStream(streamId, requestChannel(payloads), initialRequestN); - } - - private void handleKeepAliveFrame(Frame frame) { - if (keepAliveHandler != null) { - keepAliveHandler.receive(frame); - } - } - - private void handleCancelFrame(int streamId) { - Subscription subscription = sendingSubscriptions.remove(streamId); - if (subscription != null) { - subscription.cancel(); - } - } - - private void handleError(int streamId, Throwable t) { - errorConsumer.accept(t); - sendProcessor.onNext(Frame.Error.from(streamId, t)); - } - - private void handleRequestN(int streamId, Frame frame) { - final Subscription subscription = sendingSubscriptions.get(streamId); - if (subscription != null) { - int n = Frame.RequestN.requestN(frame); - subscription.request(n >= Integer.MAX_VALUE ? Long.MAX_VALUE : n); - } - } -} 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 0f6b99d0e..a42626e78 100644 --- a/rsocket-core/src/main/java/io/rsocket/SocketAcceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/SocketAcceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,24 +17,77 @@ 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/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/StreamIdSupplier.java b/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java similarity index 51% rename from rsocket-core/src/main/java/io/rsocket/StreamIdSupplier.java rename to rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java index 383d1a2c9..15d39c993 100644 --- a/rsocket-core/src/main/java/io/rsocket/StreamIdSupplier.java +++ b/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,26 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package io.rsocket.core; -package io.rsocket; +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 int streamId; + private long streamId; - private StreamIdSupplier(int streamId) { + // Visible for testing + 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); } @@ -40,4 +35,24 @@ static StreamIdSupplier clientSupplier() { 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 index d69012de3..cd0d46754 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.framing.ErrorType; +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; /** * Application layer logic generating a Reactive Streams {@code onError} event. @@ -32,10 +33,9 @@ public final class ApplicationErrorException extends RSocketException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public ApplicationErrorException(String message) { - super(message); + this(message, null); } /** @@ -43,14 +43,8 @@ public ApplicationErrorException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public ApplicationErrorException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.APPLICATION_ERROR; + public ApplicationErrorException(String message, @Nullable Throwable cause) { + super(ErrorFrameCodec.APPLICATION_ERROR, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java index ba7d97e0b..d51ba0fb7 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.framing.ErrorType; +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 @@ -33,10 +34,9 @@ public final class CanceledException extends RSocketException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public CanceledException(String message) { - super(message); + this(message, null); } /** @@ -44,14 +44,8 @@ public CanceledException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public CanceledException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.CANCELED; + 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 index 49828ea9b..80324aa90 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.framing.ErrorType; +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 @@ -33,10 +34,9 @@ public final class ConnectionCloseException extends RSocketException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public ConnectionCloseException(String message) { - super(message); + this(message, null); } /** @@ -44,14 +44,8 @@ public ConnectionCloseException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public ConnectionCloseException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.CONNECTION_CLOSE; + 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 index a49b12ed1..b44714f7e 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.framing.ErrorType; +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 @@ -33,10 +34,9 @@ public final class ConnectionErrorException extends RSocketException implements * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public ConnectionErrorException(String message) { - super(message); + this(message, null); } /** @@ -44,14 +44,8 @@ public ConnectionErrorException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public ConnectionErrorException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.CONNECTION_ERROR; + 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 18e956664..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,22 @@ package io.rsocket.exceptions; -import static io.rsocket.frame.ErrorFrameFlyweight.APPLICATION_ERROR; -import static io.rsocket.frame.ErrorFrameFlyweight.CANCELED; -import static io.rsocket.frame.ErrorFrameFlyweight.CONNECTION_CLOSE; -import static io.rsocket.frame.ErrorFrameFlyweight.CONNECTION_ERROR; -import static io.rsocket.frame.ErrorFrameFlyweight.INVALID; -import static io.rsocket.frame.ErrorFrameFlyweight.INVALID_SETUP; -import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED; -import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED_RESUME; -import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED_SETUP; -import static io.rsocket.frame.ErrorFrameFlyweight.UNSUPPORTED_SETUP; +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.netty.buffer.ByteBuf; +import io.rsocket.RSocketErrorException; +import io.rsocket.frame.ErrorFrameCodec; import java.util.Objects; /** Utility class that generates an exception from a frame. */ @@ -36,42 +40,56 @@ public final class Exceptions { private Exceptions() {} /** - * Create a {@link RSocketException} from a {@link Frame} that matches the error code it contains. + * 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 RSocketException} that matches the error code in the {@link Frame} + * @return a {@link RSocketErrorException} that matches the error code in the Frame * @throws NullPointerException if {@code frame} is {@code null} */ - public static RuntimeException from(Frame frame) { + public static RuntimeException from(int streamId, ByteBuf frame) { Objects.requireNonNull(frame, "frame must not be null"); - int errorCode = Frame.Error.errorCode(frame); - String message = frame.getDataUtf8(); + int errorCode = ErrorFrameCodec.errorCode(frame); + String message = ErrorFrameCodec.dataUtf8(frame); - switch (errorCode) { - case APPLICATION_ERROR: - return new ApplicationErrorException(message); - case CANCELED: - return new CanceledException(message); - case CONNECTION_CLOSE: - return new ConnectionCloseException(message); - case CONNECTION_ERROR: - return new ConnectionErrorException(message); - case INVALID: - return new InvalidException(message); - case INVALID_SETUP: - return new InvalidSetupException(message); - case REJECTED: - return new RejectedException(message); - case REJECTED_RESUME: - return new RejectedResumeException(message); - case REJECTED_SETUP: - return new RejectedSetupException(message); - case UNSUPPORTED_SETUP: - return new UnsupportedSetupException(message); - default: - return new IllegalArgumentException( - String.format("Invalid Error frame: %d '%s'", errorCode, message)); + 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)); + } } } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java index 04c33b7bc..a1b77b8dd 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.framing.ErrorType; +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; /** * The request is invalid. @@ -32,10 +33,9 @@ public final class InvalidException extends RSocketException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public InvalidException(String message) { - super(message); + this(message, null); } /** @@ -43,14 +43,8 @@ public InvalidException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public InvalidException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.INVALID; + 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 c1c7766af..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.framing.ErrorType; +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 @@ -33,10 +34,9 @@ public final class InvalidSetupException extends SetupException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public InvalidSetupException(String message) { - super(message); + this(message, null); } /** @@ -44,14 +44,8 @@ public InvalidSetupException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public InvalidSetupException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.INVALID_SETUP; + 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 index 7508a1ee3..2b137282f 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RSocketException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RSocketException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,41 +16,48 @@ package io.rsocket.exceptions; -import java.util.Objects; +import io.rsocket.RSocketErrorException; +import io.rsocket.frame.ErrorFrameCodec; import reactor.util.annotation.Nullable; -/** The root of the RSocket exception hierarchy. */ -public abstract class RSocketException extends RuntimeException { +/** + * 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. + * Constructs a new exception with the specified message and error code 0x201 (Application error). * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public RSocketException(String message) { - super(Objects.requireNonNull(message, "message must not be null")); + this(message, null); } /** - * Constructs a new exception with the specified message and cause. + * 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 - * @throws NullPointerException if {@code message} is {@code null} */ public RSocketException(String message, @Nullable Throwable cause) { - super(Objects.requireNonNull(message, "message must not be null"), cause); + super(ErrorFrameCodec.APPLICATION_ERROR, message, cause); } /** - * Returns the RSocket error code - * represented by this exception + * Constructs a new exception with the specified error code, message and cause. * - * @return the RSocket error code + * @param errorCode the RSocket protocol error code + * @param message the message + * @param cause the cause of this exception */ - public abstract int errorCode(); + 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 3f698288b..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.framing.ErrorType; +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 @@ -26,7 +27,7 @@ * @see Error * Codes */ -public final class RejectedException extends RSocketException implements Retryable { +public class RejectedException extends RSocketException implements Retryable { private static final long serialVersionUID = 3926231092835143715L; @@ -34,10 +35,9 @@ public final class RejectedException extends RSocketException implements Retryab * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public RejectedException(String message) { - super(message); + this(message, null); } /** @@ -45,14 +45,8 @@ public RejectedException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public RejectedException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.REJECTED; + 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 index 0c84fa365..8a99fcffb 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.framing.ErrorType; +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; /** * The server rejected the resume, it can specify the reason in the payload. @@ -32,10 +33,9 @@ public final class RejectedResumeException extends RSocketException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public RejectedResumeException(String message) { - super(message); + this(message, null); } /** @@ -43,14 +43,8 @@ public RejectedResumeException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public RejectedResumeException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.REJECTED_RESUME; + 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 e3aeababb..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.framing.ErrorType; +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; /** * The server rejected the setup, it can specify the reason in the payload. @@ -32,10 +33,9 @@ public final class RejectedSetupException extends SetupException implements Retr * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public RejectedSetupException(String message) { - super(message); + this(message, null); } /** @@ -43,14 +43,8 @@ public RejectedSetupException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public RejectedSetupException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.REJECTED_SETUP; + public RejectedSetupException(String message, @Nullable Throwable cause) { + super(ErrorFrameCodec.REJECTED_SETUP, message, cause); } } 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 2111a51b1..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package io.rsocket.exceptions; +import io.rsocket.frame.ErrorFrameCodec; +import reactor.util.annotation.Nullable; + /** The root of the setup exception hierarchy. */ public abstract class SetupException extends RSocketException { @@ -25,10 +28,11 @@ public abstract class SetupException extends RSocketException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} + * @deprecated please use {@link #SetupException(int, String, Throwable)} */ + @Deprecated public SetupException(String message) { - super(message); + this(message, null); } /** @@ -36,9 +40,21 @@ public SetupException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} + * @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(String message, Throwable cause) { - super(message, cause); + 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 96017a7ce..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.framing.ErrorType; +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. @@ -32,10 +33,9 @@ public final class UnsupportedSetupException extends SetupException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public UnsupportedSetupException(String message) { - super(message); + this(message, null); } /** @@ -43,14 +43,8 @@ public UnsupportedSetupException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public UnsupportedSetupException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.UNSUPPORTED_SETUP; + 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 babf8194e..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ */ /** - * The hierarchy of exceptions that can be returned by the API + * A hierarchy of exceptions that represent RSocket protocol error codes. * * @see Error * Codes 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 e70f45d9f..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,129 +16,113 @@ package io.rsocket.fragmentation; -import static io.rsocket.fragmentation.FrameReassembler.createFrameReassembler; -import static io.rsocket.util.AbstractionLeakingFrameUtils.toAbstractionLeakingFrame; +import static io.rsocket.fragmentation.FrameFragmenter.fragmentFrame; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.PooledByteBufAllocator; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import io.rsocket.DuplexConnection; -import io.rsocket.Frame; -import io.rsocket.util.AbstractionLeakingFrameUtils; -import io.rsocket.util.NumberUtils; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; import java.util.Objects; -import org.jctools.maps.NonBlockingHashMapLong; import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** - * A {@link DuplexConnection} implementation that fragments and reassembles {@link Frame}s. + * A {@link DuplexConnection} implementation that fragments and reassembles {@link ByteBuf}s. * * @see Fragmentation * and Reassembly */ -public final class FragmentationDuplexConnection implements DuplexConnection { +public final class FragmentationDuplexConnection extends ReassemblyDuplexConnection + implements DuplexConnection { - private final ByteBufAllocator byteBufAllocator; + public static final int MIN_MTU_SIZE = 64; - private final DuplexConnection delegate; + private static final Logger logger = LoggerFactory.getLogger(FragmentationDuplexConnection.class); - private final FrameFragmenter frameFragmenter; - - private final NonBlockingHashMapLong frameReassemblers = - new NonBlockingHashMapLong<>(); - - /** - * Creates a new instance. - * - * @param delegate the {@link DuplexConnection} to decorate - * @param maxFragmentSize the maximum fragment size - * @throws NullPointerException if {@code delegate} is {@code null} - * @throws IllegalArgumentException if {@code maxFragmentSize} is not {@code positive} - */ - // TODO: Remove once ByteBufAllocators are shared - public FragmentationDuplexConnection(DuplexConnection delegate, int maxFragmentSize) { - this(PooledByteBufAllocator.DEFAULT, delegate, maxFragmentSize); - } + final DuplexConnection delegate; + final int mtu; + final String type; /** - * Creates a new instance. + * Class constructor. * - * @param byteBufAllocator the {@link ByteBufAllocator} to use - * @param delegate the {@link DuplexConnection} to decorate - * @param maxFragmentSize the maximum fragment size. A value of 0 indicates that frames should not - * be fragmented. - * @throws NullPointerException if {@code byteBufAllocator} or {@code delegate} are {@code null} - * @throws IllegalArgumentException if {@code maxFragmentSize} is not {@code positive} + * @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( - ByteBufAllocator byteBufAllocator, DuplexConnection delegate, int maxFragmentSize) { + DuplexConnection delegate, int mtu, int maxInboundPayloadSize, String type) { + super(delegate, maxInboundPayloadSize); - this.byteBufAllocator = - Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); - this.delegate = Objects.requireNonNull(delegate, "delegate must not be null"); - - NumberUtils.requireNonNegative(maxFragmentSize, "maxFragmentSize must be positive"); - - this.frameFragmenter = new FrameFragmenter(byteBufAllocator, maxFragmentSize); - - delegate - .onClose() - .doFinally(signalType -> frameReassemblers.values().forEach(FrameReassembler::dispose)) - .subscribe(); + Objects.requireNonNull(delegate, "delegate must not be null"); + this.delegate = delegate; + this.mtu = assertMtu(mtu); + this.type = type; } - @Override - public double availability() { - return delegate.availability(); + private boolean shouldFragment(FrameType frameType, int readableBytes) { + return frameType.isFragmentable() && readableBytes > mtu; } - @Override - public void dispose() { - delegate.dispose(); + 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 mtu; + } } @Override - public boolean isDisposed() { - return delegate.isDisposed(); - } - - @Override - public Mono onClose() { - return delegate.onClose(); - } - - @Override - public Flux receive() { - return delegate - .receive() - .map(AbstractionLeakingFrameUtils::fromAbstractionLeakingFrame) - .concatMap(t2 -> toReassembledFrames(t2.getT1(), t2.getT2())); - } - - @Override - public Mono send(Publisher frames) { - Objects.requireNonNull(frames, "frames must not be null"); - + public Mono send(Publisher frames) { return delegate.send( Flux.from(frames) - .map(AbstractionLeakingFrameUtils::fromAbstractionLeakingFrame) - .concatMap(t2 -> toFragmentedFrames(t2.getT1(), t2.getT2()))); + .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))); + })); } - private Flux toFragmentedFrames(int streamId, io.rsocket.framing.Frame frame) { - return this.frameFragmenter - .fragment(frame) - .map(fragment -> toAbstractionLeakingFrame(byteBufAllocator, streamId, fragment)); + @Override + 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); } - private Mono toReassembledFrames(int streamId, io.rsocket.framing.Frame fragment) { - FrameReassembler frameReassembler = - frameReassemblers.computeIfAbsent( - (long) streamId, i -> createFrameReassembler(byteBufAllocator)); - - return Mono.justOrEmpty(frameReassembler.reassemble(fragment)) - .map(frame -> toAbstractionLeakingFrame(byteBufAllocator, streamId, frame)); + 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 a0c7911f9..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,21 +16,21 @@ package io.rsocket.fragmentation; -import static io.rsocket.framing.PayloadFrame.createPayloadFrame; -import static io.rsocket.util.DisposableUtils.disposeQuietly; -import static java.lang.Math.min; - import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.rsocket.framing.FragmentableFrame; -import io.rsocket.framing.Frame; -import java.util.Objects; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.Disposable; +import io.netty.buffer.Unpooled; +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; -import reactor.util.annotation.Nullable; /** * The implementation of the RSocket fragmentation behavior. @@ -40,164 +40,196 @@ * and Reassembly */ final class FrameFragmenter { - - private final ByteBufAllocator byteBufAllocator; - - private final Logger logger = LoggerFactory.getLogger(this.getClass()); - - private final int maxFragmentSize; - - /** - * Creates a new instance - * - * @param byteBufAllocator the {@link ByteBufAllocator} to use - * @param maxFragmentSize the maximum size of each fragment - */ - FrameFragmenter(ByteBufAllocator byteBufAllocator, int maxFragmentSize) { - this.byteBufAllocator = - Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); - this.maxFragmentSize = maxFragmentSize; - } - - /** - * Returns a {@link Flux} of fragments frames - * - * @param frame the {@link Frame} to fragment - * @return a {@link Flux} of fragment frames - * @throws NullPointerException if {@code frame} is {@code null} - */ - public Flux fragment(Frame frame) { - Objects.requireNonNull(frame, "frame must not be null"); - - if (!shouldFragment(frame)) { - logger.debug("Not fragmenting {}", frame); - return Flux.just(frame); - } - - logger.debug("Fragmenting {}", frame); + 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 FragmentationState((FragmentableFrame) frame), - this::generate, - FragmentationState::dispose); + new Consumer>() { + boolean first = true; + + @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); + } + + sink.next(byteBuf); + if (!metadata.isReadable() && !data.isReadable()) { + sink.complete(); + } + } + }) + .doFinally(signalType -> ReferenceCountUtil.safeRelease(frame)); } - private FragmentationState generate(FragmentationState state, SynchronousSink sink) { - int fragmentLength = maxFragmentSize; - - ByteBuf metadata; - if (state.hasReadableMetadata()) { - metadata = state.readMetadataFragment(fragmentLength); - fragmentLength -= metadata.readableBytes(); - } else { - metadata = null; + 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: } - if (state.hasReadableMetadata()) { - Frame fragment = state.createFrame(byteBufAllocator, false, metadata, null); - logger.debug("Fragment {}", fragment); - - sink.next(fragment); - return state; + 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 data; - data = state.hasReadableData() ? state.readDataFragment(fragmentLength) : null; - - if (state.hasReadableData()) { - Frame fragment = state.createFrame(byteBufAllocator, false, metadata, data); - logger.debug("Fragment {}", fragment); - - sink.next(fragment); - return state; + ByteBuf dataFragment = Unpooled.EMPTY_BUFFER; + if (remaining > 0 && data.isReadable()) { + int r = Math.min(remaining, data.readableBytes()); + dataFragment = data.readRetainedSlice(r); } - Frame fragment = state.createFrame(byteBufAllocator, true, metadata, data); - logger.debug("Final Fragment {}", fragment); - - sink.next(fragment); - sink.complete(); - return state; - } - - private int getFragmentableLength(FragmentableFrame fragmentableFrame) { - return fragmentableFrame.getMetadataLength().orElse(0) + fragmentableFrame.getDataLength(); - } - - private boolean shouldFragment(Frame frame) { - if (maxFragmentSize == 0 || !(frame instanceof FragmentableFrame)) { - return false; + 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: + return PayloadFrameCodec.encode( + allocator, streamId, true, true, true, metadataFragment, dataFragment); + case COMPLETE: + return PayloadFrameCodec.encode( + allocator, streamId, true, true, false, metadataFragment, dataFragment); + default: + throw new IllegalStateException("unsupported fragment type: " + frameType); } - - FragmentableFrame fragmentableFrame = (FragmentableFrame) frame; - return !fragmentableFrame.isFollowsFlagSet() - && getFragmentableLength(fragmentableFrame) > maxFragmentSize; } - static final class FragmentationState implements Disposable { - - private final FragmentableFrame frame; - - private int dataIndex = 0; - - private boolean initialFragmentCreated = false; - - private int metadataIndex = 0; - - FragmentationState(FragmentableFrame frame) { - this.frame = frame; - } - - @Override - public void dispose() { - disposeQuietly(frame); - } - - Frame createFrame( - ByteBufAllocator byteBufAllocator, - boolean complete, - @Nullable ByteBuf metadata, - @Nullable ByteBuf data) { - - if (initialFragmentCreated) { - return createPayloadFrame(byteBufAllocator, !complete, data == null, metadata, data); - } else { - initialFragmentCreated = true; - return frame.createFragment(byteBufAllocator, metadata, data); - } - } - - boolean hasReadableData() { - return frame.getDataLength() - dataIndex > 0; + static ByteBuf encodeFollowsFragment( + ByteBufAllocator allocator, int mtu, int streamId, ByteBuf metadata, ByteBuf data) { + // subtract the header bytes + int remaining = mtu - FrameHeaderCodec.size(); + + 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); } - boolean hasReadableMetadata() { - Integer metadataLength = frame.getUnsafeMetadataLength(); - return metadataLength != null && metadataLength - metadataIndex > 0; + ByteBuf dataFragment = Unpooled.EMPTY_BUFFER; + if (remaining > 0 && data.isReadable()) { + int r = Math.min(remaining, data.readableBytes()); + dataFragment = data.readRetainedSlice(r); } - ByteBuf readDataFragment(int length) { - int safeLength = min(length, frame.getDataLength() - dataIndex); - - ByteBuf fragment = frame.getUnsafeData().slice(dataIndex, safeLength); - - dataIndex += fragment.readableBytes(); - return fragment; - } - - ByteBuf readMetadataFragment(int length) { - Integer metadataLength = frame.getUnsafeMetadataLength(); - ByteBuf metadata = frame.getUnsafeMetadata(); + boolean follows = data.isReadable() || metadata.isReadable(); + return PayloadFrameCodec.encode( + allocator, streamId, follows, false, true, metadataFragment, dataFragment); + } - if (metadataLength == null || metadata == null) { - throw new IllegalStateException("Cannot read metadata fragment with no metadata"); + 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; + } + } - int safeLength = min(length, metadataLength - metadataIndex); - - ByteBuf fragment = metadata.slice(metadataIndex, safeLength); - - metadataIndex += fragment.readableBytes(); - return fragment; + 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 ba1886bcf..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,26 @@ package io.rsocket.fragmentation; -import static io.rsocket.util.DisposableUtils.disposeQuietly; -import static io.rsocket.util.RecyclerFactory.createRecycler; - import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import io.rsocket.framing.FragmentableFrame; -import io.rsocket.framing.Frame; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +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; /** @@ -39,132 +45,312 @@ * href="https://github.com/rsocket/rsocket/blob/master/Protocol.md#fragmentation-and-reassembly">Fragmentation * and Reassembly */ -final class FrameReassembler implements Disposable { +final class FrameReassembler extends AtomicBoolean implements Disposable { - private static final Recycler RECYCLER = createRecycler(FrameReassembler::new); + private static final long serialVersionUID = -4394598098863449055L; - private final Handle handle; + private static final Logger logger = LoggerFactory.getLogger(FrameReassembler.class); - private ByteBufAllocator byteBufAllocator; + final IntObjectMap headers; + final IntObjectMap metadata; + final IntObjectMap data; - private ReassemblyState state; + final ByteBufAllocator allocator; + final int maxInboundPayloadSize; - private FrameReassembler(Handle handle) { - this.handle = handle; + 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 (state != null) { - disposeQuietly(state); + 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(); + } } + } - byteBufAllocator = null; - state = null; + @Override + public boolean isDisposed() { + return get(); + } - handle.recycle(this); + @Nullable + synchronized ByteBuf getHeader(int streamId) { + return headers.get(streamId); } - /** - * Creates a new instance - * - * @param byteBufAllocator the {@link ByteBufAllocator} to use - * @return the {@code FrameReassembler} - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - static FrameReassembler createFrameReassembler(ByteBufAllocator byteBufAllocator) { - return RECYCLER.get().setByteBufAllocator(byteBufAllocator); + synchronized CompositeByteBuf getMetadata(int streamId) { + CompositeByteBuf byteBuf = metadata.get(streamId); + + if (byteBuf == null) { + byteBuf = allocator.compositeBuffer(); + metadata.put(streamId, byteBuf); + } + + return byteBuf; } - /** - * Reassembles a frame. If the frame is not a candidate for fragmentation, emits the frame. If - * frame is a candidate for fragmentation, accumulates the content until the final fragment. - * - * @param frame the frame to inspect for reassembly - * @return the reassembled frame if complete, otherwise {@code null} - * @throws NullPointerException if {@code frame} is {@code null} - */ - @Nullable - Frame reassemble(Frame frame) { - Objects.requireNonNull(frame, "frame must not be null"); + synchronized int getMetadataSize(int streamId) { + CompositeByteBuf byteBuf = metadata.get(streamId); - if (!(frame instanceof FragmentableFrame)) { - return frame; + if (byteBuf == null) { + return 0; } - FragmentableFrame fragmentableFrame = (FragmentableFrame) frame; + return byteBuf.readableBytes(); + } - if (fragmentableFrame.isFollowsFlagSet()) { - if (state == null) { - state = new ReassemblyState(fragmentableFrame); - } else { - state.accumulate(fragmentableFrame); - } - } else if (state != null) { - state.accumulate(fragmentableFrame); + synchronized CompositeByteBuf getData(int streamId) { + CompositeByteBuf byteBuf = data.get(streamId); - Frame reassembledFrame = state.createFrame(byteBufAllocator); - state.dispose(); - state = null; + if (byteBuf == null) { + byteBuf = allocator.compositeBuffer(); + data.put(streamId, byteBuf); + } - return reassembledFrame; - } else { - return fragmentableFrame; + return byteBuf; + } + + synchronized int getDataSize(int streamId) { + CompositeByteBuf byteBuf = data.get(streamId); + + if (byteBuf == null) { + return 0; } - return null; + return byteBuf.readableBytes(); } - FrameReassembler setByteBufAllocator(ByteBufAllocator byteBufAllocator) { - this.byteBufAllocator = - Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); + @Nullable + synchronized ByteBuf removeHeader(int streamId) { + return headers.remove(streamId); + } - return this; + @Nullable + synchronized CompositeByteBuf removeMetadata(int streamId) { + return metadata.remove(streamId); } - static final class ReassemblyState implements Disposable { + @Nullable + synchronized CompositeByteBuf removeData(int streamId) { + return data.remove(streamId); + } - private ByteBuf data; + synchronized void putHeader(int streamId, ByteBuf header) { + headers.put(streamId, header); + } - private List fragments = new ArrayList<>(); + void cancelAssemble(int streamId) { + ByteBuf header = removeHeader(streamId); + CompositeByteBuf metadata = removeMetadata(streamId); + CompositeByteBuf data = removeData(streamId); - private ByteBuf metadata; + if (header != null) { + ReferenceCountUtil.safeRelease(header); + } - ReassemblyState(FragmentableFrame fragment) { - accumulate(fragment); + if (metadata != null) { + ReferenceCountUtil.safeRelease(metadata); } - @Override - public void dispose() { - fragments.forEach(Disposable::dispose); + 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"); + } + } - void accumulate(FragmentableFrame fragment) { - fragments.add(fragment); - metadata = accumulateMetadata(fragment); - data = accumulateData(fragment); + 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) { - Frame createFrame(ByteBufAllocator byteBufAllocator) { - FragmentableFrame root = fragments.get(0); - return root.createNonFragment(byteBufAllocator, metadata, data); + 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); - private ByteBuf accumulateData(FragmentableFrame fragment) { - ByteBuf data = fragment.getUnsafeData(); - return this.data == null ? data.retain() : Unpooled.wrappedBuffer(this.data, data.retain()); + if (hasFollows) { + handleFollowsFlag(frame, streamId, frameType); + } else { + handleNoFollowsFlag(frame, sink, streamId); + } + + } catch (Throwable t) { + logger.error("error reassemble frame", t); + sink.error(t); } + } - private @Nullable ByteBuf accumulateMetadata(FragmentableFrame fragment) { - ByteBuf metadata = fragment.getUnsafeMetadata(); + private ByteBuf assembleFrameWithMetadata(ByteBuf frame, int streamId, ByteBuf header) { + ByteBuf metadata; + CompositeByteBuf cm = removeMetadata(streamId); - if (metadata == null) { - return this.metadata; + 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 this.metadata == null - ? metadata.retain() - : Unpooled.wrappedBuffer(this.metadata, metadata.retain()); + 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 4431f98dd..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. 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 3567584bd..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.rsocket.exceptions.*; -import io.rsocket.framing.FrameType; -import java.nio.charset.StandardCharsets; - -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 CANCELED = 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 dataLength) { - int length = FrameHeaderFlyweight.computeFrameHeaderLength(FrameType.ERROR, null, dataLength); - return length + Integer.BYTES; - } - - public static int encode( - final ByteBuf byteBuf, final int streamId, final int errorCode, final ByteBuf data) { - final int frameLength = computeFrameLength(data.readableBytes()); - - int length = - FrameHeaderFlyweight.encodeFrameHeader(byteBuf, frameLength, 0, FrameType.ERROR, streamId); - - byteBuf.setInt(ERROR_CODE_FIELD_OFFSET, errorCode); - length += Integer.BYTES; - - length += FrameHeaderFlyweight.encodeData(byteBuf, length, data); - - return length; - } - - public static int errorCodeFromException(Throwable ex) { - if (ex instanceof RSocketException) { - return ((RSocketException) ex).errorCode(); - } - - return APPLICATION_ERROR; - } - - 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; - } - - public static String message(ByteBuf content) { - return FrameHeaderFlyweight.sliceFrameData(content).toString(StandardCharsets.UTF_8); - } -} 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 8f6be3af3..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java +++ /dev/null @@ -1,367 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.rsocket.Frame; -import io.rsocket.framing.FrameType; -import javax.annotation.Nullable; -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 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; - - /** (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; - - 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, @Nullable Integer 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 @Nullable ByteBuf metadata) { - int length = 0; - - if (metadata != null) { - final int metadataLength = metadata.readableBytes(); - - 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, metadata.readerIndex(), metadataLength); - 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, data.readerIndex(), dataLength); - 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 @Nullable ByteBuf metadata, - final ByteBuf data) { - if (Frame.isFlagSet(flags, FLAGS_M) != (metadata != null)) { - throw new IllegalStateException("bad value for metadata flag"); - } - - final int frameLength = - computeFrameHeaderLength( - frameType, metadata != null ? metadata.readableBytes() : null, 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.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."); - } - } - - 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 @Nullable ByteBuf sliceFrameMetadata(final ByteBuf byteBuf) { - final FrameType frameType = frameType(byteBuf); - final int frameLength = frameLength(byteBuf); - final @Nullable Integer metadataLength = metadataLength(byteBuf, frameType, frameLength); - - if (metadataLength == null) { - return null; - } - - 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 @Nullable Integer metadataLength( - ByteBuf byteBuf, FrameType frameType, int frameLength) { - if (!hasMetadataLengthField(frameType)) { - return frameLength - metadataOffset(byteBuf); - } else { - return decodeMetadataLength(byteBuf, metadataOffset(byteBuf)); - } - } - - static @Nullable Integer decodeMetadataLength(final ByteBuf byteBuf, final int metadataOffset) { - int flags = flags(byteBuf); - if (FLAGS_M == (FLAGS_M & flags)) { - return decodeLength(byteBuf, metadataOffset); - } else { - return null; - } - } - - private static int computeMetadataLength(FrameType frameType, final @Nullable Integer length) { - if (!hasMetadataLengthField(frameType)) { - // Frames with only metadata does not need metadata length field - return length != null ? length : 0; - } else { - return length == null ? 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.fromEncodedType(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 REQUEST_FNF: - 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/framing/FrameType.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameType.java similarity index 75% rename from rsocket-core/src/main/java/io/rsocket/framing/FrameType.java rename to rsocket-core/src/main/java/io/rsocket/frame/FrameType.java index 2feaf9436..8ac743f87 100644 --- a/rsocket-core/src/main/java/io/rsocket/framing/FrameType.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameType.java @@ -14,14 +14,12 @@ * limitations under the License. */ -package io.rsocket.framing; +package io.rsocket.frame; -import io.rsocket.Frame; import java.util.Arrays; -import java.util.EnumSet; /** - * Types of {@link Frame} that can be sent. + * Types of Frame that can be sent. * * @see Frame * Types @@ -40,7 +38,7 @@ public enum FrameType { * href="https://github.com/rsocket/rsocket/blob/master/Protocol.md#setup-frame-0x01">Setup * Frame */ - SETUP(0x01, EnumSet.of(Flags.CAN_HAVE_DATA, Flags.CAN_HAVE_METADATA)), + SETUP(0x01, Flags.CAN_HAVE_DATA | Flags.CAN_HAVE_METADATA), /** * Sent by Responder to grant the ability to send requests. @@ -49,7 +47,7 @@ public enum FrameType { * href="https://github.com/rsocket/rsocket/blob/master/Protocol.md#lease-frame-0x02">Lease * Frame */ - LEASE(0x02, EnumSet.of(Flags.CAN_HAVE_METADATA)), + LEASE(0x02, Flags.CAN_HAVE_METADATA), /** * Connection keepalive. @@ -58,7 +56,7 @@ public enum FrameType { * href="https://github.com/rsocket/rsocket/blob/master/Protocol.md#frame-keepalive">Keepalive * Frame */ - KEEPALIVE(0x03, EnumSet.of(Flags.CAN_HAVE_DATA)), + KEEPALIVE(0x03, Flags.CAN_HAVE_DATA), // START REQUEST @@ -71,11 +69,10 @@ public enum FrameType { */ REQUEST_RESPONSE( 0x04, - EnumSet.of( - Flags.CAN_HAVE_DATA, - Flags.CAN_HAVE_METADATA, - Flags.IS_FRAGMENTABLE, - Flags.IS_REQUEST_TYPE)), + Flags.CAN_HAVE_DATA + | Flags.CAN_HAVE_METADATA + | Flags.IS_FRAGMENTABLE + | Flags.IS_REQUEST_TYPE), /** * A single one-way message. @@ -85,11 +82,10 @@ public enum FrameType { */ REQUEST_FNF( 0x05, - EnumSet.of( - Flags.CAN_HAVE_DATA, - Flags.CAN_HAVE_METADATA, - Flags.IS_FRAGMENTABLE, - Flags.IS_REQUEST_TYPE)), + Flags.CAN_HAVE_DATA + | Flags.CAN_HAVE_METADATA + | Flags.IS_FRAGMENTABLE + | Flags.IS_REQUEST_TYPE), /** * Request a completable stream. @@ -100,12 +96,11 @@ public enum FrameType { */ REQUEST_STREAM( 0x06, - EnumSet.of( - Flags.CAN_HAVE_METADATA, - Flags.CAN_HAVE_DATA, - Flags.HAS_INITIAL_REQUEST_N, - Flags.IS_FRAGMENTABLE, - Flags.IS_REQUEST_TYPE)), + 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. @@ -116,12 +111,11 @@ public enum FrameType { */ REQUEST_CHANNEL( 0x07, - EnumSet.of( - Flags.CAN_HAVE_METADATA, - Flags.CAN_HAVE_DATA, - Flags.HAS_INITIAL_REQUEST_N, - Flags.IS_FRAGMENTABLE, - Flags.IS_REQUEST_TYPE)), + Flags.CAN_HAVE_METADATA + | Flags.CAN_HAVE_DATA + | Flags.HAS_INITIAL_REQUEST_N + | Flags.IS_FRAGMENTABLE + | Flags.IS_REQUEST_TYPE), // DURING REQUEST @@ -150,7 +144,7 @@ public enum FrameType { * @see Payload * Frame */ - PAYLOAD(0x0A, EnumSet.of(Flags.CAN_HAVE_DATA, Flags.CAN_HAVE_METADATA, Flags.IS_FRAGMENTABLE)), + PAYLOAD(0x0A, Flags.CAN_HAVE_DATA | Flags.CAN_HAVE_METADATA | Flags.IS_FRAGMENTABLE), /** * Error at connection or application level. @@ -158,7 +152,7 @@ public enum FrameType { * @see Error * Frame */ - ERROR(0x0B, EnumSet.of(Flags.CAN_HAVE_DATA)), + ERROR(0x0B, Flags.CAN_HAVE_DATA), // METADATA @@ -169,7 +163,7 @@ public enum FrameType { * href="https://github.com/rsocket/rsocket/blob/master/Protocol.md#frame-metadata-push">Metadata * Push Frame */ - METADATA_PUSH(0x0C, EnumSet.of(Flags.CAN_HAVE_METADATA)), + METADATA_PUSH(0x0C, Flags.CAN_HAVE_METADATA), // RESUMPTION @@ -193,14 +187,13 @@ public enum FrameType { // SYNTHETIC PAYLOAD TYPES /** A {@link #PAYLOAD} frame with {@code NEXT} flag set. */ - NEXT(0xA0, EnumSet.of(Flags.CAN_HAVE_DATA, Flags.CAN_HAVE_METADATA, Flags.IS_FRAGMENTABLE)), + 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, EnumSet.of(Flags.CAN_HAVE_DATA, Flags.CAN_HAVE_METADATA, Flags.IS_FRAGMENTABLE)), + NEXT_COMPLETE(0xC0, Flags.CAN_HAVE_DATA | Flags.CAN_HAVE_METADATA | Flags.IS_FRAGMENTABLE), /** * Used To Extend more frame types as well as extensions. @@ -208,7 +201,7 @@ public enum FrameType { * @see Extension * Frame */ - EXT(0x3F, EnumSet.of(Flags.CAN_HAVE_DATA, Flags.CAN_HAVE_METADATA)); + EXT(0x3F, Flags.CAN_HAVE_DATA | Flags.CAN_HAVE_METADATA); /** The size of the encoded frame type */ static final int ENCODED_SIZE = 6; @@ -224,14 +217,13 @@ public enum FrameType { } private final int encodedType; - - private final EnumSet flags; + private final int flags; FrameType(int encodedType) { - this(encodedType, EnumSet.noneOf(Flags.class)); + this(encodedType, Flags.EMPTY); } - FrameType(int encodedType, EnumSet flags) { + FrameType(int encodedType, int flags) { this.encodedType = encodedType; this.flags = flags; } @@ -252,13 +244,17 @@ public static FrameType fromEncodedType(int 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 this.flags.contains(Flags.CAN_HAVE_DATA); + return Flags.CAN_HAVE_DATA == (flags & Flags.CAN_HAVE_DATA); } /** @@ -267,7 +263,7 @@ public boolean canHaveData() { * @return whether the frame type can have metadata */ public boolean canHaveMetadata() { - return this.flags.contains(Flags.CAN_HAVE_METADATA); + return Flags.CAN_HAVE_METADATA == (flags & Flags.CAN_HAVE_METADATA); } /** @@ -285,7 +281,7 @@ public int getEncodedType() { * @return wether the frame type starts with an initial {@code requestN} */ public boolean hasInitialRequestN() { - return this.flags.contains(Flags.HAS_INITIAL_REQUEST_N); + return Flags.HAS_INITIAL_REQUEST_N == (flags & Flags.HAS_INITIAL_REQUEST_N); } /** @@ -294,7 +290,7 @@ public boolean hasInitialRequestN() { * @return whether the frame type is fragmentable */ public boolean isFragmentable() { - return this.flags.contains(Flags.IS_FRAGMENTABLE); + return Flags.IS_FRAGMENTABLE == (flags & Flags.IS_FRAGMENTABLE); } /** @@ -303,22 +299,17 @@ public boolean isFragmentable() { * @return whether the frame type is a request type */ public boolean isRequestType() { - return this.flags.contains(Flags.IS_REQUEST_TYPE); - } - - private static int getMaximumEncodedType() { - return Arrays.stream(values()).mapToInt(frameType -> frameType.encodedType).max().orElse(0); + return Flags.IS_REQUEST_TYPE == (flags & Flags.IS_REQUEST_TYPE); } - private enum Flags { - CAN_HAVE_DATA, - - CAN_HAVE_METADATA, - - HAS_INITIAL_REQUEST_N, - - IS_FRAGMENTABLE, + 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; - IS_REQUEST_TYPE; + 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 05de9d291..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/KeepaliveFrameFlyweight.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.rsocket.framing.FrameType; - -public class KeepaliveFrameFlyweight { - /** - * (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; - - 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, null, 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 31a8520d7..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.rsocket.framing.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 4fcb407a4..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestFrameFlyweight.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.rsocket.Frame; -import io.rsocket.framing.FrameType; -import javax.annotation.Nullable; - -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 @Nullable Integer 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 @Nullable ByteBuf metadata, - final ByteBuf data) { - if (Frame.isFlagSet(flags, FrameHeaderFlyweight.FLAGS_M) != (metadata != null)) { - throw new IllegalArgumentException("metadata flag set incorrectly"); - } - - final int frameLength = - computeFrameLength( - type, metadata != null ? metadata.readableBytes() : null, 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 @Nullable ByteBuf metadata, - final ByteBuf data) { - if (Frame.isFlagSet(flags, FrameHeaderFlyweight.FLAGS_M) != (metadata != null)) { - throw new IllegalArgumentException("metadata flag set incorrectly"); - } - if (type.hasInitialRequestN()) { - throw new AssertionError(type + " must not be encoded without initial request N"); - } - final int frameLength = - computeFrameLength( - type, metadata != null ? metadata.readableBytes() : null, 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 69d3697b0..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.rsocket.framing.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 31ee02d33..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.frame; - -import static io.rsocket.frame.FrameHeaderFlyweight.FLAGS_M; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.rsocket.framing.FrameType; -import java.nio.charset.StandardCharsets; - -public class SetupFrameFlyweight { - private SetupFrameFlyweight() {} - - /** - * 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 VALID_FLAGS = FLAGS_RESUME_ENABLE | FLAGS_WILL_HONOR_LEASE | FLAGS_M; - - 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, resumeToken.readerIndex(), 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 96% 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 e238b3fe2..35e4aa86a 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/VersionFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/VersionCodec.java @@ -16,7 +16,7 @@ package io.rsocket.frame; -public class VersionFlyweight { +public class VersionCodec { public static int encode(int major, int minor) { return (major << 16) | (minor & 0xFFFF); 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-core/src/main/java/io/rsocket/frame/decoder/package-info.java b/rsocket-core/src/main/java/io/rsocket/frame/decoder/package-info.java new file mode 100644 index 000000000..82e8acaf3 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/decoder/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support for encoding and decoding of RSocket frames to and from {@link io.rsocket.Payload + * Payload}. + */ +@NonNullApi +package io.rsocket.frame.decoder; + +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 8aa3538d6..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,5 +14,11 @@ * 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/framing/AbstractRecyclableDataFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableDataFrame.java deleted file mode 100644 index 1b41b1795..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableDataFrame.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import reactor.util.annotation.Nullable; - -/** - * An abstract implementation of {@link DataFrame} that enables recycling for performance. - * - * @param the implementing type - * @see io.netty.util.Recycler - * @see Frame - * Data - */ -abstract class AbstractRecyclableDataFrame> - extends AbstractRecyclableFrame implements DataFrame { - - AbstractRecyclableDataFrame(Handle handle) { - super(handle); - } - - /** - * Appends data to the {@link ByteBuf}. - * - * @param byteBuf the {@link ByteBuf} to append to - * @param data the data to append - * @return the {@link ByteBuf} with data appended to it - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - static ByteBuf appendData(ByteBuf byteBuf, @Nullable ByteBuf data) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - if (data == null) { - return byteBuf; - } - - return Unpooled.wrappedBuffer(byteBuf, data.retain()); - } - - /** - * Returns the data. - * - * @param dataOffset the offset that the data starts at, relative to start of the {@link ByteBuf} - * @return the data - */ - final ByteBuf getData(int dataOffset) { - ByteBuf byteBuf = getByteBuf(); - return byteBuf.slice(dataOffset, byteBuf.readableBytes() - dataOffset).asReadOnly(); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableFragmentableFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableFragmentableFrame.java deleted file mode 100644 index 43a87b260..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableFragmentableFrame.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import io.netty.buffer.ByteBuf; -import io.netty.util.Recycler.Handle; -import java.util.Objects; - -/** - * An abstract implementation of {@link FragmentableFrame} that enables recycling for performance. - * - * @param the implementing type - * @see io.netty.util.Recycler - * @see Frame - * Metadata and Data - */ -abstract class AbstractRecyclableFragmentableFrame< - SELF extends AbstractRecyclableFragmentableFrame> - extends AbstractRecyclableMetadataAndDataFrame implements FragmentableFrame { - - private static final int FLAG_FOLLOWS = 1 << 7; - - AbstractRecyclableFragmentableFrame(Handle handle) { - super(handle); - } - - @Override - public final boolean isFollowsFlagSet() { - return isFlagSet(FLAG_FOLLOWS); - } - - /** - * Sets the Follows flag. - * - * @param byteBuf the {@link ByteBuf} to set the Follows flag on - * @return the {@link ByteBuf} with the Follows flag set - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - static ByteBuf setFollowsFlag(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return setFlag(byteBuf, FLAG_FOLLOWS); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableFrame.java deleted file mode 100644 index 09f28e50a..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableFrame.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.util.ReferenceCountUtil.release; -import static java.nio.charset.StandardCharsets.UTF_8; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import reactor.util.annotation.Nullable; - -/** - * An abstract implementation of {@link Frame} that enables recycling for performance. - * - * @param the implementing type - * @see io.netty.util.Recycler - */ -abstract class AbstractRecyclableFrame> - implements Frame { - - /** The size of the {@link FrameType} and flags in {@code byte}s. */ - static final int FRAME_TYPE_AND_FLAGS_BYTES = 2; - - private static final int FLAGS_MASK = 0b00000011_11111111; - - private final Handle handle; - - private ByteBuf byteBuf; - - AbstractRecyclableFrame(Handle handle) { - this.handle = handle; - } - - @Override - @SuppressWarnings("unchecked") - public final void dispose() { - if (byteBuf != null) { - release(byteBuf); - } - - byteBuf = null; - handle.recycle((SELF) this); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof AbstractRecyclableFrame)) { - return false; - } - AbstractRecyclableFrame that = (AbstractRecyclableFrame) o; - return Objects.equals(byteBuf, that.byteBuf); - } - - public FrameType getFrameType() { - int encodedType = byteBuf.getUnsignedShort(0) >> FRAME_TYPE_SHIFT; - return FrameType.fromEncodedType(encodedType); - } - - public final ByteBuf getUnsafeFrame() { - return byteBuf.asReadOnly(); - } - - @Override - public int hashCode() { - return Objects.hash(byteBuf); - } - - /** - * Create the {@link FrameType} and flags. - * - * @param byteBufAllocator the {@link ByteBufAllocator} to use - * @param frameType the {@link FrameType} - * @return the {@link ByteBuf} with {@link FrameType} and {@code flags} appended to it - * @throws NullPointerException if {@code byteBuf} or {@code frameType} is {@code null} - */ - static ByteBuf createFrameTypeAndFlags(ByteBufAllocator byteBufAllocator, FrameType frameType) { - Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); - Objects.requireNonNull(frameType, "frameType must not be null"); - - return byteBufAllocator.buffer().writeShort(getFrameTypeAndFlags(frameType)); - } - - /** - * Returns the {@link String} as a {@code UTF-8} encoded {@link ByteBuf}. - * - * @param s the {@link String} to convert - * @return the {@link String} as a {@code UTF-8} encoded {@link ByteBuf} or {@code null} if {@code - * s} is {@code null}. - */ - static @Nullable ByteBuf getUtf8AsByteBuf(@Nullable String s) { - return s == null ? null : Unpooled.copiedBuffer(s, UTF_8); - } - - /** - * Returns the {@link String} as a {@code UTF-8} encoded {@link ByteBuf}. - * - * @param s the {@link String} to convert - * @return the {@link String} as a {@code UTF-8} encoded {@link ByteBuf} - * @throws NullPointerException if {@code s} is {@code null} - */ - static ByteBuf getUtf8AsByteBufRequired(String s, String message) { - Objects.requireNonNull(s, message); - return Unpooled.copiedBuffer(s, UTF_8); - } - - /** - * Sets a flag. - * - * @param byteBuf the {@link ByteBuf} to set the flag on - * @param flag the flag to set - * @return the {@link ByteBuf} with the flag set - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - static ByteBuf setFlag(ByteBuf byteBuf, int flag) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return byteBuf.setShort(0, byteBuf.getShort(0) | (flag & FLAGS_MASK)); - } - - /** - * Returns the internal {@link ByteBuf}. - * - * @return the internal {@link ByteBuf} - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - final ByteBuf getByteBuf() { - return Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - } - - /** - * Sets the internal {@link ByteBuf}. - * - * @param byteBuf the {@link ByteBuf} - * @return {@code this} - */ - @SuppressWarnings("unchecked") - final SELF setByteBuf(ByteBuf byteBuf) { - this.byteBuf = byteBuf; - return (SELF) this; - } - - /** - * Returns whether a {@code flag} is set. - * - * @param flag the {@code flag} to test for - * @return whether a {@code flag} is set - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - final boolean isFlagSet(int flag) { - return (getByteBuf().getShort(0) & flag) != 0; - } - - private static int getFrameTypeAndFlags(FrameType frameType) { - return frameType.getEncodedType() << FRAME_TYPE_SHIFT; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableMetadataAndDataFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableMetadataAndDataFrame.java deleted file mode 100644 index 4b6d8ebe6..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableMetadataAndDataFrame.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.rsocket.framing.LengthUtils.getLengthAsUnsignedMedium; -import static io.rsocket.util.NumberUtils.MEDIUM_BYTES; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import reactor.util.annotation.Nullable; - -/** - * An abstract implementation of {@link MetadataAndDataFrame} that enables recycling for - * performance. - * - * @param the implementing type - * @see io.netty.util.Recycler - * @see Frame - * Metadata and Data - */ -abstract class AbstractRecyclableMetadataAndDataFrame< - SELF extends AbstractRecyclableMetadataAndDataFrame> - extends AbstractRecyclableFrame implements MetadataAndDataFrame { - - private static final int FLAG_METADATA = 1 << 8; - - AbstractRecyclableMetadataAndDataFrame(Handle handle) { - super(handle); - } - - /** - * Appends data to the {@link ByteBuf}. - * - * @param byteBuf the {@link ByteBuf} to append to - * @param data the data to append - * @return the {@link ByteBuf} with data appended to it - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - static ByteBuf appendData(ByteBuf byteBuf, @Nullable ByteBuf data) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - if (data == null) { - return byteBuf; - } - - return Unpooled.wrappedBuffer(byteBuf, data.retain()); - } - - /** - * Appends metadata to the {@link ByteBuf}. - * - * @param byteBufAllocator the {@link ByteBufAllocator} to use - * @param byteBuf the {@link ByteBuf} to append to - * @param metadata the metadata to append - * @return the {@link ByteBuf} with metadata appended to it - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - static ByteBuf appendMetadata( - ByteBufAllocator byteBufAllocator, ByteBuf byteBuf, @Nullable ByteBuf metadata) { - Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - if (metadata == null) { - return byteBuf.writeMedium(0); - } - - ByteBuf frame = - setFlag(byteBuf, FLAG_METADATA).writeMedium(getLengthAsUnsignedMedium(metadata)); - frame = Unpooled.wrappedBuffer(frame, metadata.retain(), byteBufAllocator.buffer()); - - return frame; - } - - /** - * Returns the data. - * - * @param metadataLengthOffset the offset that the metadataLength starts at, relative to start of - * the {@link ByteBuf} - * @return the data - */ - final ByteBuf getData(int metadataLengthOffset) { - int dataOffset = getDataOffset(metadataLengthOffset); - ByteBuf byteBuf = getByteBuf(); - return byteBuf.slice(dataOffset, byteBuf.readableBytes() - dataOffset).asReadOnly(); - } - - /** - * Returns the metadata. - * - * @param metadataLengthOffset the offset that the metadataLength starts at, relative to start of - * the {@link ByteBuf} - * @return the data - */ - final @Nullable ByteBuf getMetadata(int metadataLengthOffset) { - if (!isFlagSet(FLAG_METADATA)) { - return null; - } - - ByteBuf byteBuf = getByteBuf(); - return byteBuf - .slice(getMetadataOffset(metadataLengthOffset), getMetadataLength(metadataLengthOffset)) - .asReadOnly(); - } - - private static int getMetadataOffset(int metadataLengthOffset) { - return metadataLengthOffset + MEDIUM_BYTES; - } - - private int getDataOffset(int metadataLengthOffset) { - return getMetadataOffset(metadataLengthOffset) + getMetadataLength(metadataLengthOffset); - } - - private int getMetadataLength(int metadataLengthOffset) { - return getByteBuf().getUnsignedMedium(metadataLengthOffset); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableMetadataFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableMetadataFrame.java deleted file mode 100644 index 16f541189..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableMetadataFrame.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import reactor.util.annotation.Nullable; - -/** - * An abstract implementation of {@link MetadataFrame} that enables recycling for performance. - * - * @param the implementing type - * @see io.netty.util.Recycler - * @see Frame - * Metadata - */ -abstract class AbstractRecyclableMetadataFrame> - extends AbstractRecyclableFrame implements MetadataFrame { - - private static final int FLAG_METADATA = 1 << 8; - - AbstractRecyclableMetadataFrame(Handle handle) { - super(handle); - } - - /** - * Appends metadata to the {@link ByteBuf}. - * - * @param byteBuf the {@link ByteBuf} to append to - * @param metadata the metadata to append - * @return the {@link ByteBuf} with metadata appended to it - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - static ByteBuf appendMetadata(ByteBuf byteBuf, @Nullable ByteBuf metadata) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - if (metadata == null) { - return byteBuf; - } - - setFlag(byteBuf, FLAG_METADATA); - - return Unpooled.wrappedBuffer(byteBuf, metadata.retain()); - } - - /** - * Returns the metadata. - * - * @param metadataOffset the offset that the metadata starts at, relative to start of the {@link - * ByteBuf} - * @return the metadata or {@code null} if the metadata flag is not set - */ - final @Nullable ByteBuf getMetadata(int metadataOffset) { - if (!isFlagSet(FLAG_METADATA)) { - return null; - } - - ByteBuf byteBuf = getByteBuf(); - return byteBuf.slice(metadataOffset, byteBuf.readableBytes() - metadataOffset).asReadOnly(); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/CancelFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/CancelFrame.java deleted file mode 100644 index e96dd045c..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/CancelFrame.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.rsocket.framing.FrameType.CANCEL; -import static io.rsocket.util.RecyclerFactory.createRecycler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import java.util.Objects; - -/** - * An RSocket {@code CANCEL} frame. - * - * @see Cancel - * Frame - */ -public final class CancelFrame extends AbstractRecyclableFrame { - - private static final Recycler RECYCLER = createRecycler(CancelFrame::new); - - private CancelFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code CANCEL} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code CANCEL} frame - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static CancelFrame createCancelFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code CANCEL} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @return the {@code CANCEL} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static CancelFrame createCancelFrame(ByteBufAllocator byteBufAllocator) { - ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, CANCEL); - - return RECYCLER.get().setByteBuf(byteBuf); - } - - @Override - public String toString() { - return "CancelFrame{} "; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/DataFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/DataFrame.java deleted file mode 100644 index 525f2fe58..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/DataFrame.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import io.netty.buffer.ByteBuf; -import java.util.Objects; -import java.util.function.Function; - -/** An RSocket frame that only contains data. */ -public interface DataFrame extends Frame { - - /** - * Returns the data as a UTF-8 {@link String}. - * - * @return the data as a UTF-8 {@link String} - */ - default String getDataAsUtf8() { - return getUnsafeData().toString(UTF_8); - } - - /** - * Returns the length of the data in the frame. - * - * @return the length of the data in the frame - */ - default int getDataLength() { - return getUnsafeData().readableBytes(); - } - - /** - * Returns the data directly. - * - *

Note: this data will be outside of the {@link Frame}'s lifecycle and may be released - * at any time. It is highly recommended that you {@link ByteBuf#retain()} the data if you store - * it. - * - * @return the data directly - * @see #getDataAsUtf8() - * @see #mapData(Function) - */ - ByteBuf getUnsafeData(); - - /** - * Exposes the data for mapping to a different type. - * - * @param function the function to transform the data to a different type - * @param the different type - * @return the data mapped to a different type - * @throws NullPointerException if {@code function} is {@code null} - */ - default T mapData(Function function) { - Objects.requireNonNull(function, "function must not be null"); - - return function.apply(getUnsafeData()); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/ErrorFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/ErrorFrame.java deleted file mode 100644 index a94675388..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/ErrorFrame.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.util.ReferenceCountUtil.release; -import static io.rsocket.framing.FrameType.ERROR; -import static io.rsocket.util.RecyclerFactory.createRecycler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import reactor.util.annotation.Nullable; - -/** - * An RSocket {@code ERROR} frame. - * - * @see Error - * Frame - */ -public final class ErrorFrame extends AbstractRecyclableDataFrame { - - private static final int OFFSET_ERROR_CODE = FRAME_TYPE_AND_FLAGS_BYTES; - - private static final int OFFSET_DATA = OFFSET_ERROR_CODE + Integer.BYTES; - - private static final Recycler RECYCLER = createRecycler(ErrorFrame::new); - - private ErrorFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code ERROR} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code ERROR} frame. - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static ErrorFrame createErrorFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code ERROR} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param errorCode the error code - * @param data the error data - * @return the {@code ERROR} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static ErrorFrame createErrorFrame( - ByteBufAllocator byteBufAllocator, int errorCode, @Nullable String data) { - - ByteBuf dataByteBuf = getUtf8AsByteBuf(data); - - try { - return createErrorFrame(byteBufAllocator, errorCode, dataByteBuf); - } finally { - release(dataByteBuf); - } - } - - /** - * Creates the {@code ERROR} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param errorCode the error code - * @param data the error data - * @return the {@code ERROR} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static ErrorFrame createErrorFrame( - ByteBufAllocator byteBufAllocator, int errorCode, @Nullable ByteBuf data) { - - ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, ERROR).writeInt(errorCode); - byteBuf = appendData(byteBuf, data); - - return RECYCLER.get().setByteBuf(byteBuf); - } - - /** - * Returns the error code. - * - * @return the error code - */ - public int getErrorCode() { - return getByteBuf().getInt(OFFSET_ERROR_CODE); - } - - @Override - public ByteBuf getUnsafeData() { - return getData(OFFSET_DATA); - } - - @Override - public String toString() { - return "ErrorFrame{" - + "errorCode=" - + getErrorCode() - + ", data=" - + mapData(ByteBufUtil::hexDump) - + '}'; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/ErrorType.java b/rsocket-core/src/main/java/io/rsocket/framing/ErrorType.java deleted file mode 100644 index 46afa2a1a..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/ErrorType.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -/** - * The types of {@link Error} that can be set. - * - * @see Error - * Codes - */ -public final class ErrorType { - - /** - * Application layer logic generating a Reactive Streams onError event. Stream ID MUST be > 0. - */ - public static final int APPLICATION_ERROR = 0x00000201;; - - /** - * The Responder canceled the request but may have started processing it (similar to REJECTED but - * doesn't guarantee lack of side-effects). Stream ID MUST be > 0. - */ - public static final int CANCELED = 0x00000203; - - /** - * The connection is being terminated. Stream ID MUST be 0. Sender or Receiver of this frame MUST - * wait for outstanding streams to terminate before closing the connection. New requests MAY not - * be accepted. - */ - public static final int CONNECTION_CLOSE = 0x00000102; - - /** - * The connection is being terminated. Stream ID MUST be 0. Sender or Receiver of this frame MAY - * close the connection immediately without waiting for outstanding streams to terminate. - */ - public static final int CONNECTION_ERROR = 0x00000101; - - /** The request is invalid. Stream ID MUST be > 0. */ - public static final int INVALID = 0x00000204; - - /** - * The Setup frame is invalid for the server (it could be that the client is too recent for the - * old server). Stream ID MUST be 0. - */ - public static final int INVALID_SETUP = 0x00000001; - - /** - * 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. Stream ID MUST be > 0. - */ - public static final int REJECTED = 0x00000202; - - /** - * The server rejected the resume, it can specify the reason in the payload. Stream ID MUST be 0. - */ - public static final int REJECTED_RESUME = 0x00000004; - - /** - * The server rejected the setup, it can specify the reason in the payload. Stream ID MUST be 0. - */ - public static final int REJECTED_SETUP = 0x00000003; - - /** Reserved. */ - public static final int RESERVED = 0x00000000; - - /** Reserved for Extension Use. */ - public static final int RESERVED_FOR_EXTENSION = 0xFFFFFFFF; - - /** - * Some (or all) of the parameters specified by the client are unsupported by the server. Stream - * ID MUST be 0. - */ - public static final int UNSUPPORTED_SETUP = 0x00000002; - - private ErrorType() {} -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/ExtensionFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/ExtensionFrame.java deleted file mode 100644 index 37d6b5785..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/ExtensionFrame.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.util.ReferenceCountUtil.release; -import static io.rsocket.framing.FrameType.EXT; -import static io.rsocket.util.RecyclerFactory.createRecycler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import reactor.util.annotation.Nullable; - -/** - * An RSocket {@code EXT} frame. - * - * @see Extension - * Frame - */ -public final class ExtensionFrame extends AbstractRecyclableMetadataAndDataFrame { - - private static final int FLAG_IGNORE = 1 << 9; - - private static final int OFFSET_EXTENDED_TYPE = FRAME_TYPE_AND_FLAGS_BYTES; - - private static final int OFFSET_METADATA_LENGTH = OFFSET_EXTENDED_TYPE + Integer.BYTES; - - private static final Recycler RECYCLER = createRecycler(ExtensionFrame::new); - - private ExtensionFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code EXT} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code EXT} frame. - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static ExtensionFrame createExtensionFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code EXT} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param ignore whether to set the Ignore flag - * @param extendedType the type of the extended frame - * @param metadata the {@code metadata} - * @param data the {@code data} - * @return the {@code EXT} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static ExtensionFrame createExtensionFrame( - ByteBufAllocator byteBufAllocator, - boolean ignore, - int extendedType, - @Nullable String metadata, - @Nullable String data) { - - ByteBuf metadataByteBuf = getUtf8AsByteBuf(metadata); - ByteBuf dataByteBuf = getUtf8AsByteBuf(data); - - try { - return createExtensionFrame( - byteBufAllocator, ignore, extendedType, metadataByteBuf, dataByteBuf); - } finally { - release(metadataByteBuf); - release(dataByteBuf); - } - } - - /** - * Creates the {@code EXT} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param ignore whether to set the Ignore flag - * @param extendedType the type of the extended frame - * @param metadata the {@code metadata} - * @param data the {@code data} - * @return the {@code EXT} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static ExtensionFrame createExtensionFrame( - ByteBufAllocator byteBufAllocator, - boolean ignore, - int extendedType, - @Nullable ByteBuf metadata, - @Nullable ByteBuf data) { - - ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, EXT); - - if (ignore) { - byteBuf = setFlag(byteBuf, FLAG_IGNORE); - } - - byteBuf = byteBuf.writeInt(extendedType); - byteBuf = appendMetadata(byteBufAllocator, byteBuf, metadata); - byteBuf = appendData(byteBuf, data); - - return RECYCLER.get().setByteBuf(byteBuf); - } - - /** - * Returns the extended type. - * - * @return the extended type - */ - public int getExtendedType() { - return getByteBuf().getInt(OFFSET_EXTENDED_TYPE); - } - - @Override - public ByteBuf getUnsafeData() { - return getData(OFFSET_METADATA_LENGTH); - } - - @Override - public @Nullable ByteBuf getUnsafeMetadata() { - return getMetadata(OFFSET_METADATA_LENGTH); - } - - /** - * Returns whether the Ignore flag is set. - * - * @return whether the Ignore flag is set - */ - public boolean isIgnoreFlagSet() { - return isFlagSet(FLAG_IGNORE); - } - - @Override - public String toString() { - return "ExtensionFrame{" - + "ignore=" - + isIgnoreFlagSet() - + ", extendedType=" - + getExtendedType() - + ", metadata=" - + mapMetadata(ByteBufUtil::hexDump) - + ", data=" - + mapData(ByteBufUtil::hexDump) - + '}'; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/FragmentableFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/FragmentableFrame.java deleted file mode 100644 index d8b083951..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/FragmentableFrame.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import reactor.util.annotation.Nullable; - -/** An RSocket frame that is fragmentable */ -public interface FragmentableFrame extends MetadataAndDataFrame { - - /** - * Generates the fragment for this frame. - * - * @param byteBufAllocator the {@link ByteBufAllocator} to use - * @param metadata the metadata - * @param data the data - * @return the fragment for this frame - * @throws NullPointerException if {@code ByteBufAllocator} is {@code null} - */ - FragmentableFrame createFragment( - ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data); - - /** - * Generates the non-fragment for this frame. - * - * @param byteBufAllocator the {@link ByteBufAllocator} to use - * @param metadata the metadata - * @param data the data - * @return the non-fragment for this frame - * @throws NullPointerException if {@code ByteBufAllocator} is {@code null} - */ - FragmentableFrame createNonFragment( - ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data); - - /** - * Returns whether the Follows flag is set. - * - * @return whether the Follows flag is set - */ - boolean isFollowsFlagSet(); -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/Frame.java b/rsocket-core/src/main/java/io/rsocket/framing/Frame.java deleted file mode 100644 index 9292bc9af..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/Frame.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import io.netty.buffer.ByteBuf; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Function; -import reactor.core.Disposable; - -/** - * An RSocket frame. - * - * @see Framing - */ -public interface Frame extends Disposable { - - /** The shift length for the frame type. */ - int FRAME_TYPE_SHIFT = Short.SIZE - FrameType.ENCODED_SIZE; - - /** - * Exposes the {@code Frame} as a {@link ByteBuf} for consumption. - * - * @param consumer the {@link Consumer} to consume the {@code Frame} as a {@link ByteBuf} - * @throws NullPointerException if {@code consumer} is {@code null} - */ - default void consumeFrame(Consumer consumer) { - Objects.requireNonNull(consumer, "consumer must not be null"); - - consumer.accept(getUnsafeFrame()); - } - - /** - * Returns the {@link FrameType}. - * - * @return the {@link FrameType} - */ - FrameType getFrameType(); - - /** - * Returns the frame directly. - * - *

Note: this frame will be outside of the {@code Frame}'s lifecycle and may be released - * at any time. It is highly recommended that you {@link ByteBuf#retain()} the frame if you store - * it. - * - * @return the frame directly - * @see #consumeFrame(Consumer) - * @see #mapFrame(Function) - */ - ByteBuf getUnsafeFrame(); - - /** - * Exposes the {@code Frame} as a {@link ByteBuf} for mapping to a different type. - * - * @param function the {@link Function} to transform the {@code Frame} as a {@link ByteBuf} to a - * different type - * @param the different type - * @return the {@code Frame} as a {@link ByteBuf} mapped to a different type - * @throws NullPointerException if {@code function} is {@code null} - */ - default T mapFrame(Function function) { - Objects.requireNonNull(function, "function must not be null"); - - return function.apply(getUnsafeFrame()); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/FrameFactory.java b/rsocket-core/src/main/java/io/rsocket/framing/FrameFactory.java deleted file mode 100644 index 9631f09b0..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/FrameFactory.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.rsocket.framing.CancelFrame.createCancelFrame; -import static io.rsocket.framing.ErrorFrame.createErrorFrame; -import static io.rsocket.framing.ExtensionFrame.createExtensionFrame; -import static io.rsocket.framing.Frame.FRAME_TYPE_SHIFT; -import static io.rsocket.framing.KeepaliveFrame.createKeepaliveFrame; -import static io.rsocket.framing.LeaseFrame.createLeaseFrame; -import static io.rsocket.framing.MetadataPushFrame.createMetadataPushFrame; -import static io.rsocket.framing.PayloadFrame.createPayloadFrame; -import static io.rsocket.framing.RequestChannelFrame.createRequestChannelFrame; -import static io.rsocket.framing.RequestFireAndForgetFrame.createRequestFireAndForgetFrame; -import static io.rsocket.framing.RequestNFrame.createRequestNFrame; -import static io.rsocket.framing.RequestResponseFrame.createRequestResponseFrame; -import static io.rsocket.framing.RequestStreamFrame.createRequestStreamFrame; -import static io.rsocket.framing.ResumeFrame.createResumeFrame; -import static io.rsocket.framing.ResumeOkFrame.createResumeOkFrame; -import static io.rsocket.framing.SetupFrame.createSetupFrame; - -import io.netty.buffer.ByteBuf; -import java.util.Objects; - -/** - * A factory for creating RSocket frames from {@link ByteBuf}s. - * - * @see Frame - * Types - */ -public final class FrameFactory { - - private FrameFactory() {} - - /** - * Returns a strongly-type {@link Frame} created from a {@link ByteBuf}. - * - * @param byteBuf the {@code ByteBuf} to create the {@link Frame} from - * @return the strongly-typed {@link Frame} - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static Frame createFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - FrameType frameType = getFrameType(byteBuf); - switch (frameType) { - case SETUP: - return createSetupFrame(byteBuf); - case LEASE: - return createLeaseFrame(byteBuf); - case KEEPALIVE: - return createKeepaliveFrame(byteBuf); - case REQUEST_RESPONSE: - return createRequestResponseFrame(byteBuf); - case REQUEST_FNF: - return createRequestFireAndForgetFrame(byteBuf); - case REQUEST_STREAM: - return createRequestStreamFrame(byteBuf); - case REQUEST_CHANNEL: - return createRequestChannelFrame(byteBuf); - case REQUEST_N: - return createRequestNFrame(byteBuf); - case CANCEL: - return createCancelFrame(byteBuf); - case PAYLOAD: - return createPayloadFrame(byteBuf); - case ERROR: - return createErrorFrame(byteBuf); - case METADATA_PUSH: - return createMetadataPushFrame(byteBuf); - case RESUME: - return createResumeFrame(byteBuf); - case RESUME_OK: - return createResumeOkFrame(byteBuf); - case EXT: - return createExtensionFrame(byteBuf); - default: - throw new IllegalArgumentException( - String.format("Cannot create frame for type %s", frameType)); - } - }; - - private static FrameType getFrameType(ByteBuf byteBuf) { - int encodedType = byteBuf.getUnsignedShort(0) >> FRAME_TYPE_SHIFT; - return FrameType.fromEncodedType(encodedType); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/FrameLengthFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/FrameLengthFrame.java deleted file mode 100644 index 9bc50c7c1..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/FrameLengthFrame.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.rsocket.framing.LengthUtils.getLengthAsUnsignedMedium; -import static io.rsocket.util.NumberUtils.MEDIUM_BYTES; -import static io.rsocket.util.RecyclerFactory.createRecycler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import java.util.function.Function; - -/** - * An RSocket frame with a frame length. - * - * @see Framing - * Format - */ -public final class FrameLengthFrame extends AbstractRecyclableFrame { - - private static final int FRAME_LENGTH_BYTES = MEDIUM_BYTES; - - private static final Recycler RECYCLER = createRecycler(FrameLengthFrame::new); - - private FrameLengthFrame(Handle handle) { - super(handle); - } - - /** - * Creates the frame with a frame length. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the frame with a frame length - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static FrameLengthFrame createFrameLengthFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the frame with a frame length. - * - * @param byteBufAllocator the {@link ByteBufAllocator} to use - * @param frame the frame to prepend the frame length to - * @return the frame with a frame length - * @throws NullPointerException if {@code byteBufAllocator} or {@code frame} is {@code null} - */ - public static FrameLengthFrame createFrameLengthFrame( - ByteBufAllocator byteBufAllocator, Frame frame) { - - Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); - Objects.requireNonNull(frame, "frame must not be null"); - - ByteBuf frameLengthByteBuf = - frame.mapFrame( - frameByteBuf -> { - ByteBuf byteBuf = - byteBufAllocator - .buffer(FRAME_LENGTH_BYTES) - .writeMedium(getLengthAsUnsignedMedium(frameByteBuf)); - - return Unpooled.wrappedBuffer(byteBuf, frameByteBuf.retain()); - }); - - return RECYCLER.get().setByteBuf(frameLengthByteBuf); - } - - /** - * Returns the frame length. - * - * @return the frame length - */ - public int getFrameLength() { - return getByteBuf().getUnsignedMedium(0); - } - - /** - * Returns the frame without frame length directly. - * - *

Note: this frame without frame length will be outside of the {@link Frame}'s - * lifecycle and may be released at any time. It is highly recommended that you {@link - * ByteBuf#retain()} the frame without frame length if you store it. - * - * @return the frame without frame length directly - * @see #mapFrameWithoutFrameLength(Function) - */ - public ByteBuf getUnsafeFrameWithoutFrameLength() { - ByteBuf byteBuf = getByteBuf(); - return byteBuf - .slice(FRAME_LENGTH_BYTES, byteBuf.readableBytes() - FRAME_LENGTH_BYTES) - .asReadOnly(); - } - - /** - * Exposes the {@link Frame} without the frame length as a {@link ByteBuf} for mapping to a - * different type. - * - * @param function the function to transform the {@link Frame} without the frame length as a - * {@link ByteBuf} to a different type - * @param the different type - * @return the {@link Frame} without the frame length as a {@link ByteBuf} mapped to a different - * type - * @throws NullPointerException if {@code function} is {@code null} - */ - public T mapFrameWithoutFrameLength(Function function) { - Objects.requireNonNull(function, "function must not be null"); - - return function.apply(getUnsafeFrameWithoutFrameLength()); - } - - @Override - public String toString() { - return "FrameLengthFrame{" - + "frameLength=" - + getFrameLength() - + ", frameWithoutFrameLength=" - + mapFrameWithoutFrameLength(ByteBufUtil::hexDump) - + '}'; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/KeepaliveFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/KeepaliveFrame.java deleted file mode 100644 index 0dc90f1f6..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/KeepaliveFrame.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.rsocket.framing.FrameType.KEEPALIVE; -import static io.rsocket.util.RecyclerFactory.createRecycler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import reactor.util.annotation.Nullable; - -/** - * An RSocket {@code KEEPALIVE} frame. - * - * @see Keeplive - * Frame - */ -public final class KeepaliveFrame extends AbstractRecyclableDataFrame { - - private static final int FLAG_RESPOND = 1 << 7; - - private static final int OFFSET_LAST_RECEIVED_POSITION = FRAME_TYPE_AND_FLAGS_BYTES; - - private static final int OFFSET_DATA = OFFSET_LAST_RECEIVED_POSITION + Long.BYTES; - - private static final Recycler RECYCLER = createRecycler(KeepaliveFrame::new); - - private KeepaliveFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code KEEPALIVE} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code KEEPALIVE} frame. - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static KeepaliveFrame createKeepaliveFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code KEEPALIVE} frame. - * - * @param byteBufAllocator the {@link ByteBufAllocator} to use - * @param respond whether to set the Respond flag - * @param lastReceivedPosition the last received position - * @param data the frame data - * @return the {@code KEEPALIVE} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static KeepaliveFrame createKeepaliveFrame( - ByteBufAllocator byteBufAllocator, - boolean respond, - long lastReceivedPosition, - @Nullable ByteBuf data) { - - ByteBuf byteBuf = - createFrameTypeAndFlags(byteBufAllocator, KEEPALIVE).writeLong(lastReceivedPosition); - - if (respond) { - byteBuf = setFlag(byteBuf, FLAG_RESPOND); - } - - byteBuf = appendData(byteBuf, data); - - return RECYCLER.get().setByteBuf(byteBuf); - } - - /** - * Returns the last received position. - * - * @return the last received position - */ - public long getLastReceivedPosition() { - return getByteBuf().getLong(OFFSET_LAST_RECEIVED_POSITION); - } - - @Override - public ByteBuf getUnsafeData() { - return getData(OFFSET_DATA); - } - - /** - * Returns whether the respond flag is set. - * - * @return whether the respond flag is set - */ - public boolean isRespondFlagSet() { - return isFlagSet(FLAG_RESPOND); - } - - @Override - public String toString() { - return "KeepaliveFrame{" - + "respond=" - + isRespondFlagSet() - + ", lastReceivedPosition=" - + getLastReceivedPosition() - + ", data=" - + mapData(ByteBufUtil::hexDump) - + '}'; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/LeaseFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/LeaseFrame.java deleted file mode 100644 index 763fbc60f..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/LeaseFrame.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.rsocket.framing.FrameType.LEASE; -import static io.rsocket.util.RecyclerFactory.createRecycler; -import static java.lang.Math.toIntExact; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import io.rsocket.util.NumberUtils; -import java.time.Duration; -import java.util.Objects; -import reactor.util.annotation.Nullable; - -/** - * An RSocket {@code LEASE} frame. - * - * @see Lease - * Frame - */ -public final class LeaseFrame extends AbstractRecyclableMetadataFrame { - - private static final int OFFSET_TIME_TO_LIVE = FRAME_TYPE_AND_FLAGS_BYTES; - - private static final int OFFSET_NUMBER_OF_REQUESTS = OFFSET_TIME_TO_LIVE + Integer.BYTES; - - private static final int OFFSET_METADATA = OFFSET_NUMBER_OF_REQUESTS + Integer.BYTES; - - private static final Recycler RECYCLER = createRecycler(LeaseFrame::new); - - private LeaseFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code LEASE} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code LEASE} frame. - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static LeaseFrame createLeaseFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code LEASE} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param timeToLive the validity of lease from time of reception - * @param numberOfRequests the number of requests that may be sent until the next lease - * @param metadata the metadata - * @return the {@code LEASE} frame - * @throws IllegalArgumentException if {@code timeToLive} is not a positive duration - * @throws IllegalArgumentException if {@code numberOfRequests} is not positive - * @throws NullPointerException if {@code byteBufAllocator} or {@code timeToLive} is {@code null} - * @throws IllegalArgumentException if {@code timeToLive} is not a positive duration or {@code - * numberOfRequests} is not positive - */ - public static LeaseFrame createLeaseFrame( - ByteBufAllocator byteBufAllocator, - Duration timeToLive, - int numberOfRequests, - @Nullable ByteBuf metadata) { - - Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); - Objects.requireNonNull(timeToLive, "timeToLive must not be null"); - NumberUtils.requirePositive(timeToLive.toMillis(), "timeToLive must be a positive duration"); - NumberUtils.requirePositive(numberOfRequests, "numberOfRequests must be positive"); - - ByteBuf byteBuf = - createFrameTypeAndFlags(byteBufAllocator, LEASE) - .writeInt(toIntExact(timeToLive.toMillis())) - .writeInt(numberOfRequests); - - byteBuf = appendMetadata(byteBuf, metadata); - - return RECYCLER.get().setByteBuf(byteBuf); - } - - /** - * Returns the number of requests - * - * @return the number of requests - */ - public int getNumberOfRequests() { - return getByteBuf().getInt(OFFSET_NUMBER_OF_REQUESTS); - } - - /** - * Returns the time to live. - * - * @return the time to live - */ - public Duration getTimeToLive() { - return Duration.ofMillis(getByteBuf().getInt(OFFSET_TIME_TO_LIVE)); - } - - @Override - public @Nullable ByteBuf getUnsafeMetadata() { - return getMetadata(OFFSET_METADATA); - } - - @Override - public String toString() { - return "LeaseFrame{" - + "timeToLive=" - + getTimeToLive() - + ", numberOfRequests=" - + getNumberOfRequests() - + ", metadata=" - + mapMetadata(ByteBufUtil::hexDump) - + +'}'; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/LengthUtils.java b/rsocket-core/src/main/java/io/rsocket/framing/LengthUtils.java deleted file mode 100644 index 2374e8f81..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/LengthUtils.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.rsocket.util.NumberUtils.requireUnsignedByte; -import static io.rsocket.util.NumberUtils.requireUnsignedMedium; -import static io.rsocket.util.NumberUtils.requireUnsignedShort; - -import io.netty.buffer.ByteBuf; -import java.util.Objects; - -/** Utilities for working with {@code ByteBuf} lengths */ -final class LengthUtils { - - private LengthUtils() {} - - /** - * Returns the length of a {@link ByteBuf} as an unsigned {@code byte}. - * - * @param byteBuf the {@link ByteBuf} to get the length of - * @return the length of a {@link ByteBuf} as an unsigned {@code byte} - */ - static int getLengthAsUnsignedByte(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return requireUnsignedByte(byteBuf.readableBytes()); - } - - /** - * Returns the length of a {@link ByteBuf} as an unsigned {@code medium} - * - * @param byteBuf the {@link ByteBuf} to get the length of - * @return the length of a {@link ByteBuf} as an unsigned {@code medium} - */ - static int getLengthAsUnsignedMedium(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return requireUnsignedMedium(byteBuf.readableBytes()); - } - - /** - * Returns the length of a {@link ByteBuf} as an unsigned {@code short} - * - * @param byteBuf the {@link ByteBuf} to get the length of - * @return the length of a {@link ByteBuf} as an unsigned {@code short} - */ - static int getLengthAsUnsignedShort(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return requireUnsignedShort(byteBuf.readableBytes()); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/MetadataFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/MetadataFrame.java deleted file mode 100644 index 99d43efcd..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/MetadataFrame.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import io.netty.buffer.ByteBuf; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; -import reactor.util.annotation.Nullable; - -/** An RSocket frame that only contains metadata. */ -public interface MetadataFrame extends Frame { - - /** - * Returns the metadata as a UTF-8 {@link String}. If the Metadata flag is not set, returns {@link - * Optional#empty()}. - * - * @return optionally, the metadata as a UTF-8 {@link String} - */ - default Optional getMetadataAsUtf8() { - return Optional.ofNullable(getUnsafeMetadataAsUtf8()); - } - - /** - * Returns the length of the metadata in the frame. If the Metadata flag is not set, returns - * {@link Optional#empty()}. - * - * @return optionally, the length of the metadata in the frame - */ - default Optional getMetadataLength() { - return Optional.ofNullable(getUnsafeMetadataLength()); - } - - /** - * Returns the metadata directly. If the Metadata flag is not set, returns {@code null}. - * - *

Note: this metadata will be outside of the {@link Frame}'s lifecycle and may be - * released at any time. It is highly recommended that you {@link ByteBuf#retain()} the metadata - * if you store it. - * - * @return the metadata directly, or {@code null} if the Metadata flag is not set - * @see #getMetadataAsUtf8() - * @see #mapMetadata(Function) - */ - @Nullable - ByteBuf getUnsafeMetadata(); - - /** - * Returns the metadata as a UTF-8 {@link String}. If the Metadata flag is not set, returns {@code - * null}. - * - * @return the metadata as a UTF-8 {@link String} or {@code null} if the Metadata flag is not set. - * @see #getMetadataAsUtf8() - */ - default @Nullable String getUnsafeMetadataAsUtf8() { - ByteBuf byteBuf = getUnsafeMetadata(); - return byteBuf == null ? null : byteBuf.toString(UTF_8); - } - - /** - * Returns the length of the metadata in the frame directly. If the Metadata flag is not set, - * returns {@code null}. - * - * @return the length of the metadata in frame directly, or {@code null} if the Metadata flag is - * not set - * @see #getMetadataLength() - */ - default @Nullable Integer getUnsafeMetadataLength() { - ByteBuf byteBuf = getUnsafeMetadata(); - return byteBuf == null ? null : byteBuf.readableBytes(); - } - - /** - * Exposes the metadata for mapping to a different type. If the Metadata flag is not set, returns - * {@link Optional#empty()}. - * - * @param function the function to transform the metadata to a different type - * @param the different type - * @return optionally, the metadata mapped to a different type - * @throws NullPointerException if {@code function} is {@code null} - */ - default Optional mapMetadata(Function function) { - Objects.requireNonNull(function, "function must not be null"); - - return Optional.ofNullable(getUnsafeMetadata()).map(function); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/MetadataPushFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/MetadataPushFrame.java deleted file mode 100644 index 2490fe84c..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/MetadataPushFrame.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.util.ReferenceCountUtil.release; -import static io.rsocket.framing.FrameType.METADATA_PUSH; -import static io.rsocket.util.RecyclerFactory.createRecycler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import reactor.util.annotation.Nullable; - -/** - * An RSocket {@code METADATA_PUSH} frame. - * - * @see Metadata - * Push Frame - */ -public final class MetadataPushFrame extends AbstractRecyclableMetadataFrame { - - private static final int OFFSET_METADATA = FRAME_TYPE_AND_FLAGS_BYTES; - - private static final Recycler RECYCLER = - createRecycler(MetadataPushFrame::new); - - private MetadataPushFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code METADATA_PUSH} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code METADATA_PUSH} frame. - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static MetadataPushFrame createMetadataPushFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code METADATA_PUSH} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param metadata the metadata - * @return the {@code METADATA_PUSH} frame - * @throws NullPointerException if {@code byteBufAllocator} or {@code metadata} is {@code null} - */ - public static MetadataPushFrame createMetadataPushFrame( - ByteBufAllocator byteBufAllocator, String metadata) { - - ByteBuf metadataByteBuf = getUtf8AsByteBufRequired(metadata, "metadata must not be null"); - - try { - return createMetadataPushFrame(byteBufAllocator, metadataByteBuf); - } finally { - release(metadataByteBuf); - } - } - - /** - * Creates the {@code METADATA_PUSH} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param metadata the metadata - * @return the {@code METADATA_PUSH} frame - * @throws NullPointerException if {@code byteBufAllocator} or {@code metadata} is {@code null} - */ - public static MetadataPushFrame createMetadataPushFrame( - ByteBufAllocator byteBufAllocator, ByteBuf metadata) { - - Objects.requireNonNull(metadata, "metadata must not be null"); - - ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, METADATA_PUSH); - byteBuf = appendMetadata(byteBuf, metadata); - - return RECYCLER.get().setByteBuf(byteBuf); - } - - @Override - public @Nullable ByteBuf getUnsafeMetadata() { - return getMetadata(OFFSET_METADATA); - } - - @Override - public String toString() { - return "MetadataPushFrame{" + "metadata=" + mapMetadata(ByteBufUtil::hexDump) + '}'; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/PayloadFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/PayloadFrame.java deleted file mode 100644 index c91777df6..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/PayloadFrame.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.util.ReferenceCountUtil.release; -import static io.rsocket.util.RecyclerFactory.createRecycler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import reactor.util.annotation.Nullable; - -/** - * An RSocket {@code PAYLOAD} frame. - * - * @see Payload - * Frame - */ -public final class PayloadFrame extends AbstractRecyclableFragmentableFrame { - - private static final int FLAG_COMPLETE = 1 << 6; - - private static final int FLAG_NEXT = 1 << 5; - - private static final int OFFSET_METADATA_LENGTH = FRAME_TYPE_AND_FLAGS_BYTES; - - private static final Recycler RECYCLER = createRecycler(PayloadFrame::new); - - private PayloadFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code PAYLOAD} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code PAYLOAD} frame. - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static PayloadFrame createPayloadFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code PAYLOAD} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param follows whether to set the Follows flag - * @param complete respond whether to set the Complete flag - * @param metadata the metadata - * @param data the data - * @return the {@code PAYLOAD} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static PayloadFrame createPayloadFrame( - ByteBufAllocator byteBufAllocator, - boolean follows, - boolean complete, - @Nullable String metadata, - @Nullable String data) { - - ByteBuf metadataByteBuf = getUtf8AsByteBuf(metadata); - ByteBuf dataByteBuf = getUtf8AsByteBuf(data); - - try { - return createPayloadFrame(byteBufAllocator, follows, complete, metadataByteBuf, dataByteBuf); - } finally { - release(metadataByteBuf); - release(dataByteBuf); - } - } - - /** - * Creates the {@code PAYLOAD} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param follows respond whether to set the Follows flag - * @param complete respond whether to set the Complete flag - * @param metadata the metadata - * @param data the data - * @return the {@code PAYLOAD} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static PayloadFrame createPayloadFrame( - ByteBufAllocator byteBufAllocator, - boolean follows, - boolean complete, - @Nullable ByteBuf metadata, - @Nullable ByteBuf data) { - - if (!complete && (data == null)) { - throw new IllegalArgumentException( - "Payload frame must either be complete, have data, or both"); - } - - ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, FrameType.PAYLOAD); - - if (follows) { - byteBuf = setFollowsFlag(byteBuf); - } - - if (complete) { - byteBuf = setFlag(byteBuf, FLAG_COMPLETE); - } - - byteBuf = appendMetadata(byteBufAllocator, byteBuf, metadata); - - if (data != null) { - byteBuf = setFlag(byteBuf, FLAG_NEXT); - } - - byteBuf = appendData(byteBuf, data); - - return RECYCLER.get().setByteBuf(byteBuf); - } - - @Override - public PayloadFrame createFragment( - ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { - - return createPayloadFrame(byteBufAllocator, true, isCompleteFlagSet(), metadata, data); - } - - @Override - public PayloadFrame createNonFragment( - ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { - - return createPayloadFrame(byteBufAllocator, false, isCompleteFlagSet(), metadata, data); - } - - @Override - public ByteBuf getUnsafeData() { - return getData(OFFSET_METADATA_LENGTH); - } - - @Override - public @Nullable ByteBuf getUnsafeMetadata() { - return getMetadata(OFFSET_METADATA_LENGTH); - } - - /** - * Returns whether the Complete flag is set. - * - * @return whether the Complete flag is set - */ - public boolean isCompleteFlagSet() { - return isFlagSet(FLAG_COMPLETE); - } - - /** - * Returns whether the Next flag is set. - * - * @return whether the Next flag is set - */ - public boolean isNextFlagSet() { - return isFlagSet(FLAG_NEXT); - } - - @Override - public String toString() { - return "PayloadFrame{" - + "follows=" - + isFollowsFlagSet() - + ", complete=" - + isCompleteFlagSet() - + ", next=" - + isNextFlagSet() - + ", metadata=" - + mapMetadata(ByteBufUtil::hexDump) - + ", data=" - + mapData(ByteBufUtil::hexDump) - + '}'; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/RequestChannelFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/RequestChannelFrame.java deleted file mode 100644 index 95b5def86..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/RequestChannelFrame.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.util.ReferenceCountUtil.release; -import static io.rsocket.util.RecyclerFactory.createRecycler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import io.rsocket.util.NumberUtils; -import java.util.Objects; -import reactor.util.annotation.Nullable; - -/** - * An RSocket {@code REQUEST_CHANNEL} frame. - * - * @see Request - * Channel Frame - */ -public final class RequestChannelFrame - extends AbstractRecyclableFragmentableFrame { - - private static final int FLAG_COMPLETE = 1 << 6; - - private static final int OFFSET_INITIAL_REQUEST_N = FRAME_TYPE_AND_FLAGS_BYTES; - - private static final int OFFSET_METADATA_LENGTH = OFFSET_INITIAL_REQUEST_N + Integer.BYTES; - - private static final Recycler RECYCLER = - createRecycler(RequestChannelFrame::new); - - private RequestChannelFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code REQUEST_CHANNEL} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code REQUEST_CHANNEL} frame. - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static RequestChannelFrame createRequestChannelFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code REQUEST_CHANNEL} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param follows whether to set the Follows flag - * @param complete whether to set the Complete flag - * @param initialRequestN the initial requestN - * @param metadata the metadata - * @param data the data - * @return the {@code REQUEST_CHANNEL} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static RequestChannelFrame createRequestChannelFrame( - ByteBufAllocator byteBufAllocator, - boolean follows, - boolean complete, - int initialRequestN, - @Nullable String metadata, - @Nullable String data) { - - ByteBuf metadataByteBuf = getUtf8AsByteBuf(metadata); - ByteBuf dataByteBuf = getUtf8AsByteBuf(data); - - try { - return createRequestChannelFrame( - byteBufAllocator, follows, complete, initialRequestN, metadataByteBuf, dataByteBuf); - } finally { - release(metadataByteBuf); - release(dataByteBuf); - } - } - - /** - * Creates the {@code REQUEST_CHANNEL} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param follows whether to set the Follows flag - * @param complete whether to set the Complete flag - * @param initialRequestN the initial requestN - * @param metadata the metadata - * @param data the data - * @return the {@code REQUEST_CHANNEL} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - * @throws IllegalArgumentException if {@code initialRequestN} is not positive - */ - public static RequestChannelFrame createRequestChannelFrame( - ByteBufAllocator byteBufAllocator, - boolean follows, - boolean complete, - int initialRequestN, - @Nullable ByteBuf metadata, - @Nullable ByteBuf data) { - - NumberUtils.requirePositive(initialRequestN, "initialRequestN must be positive"); - - ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, FrameType.REQUEST_CHANNEL); - - if (follows) { - byteBuf = setFollowsFlag(byteBuf); - } - - if (complete) { - byteBuf = setFlag(byteBuf, FLAG_COMPLETE); - } - - byteBuf = byteBuf.writeInt(initialRequestN); - byteBuf = appendMetadata(byteBufAllocator, byteBuf, metadata); - byteBuf = appendData(byteBuf, data); - - return RECYCLER.get().setByteBuf(byteBuf); - } - - @Override - public RequestChannelFrame createFragment( - ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { - - return createRequestChannelFrame( - byteBufAllocator, true, isCompleteFlagSet(), getInitialRequestN(), metadata, data); - } - - @Override - public RequestChannelFrame createNonFragment( - ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { - - return createRequestChannelFrame( - byteBufAllocator, false, isCompleteFlagSet(), getInitialRequestN(), metadata, data); - } - - /** - * Returns the initial requestN. - * - * @return the initial requestN - */ - public int getInitialRequestN() { - return getByteBuf().getInt(OFFSET_INITIAL_REQUEST_N); - } - - @Override - public ByteBuf getUnsafeData() { - return getData(OFFSET_METADATA_LENGTH); - } - - @Override - public @Nullable ByteBuf getUnsafeMetadata() { - return getMetadata(OFFSET_METADATA_LENGTH); - } - - /** - * Returns whether the Complete flag is set. - * - * @return whether the Complete flag is set - */ - public boolean isCompleteFlagSet() { - return isFlagSet(FLAG_COMPLETE); - } - - @Override - public String toString() { - return "RequestChannelFrame{" - + "follows=" - + isFollowsFlagSet() - + ", complete=" - + isCompleteFlagSet() - + ", initialRequestN=" - + getInitialRequestN() - + ", metadata=" - + mapMetadata(ByteBufUtil::hexDump) - + ", data=" - + mapData(ByteBufUtil::hexDump) - + '}'; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/RequestFireAndForgetFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/RequestFireAndForgetFrame.java deleted file mode 100644 index 2f4dbe978..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/RequestFireAndForgetFrame.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.util.ReferenceCountUtil.release; -import static io.rsocket.util.RecyclerFactory.createRecycler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import reactor.util.annotation.Nullable; - -/** - * An RSocket {@code REQUEST_FNF} frame. - * - * @see Request - * Fire and Forget Frame - */ -public final class RequestFireAndForgetFrame - extends AbstractRecyclableFragmentableFrame { - - private static final int OFFSET_METADATA_LENGTH = FRAME_TYPE_AND_FLAGS_BYTES; - - private static final Recycler RECYCLER = - createRecycler(RequestFireAndForgetFrame::new); - - private RequestFireAndForgetFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code REQUEST_FNF} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code REQUEST_FNF} frame. - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static RequestFireAndForgetFrame createRequestFireAndForgetFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code REQUEST_FNF} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param follows whether to set the Follows flag - * @param metadata the metadata - * @param data the data - * @return the {@code REQUEST_FNF} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static RequestFireAndForgetFrame createRequestFireAndForgetFrame( - ByteBufAllocator byteBufAllocator, - boolean follows, - @Nullable String metadata, - @Nullable String data) { - - ByteBuf metadataByteBuf = getUtf8AsByteBuf(metadata); - ByteBuf dataByteBuf = getUtf8AsByteBuf(data); - - try { - return createRequestFireAndForgetFrame( - byteBufAllocator, follows, metadataByteBuf, dataByteBuf); - } finally { - release(metadataByteBuf); - release(dataByteBuf); - } - } - - /** - * Creates the {@code REQUEST_FNF} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param follows whether to set the Follows flag - * @param metadata the metadata - * @param data the data - * @return the {@code REQUEST_FNF} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static RequestFireAndForgetFrame createRequestFireAndForgetFrame( - ByteBufAllocator byteBufAllocator, - boolean follows, - @Nullable ByteBuf metadata, - @Nullable ByteBuf data) { - - ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, FrameType.REQUEST_FNF); - - if (follows) { - byteBuf = setFollowsFlag(byteBuf); - } - - byteBuf = appendMetadata(byteBufAllocator, byteBuf, metadata); - byteBuf = appendData(byteBuf, data); - - return RECYCLER.get().setByteBuf(byteBuf); - } - - @Override - public RequestFireAndForgetFrame createFragment( - ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { - - return createRequestFireAndForgetFrame(byteBufAllocator, true, metadata, data); - } - - @Override - public RequestFireAndForgetFrame createNonFragment( - ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { - - return createRequestFireAndForgetFrame(byteBufAllocator, false, metadata, data); - } - - @Override - public ByteBuf getUnsafeData() { - return getData(OFFSET_METADATA_LENGTH); - } - - @Override - public @Nullable ByteBuf getUnsafeMetadata() { - return getMetadata(OFFSET_METADATA_LENGTH); - } - - @Override - public String toString() { - return "RequestFireAndForgetFrame{" - + "follows=" - + isFollowsFlagSet() - + ", metadata=" - + mapMetadata(ByteBufUtil::hexDump) - + ", data=" - + mapData(ByteBufUtil::hexDump) - + '}'; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/RequestNFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/RequestNFrame.java deleted file mode 100644 index d8c89e7b6..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/RequestNFrame.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.rsocket.framing.FrameType.REQUEST_N; -import static io.rsocket.util.RecyclerFactory.createRecycler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import io.rsocket.util.NumberUtils; -import java.util.Objects; - -/** - * An RSocket {@code REQUEST_N} frame. - * - * @see RequestN - * Frame - */ -public final class RequestNFrame extends AbstractRecyclableFrame { - - private static final int OFFSET_REQUEST_N = FRAME_TYPE_AND_FLAGS_BYTES; - - private static final Recycler RECYCLER = createRecycler(RequestNFrame::new); - - private RequestNFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code REQUEST_N} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code REQUEST_N} frame. - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static RequestNFrame createRequestNFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code REQUEST_N} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param requestN the size of the request. Must be positive. - * @return the {@code REQUEST_N} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static RequestNFrame createRequestNFrame(ByteBufAllocator byteBufAllocator, int requestN) { - NumberUtils.requirePositive(requestN, "requestN must be positive"); - - ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, REQUEST_N).writeInt(requestN); - - return RECYCLER.get().setByteBuf(byteBuf); - } - - @Override - public String toString() { - return "RequestNFrame{" + "requestN=" + getRequestN() + '}'; - } - - /** - * Returns the size of the request. - * - * @return the size of the request - */ - int getRequestN() { - return getByteBuf().getInt(OFFSET_REQUEST_N); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/RequestResponseFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/RequestResponseFrame.java deleted file mode 100644 index ce834ca20..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/RequestResponseFrame.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.util.ReferenceCountUtil.release; -import static io.rsocket.util.RecyclerFactory.createRecycler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import reactor.util.annotation.Nullable; - -/** - * An RSocket {@code REQUEST_RESPONSE} frame. - * - * @see Request - * Response Frame - */ -public final class RequestResponseFrame - extends AbstractRecyclableFragmentableFrame { - - private static final int OFFSET_METADATA_LENGTH = FRAME_TYPE_AND_FLAGS_BYTES; - - private static final Recycler RECYCLER = - createRecycler(RequestResponseFrame::new); - - private RequestResponseFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code REQUEST_RESPONSE} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code REQUEST_RESPONSE} frame. - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static RequestResponseFrame createRequestResponseFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code REQUEST_RESPONSE} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param follows whether to set the Follows flag - * @param metadata the metadata - * @param data the data - * @return the {@code REQUEST_RESPONSE} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static RequestResponseFrame createRequestResponseFrame( - ByteBufAllocator byteBufAllocator, - boolean follows, - @Nullable String metadata, - @Nullable String data) { - - ByteBuf metadataByteBuf = getUtf8AsByteBuf(metadata); - ByteBuf dataByteBuf = getUtf8AsByteBuf(data); - - try { - return createRequestResponseFrame(byteBufAllocator, follows, metadataByteBuf, dataByteBuf); - } finally { - release(metadataByteBuf); - release(dataByteBuf); - } - } - - /** - * Creates the {@code REQUEST_RESPONSE} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param follows whether to set the Follows flag - * @param metadata the metadata - * @param data the data - * @return the {@code REQUEST_RESPONSE} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static RequestResponseFrame createRequestResponseFrame( - ByteBufAllocator byteBufAllocator, - boolean follows, - @Nullable ByteBuf metadata, - @Nullable ByteBuf data) { - - ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, FrameType.REQUEST_RESPONSE); - - if (follows) { - byteBuf = setFollowsFlag(byteBuf); - } - - byteBuf = appendMetadata(byteBufAllocator, byteBuf, metadata); - byteBuf = appendData(byteBuf, data); - - return RECYCLER.get().setByteBuf(byteBuf); - } - - @Override - public RequestResponseFrame createFragment( - ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { - - return createRequestResponseFrame(byteBufAllocator, true, metadata, data); - } - - @Override - public RequestResponseFrame createNonFragment( - ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { - - return createRequestResponseFrame(byteBufAllocator, false, metadata, data); - } - - @Override - public ByteBuf getUnsafeData() { - return getData(OFFSET_METADATA_LENGTH); - } - - @Override - public @Nullable ByteBuf getUnsafeMetadata() { - return getMetadata(OFFSET_METADATA_LENGTH); - } - - @Override - public String toString() { - return "RequestResponseFrame{" - + "follows=" - + isFollowsFlagSet() - + ", metadata=" - + mapMetadata(ByteBufUtil::hexDump) - + ", data=" - + mapData(ByteBufUtil::hexDump) - + '}'; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/RequestStreamFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/RequestStreamFrame.java deleted file mode 100644 index fa5747a63..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/RequestStreamFrame.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.util.ReferenceCountUtil.release; -import static io.rsocket.util.RecyclerFactory.createRecycler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import io.rsocket.util.NumberUtils; -import java.util.Objects; -import reactor.util.annotation.Nullable; - -/** - * An RSocket {@code REQUEST_STREAM} frame. - * - * @see Request - * Stream Frame - */ -public final class RequestStreamFrame - extends AbstractRecyclableFragmentableFrame { - - private static final int OFFSET_INITIAL_REQUEST_N = FRAME_TYPE_AND_FLAGS_BYTES; - - private static final int OFFSET_METADATA_LENGTH = OFFSET_INITIAL_REQUEST_N + Integer.BYTES; - - private static final Recycler RECYCLER = - createRecycler(RequestStreamFrame::new); - - private RequestStreamFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code REQUEST_STREAM} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code REQUEST_STREAM} frame. - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static RequestStreamFrame createRequestStreamFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code REQUEST_STREAM} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param follows whether to set the Follows flag - * @param initialRequestN the initial requestN - * @param metadata the metadata - * @param data the data - * @return the {@code REQUEST_STREAM} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static RequestStreamFrame createRequestStreamFrame( - ByteBufAllocator byteBufAllocator, - boolean follows, - int initialRequestN, - @Nullable String metadata, - @Nullable String data) { - - ByteBuf metadataByteBuf = getUtf8AsByteBuf(metadata); - ByteBuf dataByteBuf = getUtf8AsByteBuf(data); - - try { - return createRequestStreamFrame( - byteBufAllocator, follows, initialRequestN, metadataByteBuf, dataByteBuf); - } finally { - release(metadataByteBuf); - release(dataByteBuf); - } - } - - /** - * Creates the {@code REQUEST_STREAM} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param follows whether to set the Follows flag - * @param initialRequestN the initial requestN - * @param metadata the metadata - * @param data the data - * @return the {@code REQUEST_STREAM} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - * @throws IllegalArgumentException if {@code initialRequestN} is not positive - */ - public static RequestStreamFrame createRequestStreamFrame( - ByteBufAllocator byteBufAllocator, - boolean follows, - int initialRequestN, - @Nullable ByteBuf metadata, - @Nullable ByteBuf data) { - - NumberUtils.requirePositive(initialRequestN, "initialRequestN must be positive"); - - ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, FrameType.REQUEST_STREAM); - - if (follows) { - byteBuf = setFollowsFlag(byteBuf); - } - - byteBuf = byteBuf.writeInt(initialRequestN); - byteBuf = appendMetadata(byteBufAllocator, byteBuf, metadata); - byteBuf = appendData(byteBuf, data); - - return RECYCLER.get().setByteBuf(byteBuf); - } - - @Override - public RequestStreamFrame createFragment( - ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { - - return createRequestStreamFrame(byteBufAllocator, true, getInitialRequestN(), metadata, data); - } - - @Override - public RequestStreamFrame createNonFragment( - ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { - - return createRequestStreamFrame(byteBufAllocator, false, getInitialRequestN(), metadata, data); - } - - /** - * Returns the initial requestN. - * - * @return the initial requestN - */ - public int getInitialRequestN() { - return getByteBuf().getInt(OFFSET_INITIAL_REQUEST_N); - } - - @Override - public ByteBuf getUnsafeData() { - return getData(OFFSET_METADATA_LENGTH); - } - - @Override - public @Nullable ByteBuf getUnsafeMetadata() { - return getMetadata(OFFSET_METADATA_LENGTH); - } - - @Override - public String toString() { - return "RequestStreamFrame{" - + "follows=" - + isFollowsFlagSet() - + ", initialRequestN=" - + getInitialRequestN() - + ", metadata=" - + mapMetadata(ByteBufUtil::hexDump) - + ", data=" - + mapData(ByteBufUtil::hexDump) - + '}'; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/ResumeFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/ResumeFrame.java deleted file mode 100644 index 1ea2eda31..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/ResumeFrame.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.util.ReferenceCountUtil.release; -import static io.rsocket.framing.FrameType.RESUME; -import static io.rsocket.framing.LengthUtils.getLengthAsUnsignedShort; -import static io.rsocket.util.NumberUtils.requireUnsignedShort; -import static io.rsocket.util.RecyclerFactory.createRecycler; -import static java.nio.charset.StandardCharsets.UTF_8; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import java.util.function.Function; - -/** - * An RSocket {@code RESUME} frame. - * - * @see Resume - * Frame - */ -public final class ResumeFrame extends AbstractRecyclableFrame { - - private static final int OFFSET_MAJOR_VERSION = FRAME_TYPE_AND_FLAGS_BYTES; - - private static final int OFFSET_MINOR_VERSION = OFFSET_MAJOR_VERSION + Short.BYTES; - - private static final int OFFSET_TOKEN_LENGTH = OFFSET_MINOR_VERSION + Short.BYTES; - - private static final int OFFSET_RESUME_IDENTIFICATION_TOKEN = OFFSET_TOKEN_LENGTH + Short.BYTES; - - private static final Recycler RECYCLER = createRecycler(ResumeFrame::new); - - private ResumeFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code RESUME} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param resumeIdentificationToken the resume identification token - * @param lastReceivedServerPosition the last received server position - * @param firstAvailableClientPosition the first available client position - * @return the {@code RESUME} frame - * @throws NullPointerException if {@code byteBufAllocator} or {@code resumeIdentificationToken} - * is {@code null} - */ - public static ResumeFrame createResumeFrame( - ByteBufAllocator byteBufAllocator, - String resumeIdentificationToken, - long lastReceivedServerPosition, - long firstAvailableClientPosition) { - - ByteBuf resumeIdentificationTokenByteBuf = - getUtf8AsByteBufRequired( - resumeIdentificationToken, "resumeIdentificationToken must not be null"); - - try { - return createResumeFrame( - byteBufAllocator, - resumeIdentificationTokenByteBuf, - lastReceivedServerPosition, - firstAvailableClientPosition); - } finally { - release(resumeIdentificationToken); - } - } - - /** - * Creates the {@code RESUME} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param resumeIdentificationToken the resume identification token - * @param lastReceivedServerPosition the last received server position - * @param firstAvailableClientPosition the first available client position - * @return the {@code RESUME} frame - * @throws NullPointerException if {@code byteBufAllocator} or {@code resumeIdentificationToken} - * is {@code null} - */ - public static ResumeFrame createResumeFrame( - ByteBufAllocator byteBufAllocator, - ByteBuf resumeIdentificationToken, - long lastReceivedServerPosition, - long firstAvailableClientPosition) { - - return createResumeFrame( - byteBufAllocator, - 1, - 0, - resumeIdentificationToken, - lastReceivedServerPosition, - firstAvailableClientPosition); - } - - /** - * Creates the {@code RESUME} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code RESUME} frame. - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static ResumeFrame createResumeFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code RESUME} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param majorVersion the major version of the protocol - * @param minorVersion the minor version of the protocol - * @param resumeIdentificationToken the resume identification token - * @param lastReceivedServerPosition the last received server position - * @param firstAvailableClientPosition the first available client position - * @return the {@code RESUME} frame - * @throws NullPointerException if {@code byteBufAllocator} or {@code resumeIdentificationToken} - * is {@code null} - */ - public static ResumeFrame createResumeFrame( - ByteBufAllocator byteBufAllocator, - int majorVersion, - int minorVersion, - ByteBuf resumeIdentificationToken, - long lastReceivedServerPosition, - long firstAvailableClientPosition) { - - Objects.requireNonNull(resumeIdentificationToken, "resumeIdentificationToken must not be null"); - - ByteBuf byteBuf = - createFrameTypeAndFlags(byteBufAllocator, RESUME) - .writeShort(requireUnsignedShort(majorVersion)) - .writeShort(requireUnsignedShort(minorVersion)); - - byteBuf = byteBuf.writeShort(getLengthAsUnsignedShort(resumeIdentificationToken)); - byteBuf = - Unpooled.wrappedBuffer( - byteBuf, resumeIdentificationToken.retain(), byteBufAllocator.buffer()); - - byteBuf = byteBuf.writeLong(lastReceivedServerPosition).writeLong(firstAvailableClientPosition); - - return RECYCLER.get().setByteBuf(byteBuf); - } - - /** - * Returns the first available client position. - * - * @return the first available client position - */ - public long getFirstAvailableClientPosition() { - return getByteBuf().getLong(getFirstAvailableClientPositionOffset()); - } - - /** - * Returns the last received server position. - * - * @return the last received server position - */ - public long getLastReceivedServerPosition() { - return getByteBuf().getLong(getLastReceivedServerPositionOffset()); - } - - /** - * Returns the major version of the protocol. - * - * @return the major version of the protocol - */ - public int getMajorVersion() { - return getByteBuf().getUnsignedShort(OFFSET_MAJOR_VERSION); - } - - /** - * Returns the minor version of the protocol. - * - * @return the minor version of the protocol - */ - public int getMinorVersion() { - return getByteBuf().getUnsignedShort(OFFSET_MINOR_VERSION); - } - - /** - * Returns the resume identification token as a UTF-8 {@link String}. - * - * @return the resume identification token as a UTF-8 {@link String} - */ - public String getResumeIdentificationTokenAsUtf8() { - return mapResumeIdentificationToken(byteBuf -> byteBuf.toString(UTF_8)); - } - - /** - * Returns the resume identification token directly. - * - *

Note: this resume identification token will be outside of the {@link Frame}'s - * lifecycle and may be released at any time. It is highly recommended that you {@link - * ByteBuf#retain()} the resume identification token if you store it. - * - * @return the resume identification token directly - * @see #mapResumeIdentificationToken(Function) - */ - public ByteBuf getUnsafeResumeIdentificationToken() { - return getByteBuf().slice(OFFSET_RESUME_IDENTIFICATION_TOKEN, getTokenLength()); - } - - /** - * Exposes the resume identification token for mapping to a different type. - * - * @param function the function to transform the resume identification token to a different type - * @param the different type - * @return the resume identification token mapped to a different type - * @throws NullPointerException if {@code function} is {@code null} - */ - public T mapResumeIdentificationToken(Function function) { - Objects.requireNonNull(function, "function must not be null"); - - return function.apply(getUnsafeResumeIdentificationToken()); - } - - @Override - public String toString() { - return "ResumeFrame{" - + "majorVersion=" - + getMajorVersion() - + ", minorVersion=" - + getMinorVersion() - + ", resumeIdentificationToken=" - + mapResumeIdentificationToken(ByteBufUtil::hexDump) - + ", lastReceivedServerPosition=" - + getLastReceivedServerPosition() - + ", firstAvailableClientPosition=" - + getFirstAvailableClientPosition() - + '}'; - } - - private int getFirstAvailableClientPositionOffset() { - return getLastReceivedServerPositionOffset() + Long.BYTES; - } - - private int getLastReceivedServerPositionOffset() { - return OFFSET_RESUME_IDENTIFICATION_TOKEN + getTokenLength(); - } - - private int getTokenLength() { - return getByteBuf().getUnsignedShort(OFFSET_TOKEN_LENGTH); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/ResumeOkFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/ResumeOkFrame.java deleted file mode 100644 index 5fbf2b98d..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/ResumeOkFrame.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.rsocket.framing.FrameType.RESUME_OK; -import static io.rsocket.util.RecyclerFactory.createRecycler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import java.util.Objects; - -/** - * An RSocket {@code RESUME_OK} frame. - * - * @see Resume - * OK Frame - */ -public final class ResumeOkFrame extends AbstractRecyclableFrame { - - private static final int OFFSET_LAST_RECEIVED_CLIENT_POSITION = FRAME_TYPE_AND_FLAGS_BYTES; - - private static final Recycler RECYCLER = createRecycler(ResumeOkFrame::new); - - private ResumeOkFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code RESUME_OK} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code RESUME_OK} frame. - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static ResumeOkFrame createResumeOkFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code RESUME_OK} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param lastReceivedClientPosition the last received server position - * @return the {@code RESUME_OK} frame - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - public static ResumeOkFrame createResumeOkFrame( - ByteBufAllocator byteBufAllocator, long lastReceivedClientPosition) { - - ByteBuf byteBuf = - createFrameTypeAndFlags(byteBufAllocator, RESUME_OK).writeLong(lastReceivedClientPosition); - - return RECYCLER.get().setByteBuf(byteBuf); - } - - /** - * Returns the last received client position. - * - * @return the last received client position - */ - public long getLastReceivedClientPosition() { - return getByteBuf().getLong(OFFSET_LAST_RECEIVED_CLIENT_POSITION); - } - - @Override - public String toString() { - return "ResumeOkFrame{" + "lastReceivedClientPosition=" + getLastReceivedClientPosition() + '}'; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/SetupFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/SetupFrame.java deleted file mode 100644 index 0591578a6..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/SetupFrame.java +++ /dev/null @@ -1,491 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.util.ReferenceCountUtil.release; -import static io.rsocket.framing.LengthUtils.getLengthAsUnsignedByte; -import static io.rsocket.framing.LengthUtils.getLengthAsUnsignedShort; -import static io.rsocket.util.NumberUtils.requireUnsignedShort; -import static io.rsocket.util.RecyclerFactory.createRecycler; -import static java.lang.Math.toIntExact; -import static java.nio.charset.StandardCharsets.UTF_8; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import io.rsocket.util.NumberUtils; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; -import reactor.util.annotation.Nullable; - -/** - * An RSocket {@code SETUP} frame. - * - * @see Setup - * Frame - */ -public final class SetupFrame extends AbstractRecyclableMetadataAndDataFrame { - - private static final int FLAG_LEASE = 1 << 6; - - private static final int FLAG_RESUME_ENABLED = 1 << 7; - - private static final int OFFSET_MAJOR_VERSION = FRAME_TYPE_AND_FLAGS_BYTES; - - private static final int OFFSET_MINOR_VERSION = OFFSET_MAJOR_VERSION + Short.BYTES; - - private static final int OFFSET_KEEPALIVE_INTERVAL = OFFSET_MINOR_VERSION + Short.BYTES; - - private static final int OFFSET_MAX_LIFETIME = OFFSET_KEEPALIVE_INTERVAL + Integer.BYTES; - - private static final int OFFSET_RESUME_IDENTIFICATION_TOKEN_LENGTH = - OFFSET_MAX_LIFETIME + Integer.BYTES; - - private static final Recycler RECYCLER = createRecycler(SetupFrame::new); - - private SetupFrame(Handle handle) { - super(handle); - } - - /** - * Creates the {@code SETUP} frame. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the {@code SETUP} frame. - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static SetupFrame createSetupFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the {@code SETUP} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param lease whether to set the Lease flag - * @param keepAliveInterval the time between {@code KEEPALIVE} frames - * @param maxLifetime the time between {@code KEEPALIVE} frames before the server is assumed to be - * dead - * @param resumeIdentificationToken the resume identification token - * @param metadataMimeType metadata MIME-type encoding - * @param dataMimeType data MIME-type encoding - * @param metadata the {@code metadata} - * @param data the {@code data} - * @return the {@code SETUP} frame - * @throws NullPointerException if {@code byteBufAllocator}, {@code keepAliveInterval}, {@code - * maxLifetime}, {@code metadataMimeType}, or {@code dataMimeType} is {@code null} - * @throws IllegalArgumentException if {@code keepAliveInterval} or {@code maxLifetime} is not a - * positive duration - */ - public static SetupFrame createSetupFrame( - ByteBufAllocator byteBufAllocator, - boolean lease, - Duration keepAliveInterval, - Duration maxLifetime, - @Nullable String resumeIdentificationToken, - String metadataMimeType, - String dataMimeType, - @Nullable String metadata, - @Nullable String data) { - - ByteBuf resumeIdentificationTokenByteBuf = getUtf8AsByteBuf(resumeIdentificationToken); - ByteBuf metadataByteBuf = getUtf8AsByteBuf(metadata); - ByteBuf dataByteBuf = getUtf8AsByteBuf(data); - - try { - return createSetupFrame( - byteBufAllocator, - lease, - keepAliveInterval, - maxLifetime, - resumeIdentificationTokenByteBuf, - metadataMimeType, - dataMimeType, - metadataByteBuf, - dataByteBuf); - } finally { - release(resumeIdentificationTokenByteBuf); - release(metadataByteBuf); - release(dataByteBuf); - } - } - - /** - * Creates the {@code SETUP} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param lease whether to set the Lease flag - * @param keepAliveInterval the time between {@code KEEPALIVE} frames - * @param maxLifetime the time between {@code KEEPALIVE} frames before the server is assumed to be - * dead - * @param resumeIdentificationToken the resume identification token - * @param metadataMimeType metadata MIME-type encoding - * @param dataMimeType data MIME-type encoding - * @param metadata the {@code metadata} - * @param data the {@code data} - * @return the {@code SETUP} frame - * @throws NullPointerException if {@code byteBufAllocator}, {@code keepAliveInterval}, {@code - * maxLifetime}, {@code metadataMimeType}, or {@code dataMimeType} is {@code null} - * @throws IllegalArgumentException if {@code keepAliveInterval} or {@code maxLifetime} is not a - * positive duration - */ - public static SetupFrame createSetupFrame( - ByteBufAllocator byteBufAllocator, - boolean lease, - Duration keepAliveInterval, - Duration maxLifetime, - @Nullable ByteBuf resumeIdentificationToken, - String metadataMimeType, - String dataMimeType, - @Nullable ByteBuf metadata, - @Nullable ByteBuf data) { - - return createSetupFrame( - byteBufAllocator, - lease, - 1, - 0, - keepAliveInterval, - maxLifetime, - resumeIdentificationToken, - metadataMimeType, - dataMimeType, - metadata, - data); - } - - /** - * Creates the {@code SETUP} frame. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param lease whether to set the Lease flag - * @param majorVersion the major version of the protocol - * @param minorVersion the minor version of the protocol - * @param keepAliveInterval the time between {@code KEEPALIVE} frames - * @param maxLifetime the time between {@code KEEPALIVE} frames before the server is assumed to be - * dead - * @param resumeIdentificationToken the resume identification token - * @param metadataMimeType metadata MIME-type encoding - * @param dataMimeType data MIME-type encoding - * @param metadata the {@code metadata} - * @param data the {@code data} - * @return the {@code SETUP} frame - * @throws NullPointerException if {@code byteBufAllocator}, {@code keepAliveInterval}, {@code - * maxLifetime}, {@code metadataMimeType}, or {@code dataMimeType} is {@code null} - * @throws IllegalArgumentException if {@code keepAliveInterval} or {@code maxLifetime} is not a - * positive duration - */ - public static SetupFrame createSetupFrame( - ByteBufAllocator byteBufAllocator, - boolean lease, - int majorVersion, - int minorVersion, - Duration keepAliveInterval, - Duration maxLifetime, - @Nullable ByteBuf resumeIdentificationToken, - String metadataMimeType, - String dataMimeType, - @Nullable ByteBuf metadata, - @Nullable ByteBuf data) { - - Objects.requireNonNull(keepAliveInterval, "keepAliveInterval must not be null"); - NumberUtils.requirePositive( - keepAliveInterval.toMillis(), "keepAliveInterval must be a positive duration"); - Objects.requireNonNull(maxLifetime, "maxLifetime must not be null"); - NumberUtils.requirePositive(maxLifetime.toMillis(), "maxLifetime must be a positive duration"); - - ByteBuf metadataMimeTypeByteBuf = - getUtf8AsByteBufRequired(metadataMimeType, "metadataMimeType must not be null"); - ByteBuf dataMimeTypeByteBuf = - getUtf8AsByteBufRequired(dataMimeType, "dataMimeType must not be null"); - - try { - ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, FrameType.SETUP); - - if (lease) { - byteBuf = setFlag(byteBuf, FLAG_LEASE); - } - - byteBuf = - byteBuf - .writeShort(requireUnsignedShort(majorVersion)) - .writeShort(requireUnsignedShort(minorVersion)) - .writeInt(toIntExact(keepAliveInterval.toMillis())) - .writeInt(toIntExact(maxLifetime.toMillis())); - - if (resumeIdentificationToken != null) { - byteBuf = - setFlag(byteBuf, FLAG_RESUME_ENABLED) - .writeShort(getLengthAsUnsignedShort(resumeIdentificationToken)); - byteBuf = - Unpooled.wrappedBuffer( - byteBuf, resumeIdentificationToken.retain(), byteBufAllocator.buffer()); - } - - byteBuf = byteBuf.writeByte(getLengthAsUnsignedByte(metadataMimeTypeByteBuf)); - byteBuf = - Unpooled.wrappedBuffer( - byteBuf, metadataMimeTypeByteBuf.retain(), byteBufAllocator.buffer()); - - byteBuf = byteBuf.writeByte(getLengthAsUnsignedByte(dataMimeTypeByteBuf)); - byteBuf = - Unpooled.wrappedBuffer(byteBuf, dataMimeTypeByteBuf.retain(), byteBufAllocator.buffer()); - - byteBuf = appendMetadata(byteBufAllocator, byteBuf, metadata); - byteBuf = appendData(byteBuf, data); - - return RECYCLER.get().setByteBuf(byteBuf); - } finally { - release(metadataMimeTypeByteBuf); - release(dataMimeTypeByteBuf); - } - } - - /** - * Returns the data MIME-type, decoded at {@link StandardCharsets#UTF_8}. - * - * @return the data MIME-type, decoded as {@link StandardCharsets#UTF_8} - */ - public String getDataMimeType() { - return getDataMimeType(UTF_8); - } - - /** - * Returns the data MIME-type. - * - * @param charset the {@link Charset} to decode the data MIME-type with - * @return the data MIME-type - */ - public String getDataMimeType(Charset charset) { - return getByteBuf().slice(getDataMimeTypeOffset(), getDataMimeTypeLength()).toString(charset); - } - - /** - * Returns the keep alive interval. - * - * @return the keep alive interval - */ - public Duration getKeepAliveInterval() { - return Duration.ofMillis(getByteBuf().getInt(OFFSET_KEEPALIVE_INTERVAL)); - } - - /** - * Returns the major version of the protocol. - * - * @return the major version of the protocol - */ - public int getMajorVersion() { - return getByteBuf().getUnsignedShort(OFFSET_MAJOR_VERSION); - } - - /** - * Returns the max lifetime. - * - * @return the max lifetime - */ - public Duration getMaxLifetime() { - return Duration.ofMillis(getByteBuf().getInt(OFFSET_MAX_LIFETIME)); - } - - /** - * Returns the metadata MIME-type, decoded at {@link StandardCharsets#UTF_8}. - * - * @return the metadata MIME-type, decoded as {@link StandardCharsets#UTF_8} - */ - public String getMetadataMimeType() { - return getMetadataMimeType(UTF_8); - } - - /** - * Returns the metadata MIME-type. - * - * @param charset the {@link Charset} to decode the metadata MIME-type with - * @return the metadata MIME-type - */ - public String getMetadataMimeType(Charset charset) { - return getByteBuf() - .slice(getMetadataMimeTypeOffset(), getMetadataMimeTypeLength()) - .toString(charset); - } - - /** - * Returns the minor version of the protocol. - * - * @return the minor version of the protocol - */ - public int getMinorVersion() { - return getByteBuf().getUnsignedShort(OFFSET_MINOR_VERSION); - } - - /** - * Returns the resume identification token as a UTF-8 {@link String}. If the Resume Enabled flag - * is not set, returns {@link Optional#empty()}. - * - * @return optionally, the resume identification token as a UTF-8 {@link String} - */ - public Optional getResumeIdentificationTokenAsUtf8() { - return Optional.ofNullable(getUnsafeResumeIdentificationTokenAsUtf8()); - } - - @Override - public ByteBuf getUnsafeData() { - return getData(getMetadataLengthOffset()); - } - - @Override - public @Nullable ByteBuf getUnsafeMetadata() { - return getMetadata(getMetadataLengthOffset()); - } - - /** - * Returns the resume identification token directly. If the Resume Enabled flag is not set, - * returns {@code null}. - * - *

Note: this resume identification token will be outside of the {@link Frame}'s - * lifecycle and may be released at any time. It is highly recommended that you {@link - * ByteBuf#retain()} the resume identification token if you store it. - * - * @return the resume identification token directly, or {@code null} if the Resume Enabled flag is - * not set - * @see #mapResumeIdentificationToken(Function) - */ - public @Nullable ByteBuf getUnsafeResumeIdentificationToken() { - if (!isFlagSet(FLAG_RESUME_ENABLED)) { - return null; - } - - ByteBuf byteBuf = getByteBuf(); - return byteBuf.slice( - getResumeIdentificationTokenOffset(), getResumeIdentificationTokenLength()); - } - - /** - * Returns the resume identification token as a UTF-8 {@link String}. If the Resume Enabled flag - * is not set, returns {@code null}. - * - * @return the resume identification token as a UTF-8 {@link String} or {@code null} if the Resume - * Enabled flag is not set. - * @see #getResumeIdentificationTokenAsUtf8() - */ - public @Nullable String getUnsafeResumeIdentificationTokenAsUtf8() { - ByteBuf byteBuf = getUnsafeResumeIdentificationToken(); - return byteBuf == null ? null : byteBuf.toString(UTF_8); - } - - /** - * Returns whether the lease flag is set. - * - * @return whether the lease flag is set - */ - public boolean isLeaseFlagSet() { - return isFlagSet(FLAG_LEASE); - } - - /** - * Exposes the resume identification token for mapping to a different type. If the Resume Enabled - * flag is not set, returns {@link Optional#empty()}. - * - * @param function the function to transform the resume identification token to a different type - * @param the different type - * @return optionally, the resume identification token mapped to a different type - * @throws NullPointerException if {@code function} is {@code null} - */ - public Optional mapResumeIdentificationToken(Function function) { - Objects.requireNonNull(function, "function must not be null"); - - return Optional.ofNullable(getUnsafeResumeIdentificationToken()).map(function); - } - - @Override - public String toString() { - return "SetupFrame{" - + "lease=" - + isLeaseFlagSet() - + ", majorVersion=" - + getMajorVersion() - + ", minorVersion=" - + getMinorVersion() - + ", keepAliveInterval=" - + getKeepAliveInterval() - + ", maxLifetime=" - + getMaxLifetime() - + ", resumeIdentificationToken=" - + mapResumeIdentificationToken(ByteBufUtil::hexDump) - + ", metadataMimeType=" - + getMetadataMimeType() - + ", dataMimeType=" - + getDataMimeType() - + ", metadata=" - + mapMetadata(ByteBufUtil::hexDump) - + ", data=" - + mapData(ByteBufUtil::hexDump) - + '}'; - } - - private int getDataMimeTypeLength() { - return getByteBuf().getUnsignedByte(getDataMimeTypeLengthOffset()); - } - - private int getDataMimeTypeLengthOffset() { - return getMetadataMimeTypeOffset() + getMetadataMimeTypeLength(); - } - - private int getDataMimeTypeOffset() { - return getDataMimeTypeLengthOffset() + Byte.BYTES; - } - - private int getMetadataLengthOffset() { - return getDataMimeTypeOffset() + getDataMimeTypeLength(); - } - - private int getMetadataMimeTypeLength() { - return getByteBuf().getUnsignedByte(getMetadataMimeTypeLengthOffset()); - } - - private int getMetadataMimeTypeLengthOffset() { - return getResumeIdentificationTokenOffset() + getResumeIdentificationTokenLength(); - } - - private int getMetadataMimeTypeOffset() { - return getMetadataMimeTypeLengthOffset() + Byte.BYTES; - } - - private int getResumeIdentificationTokenLength() { - if (isFlagSet(FLAG_RESUME_ENABLED)) { - return getByteBuf().getUnsignedShort(OFFSET_RESUME_IDENTIFICATION_TOKEN_LENGTH); - } else { - return 0; - } - } - - private int getResumeIdentificationTokenOffset() { - if (isFlagSet(FLAG_RESUME_ENABLED)) { - return OFFSET_RESUME_IDENTIFICATION_TOKEN_LENGTH + Short.BYTES; - } else { - return OFFSET_RESUME_IDENTIFICATION_TOKEN_LENGTH; - } - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/StreamIdFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/StreamIdFrame.java deleted file mode 100644 index d977d1fca..000000000 --- a/rsocket-core/src/main/java/io/rsocket/framing/StreamIdFrame.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.rsocket.util.RecyclerFactory.createRecycler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import java.util.function.Function; - -/** - * An RSocket frame with a stream id. - * - * @see Frame - * Header Format - */ -public final class StreamIdFrame extends AbstractRecyclableFrame { - - private static final Recycler RECYCLER = createRecycler(StreamIdFrame::new); - - private static final int STREAM_ID_BYTES = Integer.BYTES; - - private StreamIdFrame(Handle handle) { - super(handle); - } - - /** - * Creates the frame with a stream id. - * - * @param byteBuf the {@link ByteBuf} representing the frame - * @return the frame with a stream id - * @throws NullPointerException if {@code byteBuf} is {@code null} - */ - public static StreamIdFrame createStreamIdFrame(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf, "byteBuf must not be null"); - - return RECYCLER.get().setByteBuf(byteBuf.retain()); - } - - /** - * Creates the frame with a stream id. - * - * @param byteBufAllocator the {@code ByteBufAllocator} to use - * @param streamId the stream id - * @param frame the frame to prepend the stream id to - * @return the frame with a stream id - * @throws NullPointerException if {@code byteBufAllocator} or {@code frame} is {@code null} - */ - public static StreamIdFrame createStreamIdFrame( - ByteBufAllocator byteBufAllocator, int streamId, Frame frame) { - - Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); - Objects.requireNonNull(frame, "frame must not be null"); - - ByteBuf streamIdByteBuf = - frame.mapFrame( - frameByteBuf -> { - ByteBuf byteBuf = byteBufAllocator.buffer(STREAM_ID_BYTES).writeInt(streamId); - - return Unpooled.wrappedBuffer(byteBuf, frameByteBuf.retain()); - }); - - return RECYCLER.get().setByteBuf(streamIdByteBuf); - } - - /** - * Returns the stream id. - * - * @return the stream id - */ - public int getStreamId() { - return getByteBuf().getInt(0); - } - - /** - * Returns the frame without stream id directly. - * - *

Note: this frame without stream id will be outside of the {@link Frame}'s lifecycle - * and may be released at any time. It is highly recommended that you {@link ByteBuf#retain()} the - * frame without stream id if you store it. - * - * @return the frame without stream id directly - * @see #mapFrameWithoutStreamId(Function) - */ - public ByteBuf getUnsafeFrameWithoutStreamId() { - ByteBuf byteBuf = getByteBuf(); - return byteBuf.slice(STREAM_ID_BYTES, byteBuf.readableBytes() - STREAM_ID_BYTES).asReadOnly(); - } - - /** - * Exposes the {@link Frame} without the stream id as a {@link ByteBuf} for mapping to a different - * type. - * - * @param function the function to transform the {@link Frame} without the stream id as a {@link - * ByteBuf} to a different type - * @param the different type - * @return the {@link Frame} without the stream id as a {@link ByteBuf} mapped to a different type - * @throws NullPointerException if {@code function} is {@code null} - */ - public T mapFrameWithoutStreamId(Function function) { - Objects.requireNonNull(function, "function must not be null"); - - return function.apply(getUnsafeFrameWithoutStreamId()); - } - - @Override - public String toString() { - return "StreamIdFrame{" - + "streamId=" - + getStreamId() - + ", frameWithoutStreamId=" - + mapFrameWithoutStreamId(ByteBufUtil::hexDump) - + '}'; - } -} 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 4db60d835..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +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.framing.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; @@ -44,50 +46,76 @@ */ 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; - public ClientServerInputMultiplexer(DuplexConnection source, PluginRegistry plugins) { + private boolean setupReceived; + + 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,12 +127,17 @@ public ClientServerInputMultiplexer(DuplexConnection source, PluginRegistry plug break; } }, - t -> { - LOGGER.error("test", t); - dispose(); + ex -> { + setup.onError(ex); + server.onError(ex); + client.onError(ex); }); } + public DuplexConnection asClientServerConnection() { + return clientServerConnection; + } + public DuplexConnection asServerConnection() { return serverConnection; } @@ -113,8 +146,8 @@ public DuplexConnection asClientConnection() { return clientConnection; } - public DuplexConnection asStreamZeroConnection() { - return streamZeroConnection; + public DuplexConnection asSetupConnection() { + return setupConnection; } @Override @@ -134,43 +167,54 @@ public Mono 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 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 17372ea01..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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.CoreSubscriber; -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(CoreSubscriber 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; - - private 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/SwitchTransformFlux.java b/rsocket-core/src/main/java/io/rsocket/internal/SwitchTransformFlux.java deleted file mode 100644 index 226a9b78c..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/SwitchTransformFlux.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.internal; - -import java.util.Objects; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; -import java.util.function.BiFunction; - -import io.netty.util.ReferenceCountUtil; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscription; -import reactor.core.CoreSubscriber; -import reactor.core.Scannable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Operators; -import reactor.util.annotation.Nullable; - -public final class SwitchTransformFlux extends Flux { - - final Publisher source; - final BiFunction, Publisher> transformer; - - public SwitchTransformFlux( - Publisher source, BiFunction, Publisher> transformer) { - this.source = Objects.requireNonNull(source, "source"); - this.transformer = Objects.requireNonNull(transformer, "transformer"); - } - - @Override - public int getPrefetch() { - return 1; - } - - @Override - public void subscribe(CoreSubscriber actual) { - source.subscribe(new SwitchTransformMain<>(actual, transformer)); - } - - static final class SwitchTransformMain implements CoreSubscriber, Scannable { - - final CoreSubscriber actual; - final BiFunction, Publisher> transformer; - final SwitchTransformInner inner; - - Subscription s; - - volatile int once; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater ONCE = - AtomicIntegerFieldUpdater.newUpdater(SwitchTransformMain.class, "once"); - - SwitchTransformMain( - CoreSubscriber actual, - BiFunction, Publisher> transformer - ) { - this.actual = actual; - this.transformer = transformer; - this.inner = new SwitchTransformInner<>(this); - } - - @Override - @Nullable - public Object scanUnsafe(Attr key) { - if (key == Attr.CANCELLED) return s == Operators.cancelledSubscription(); - if (key == Attr.PREFETCH) return 1; - - return null; - } - - @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - s.request(1); - } - } - - @Override - public void onNext(T t) { - if (isCanceled()) { - return; - } - - if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { - try { - inner.first = t; - Publisher result = - Objects.requireNonNull(transformer.apply(t, inner), "The transformer returned a null value"); - result.subscribe(actual); - return; - } catch (Throwable e) { - onError(Operators.onOperatorError(s, e, t, actual.currentContext())); - ReferenceCountUtil.safeRelease(t); - return; - } - } - - inner.onNext(t); - } - - @Override - public void onError(Throwable t) { - if (isCanceled()) { - return; - } - - if (once != 0) { - inner.onError(t); - } else { - actual.onSubscribe(Operators.emptySubscription()); - actual.onError(t); - } - } - - @Override - public void onComplete() { - if (isCanceled()) { - return; - } - - if (once != 0) { - inner.onComplete(); - } else { - actual.onSubscribe(Operators.emptySubscription()); - actual.onComplete(); - } - } - - boolean isCanceled() { - return s == Operators.cancelledSubscription(); - } - - void cancel() { - s.cancel(); - s = Operators.cancelledSubscription(); - } - } - - static final class SwitchTransformInner extends Flux - implements Scannable, Subscription { - - final SwitchTransformMain parent; - - volatile CoreSubscriber actual; - @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater ACTUAL = - AtomicReferenceFieldUpdater.newUpdater(SwitchTransformInner.class, CoreSubscriber.class, "actual"); - - volatile V first; - @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater FIRST = - AtomicReferenceFieldUpdater.newUpdater(SwitchTransformInner.class, Object.class, "first"); - - volatile int once; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater ONCE = - AtomicIntegerFieldUpdater.newUpdater(SwitchTransformInner.class, "once"); - - SwitchTransformInner(SwitchTransformMain parent) { - this.parent = parent; - } - - public void onNext(V t) { - CoreSubscriber a = actual; - - if (a != null) { - a.onNext(t); - } - } - - public void onError(Throwable t) { - CoreSubscriber a = actual; - - if (a != null) { - a.onError(t); - } - } - - public void onComplete() { - CoreSubscriber a = actual; - - if (a != null) { - a.onComplete(); - } - } - - @Override - public void subscribe(CoreSubscriber actual) { - if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { - ACTUAL.lazySet(this, actual); - actual.onSubscribe(this); - } - else { - actual.onError(new IllegalStateException("SwitchTransform allows only one Subscriber")); - } - } - - @Override - public void request(long n) { - V f = first; - - if (f != null && FIRST.compareAndSet(this, f, null)) { - actual.onNext(f); - - long r = Operators.addCap(n, -1); - if (r > 0) { - parent.s.request(r); - } - } else { - parent.s.request(n); - } - } - - @Override - public void cancel() { - actual = null; - first = null; - parent.cancel(); - } - - @Override - @Nullable - public Object scanUnsafe(Attr key) { - if (key == Attr.PARENT) return parent; - if (key == Attr.ACTUAL) return actual(); - - return null; - } - - public CoreSubscriber actual() { - return actual; - } - } -} \ No newline at end of file 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 index 110ddd90d..94d5e9a7a 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -16,9 +16,11 @@ package io.rsocket.internal; -import io.netty.util.ReferenceCountUtil; +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; @@ -43,14 +45,19 @@ 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") @@ -63,24 +70,34 @@ public final class UnboundedProcessor extends FluxProcessor 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 = Queues.unboundedMultiproducer().get(); + this.queue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); + this.priorityQueue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); } @Override public int getBufferSize() { - return Queues.capacity(this.queue); + 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); } @@ -88,6 +105,7 @@ void drainRegular(Subscriber a) { int missed = 1; final Queue q = queue; + final Queue pq = priorityQueue; for (; ; ) { @@ -97,10 +115,17 @@ void drainRegular(Subscriber a) { while (r != e) { boolean d = done; - T t = q.poll(); + T t = pq.poll(); boolean empty = t == null; + if (empty) { + t = q.poll(); + empty = t == null; + } - if (checkTerminated(d, empty, a, q)) { + if (checkTerminated(d, empty, a)) { + if (!empty) { + release(t); + } return; } @@ -114,7 +139,7 @@ void drainRegular(Subscriber a) { } if (r == e) { - if (checkTerminated(done, q.isEmpty(), a, q)) { + if (checkTerminated(done, q.isEmpty() && pq.isEmpty(), a)) { return; } } @@ -130,8 +155,48 @@ void drainRegular(Subscriber a) { } } + 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() { - if (WIP.getAndIncrement(this) != 0) { + final int previousWip = WIP.getAndIncrement(this); + if (previousWip != 0) { + if (previousWip < 0 || terminated) { + this.clear(); + } return; } @@ -141,8 +206,11 @@ public void drain() { Subscriber a = actual; if (a != null) { - drainRegular(a); - + if (outputFused) { + drainFused(a); + } else { + drainRegular(a); + } return; } @@ -153,20 +221,16 @@ public void drain() { } } - boolean checkTerminated(boolean d, boolean empty, Subscriber a, Queue q) { + boolean checkTerminated(boolean d, boolean empty, Subscriber a) { if (cancelled) { - while (!q.isEmpty()) { - T t = q.poll(); - if (t != null) { - ReferenceCountUtil.safeRelease(t); - } - } - actual = null; + this.clear(); + hasDownstream = false; return true; } if (d && empty) { + this.clear(); Throwable e = error; - actual = null; + hasDownstream = false; if (e != null) { a.onError(e); } else { @@ -198,11 +262,28 @@ public Context currentContext() { 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()); - ReferenceCountUtil.safeRelease(t); + release(t); return; } @@ -210,7 +291,7 @@ public void onNext(T t) { Throwable ex = Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); onError(Operators.onOperatorError(null, ex, t, currentContext())); - ReferenceCountUtil.safeRelease(t); + release(t); return; } drain(); @@ -247,11 +328,7 @@ public void subscribe(CoreSubscriber actual) { actual.onSubscribe(this); this.actual = actual; - if (cancelled) { - this.actual = null; - } else { - drain(); - } + drain(); } else { Operators.error( actual, @@ -275,45 +352,104 @@ public void cancel() { cancelled = true; if (WIP.getAndIncrement(this) == 0) { - clear(); - actual = null; + 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 queue.size(); + return priorityQueue.size() + queue.size(); } @Override public boolean isEmpty() { - return queue.isEmpty(); + return priorityQueue.isEmpty() && queue.isEmpty(); } @Override public void clear() { - while (!queue.isEmpty()) { - T t = queue.poll(); - if (t != null) { - ReferenceCountUtil.safeRelease(t); + 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() { - cancel(); + 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 @@ -339,6 +475,19 @@ public long downstreamCount() { @Override public boolean hasDownstreams() { - return actual != null; + 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-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/MetaAttribute.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/PortableJvmInfo.java similarity index 54% rename from rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/MetaAttribute.java rename to rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/PortableJvmInfo.java index b4a0b5594..f037857e8 100644 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/MetaAttribute.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/PortableJvmInfo.java @@ -1,11 +1,9 @@ /* - * 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,16 +11,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package io.rsocket.internal.jctools.queues; -/* 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 +/** 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 09918f3d1..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 @@ -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 abf7eb7b7..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -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. @@ -69,4 +97,14 @@ default boolean isExpired() { 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 e173233ee..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -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-transport-aeron/src/main/java/io/rsocket/aeron/internal/TimedOutException.java b/rsocket-core/src/main/java/io/rsocket/lease/LeaseStats.java similarity index 72% rename from rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/TimedOutException.java rename to rsocket-core/src/main/java/io/rsocket/lease/LeaseStats.java index e77eecdfd..791f5a023 100644 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/TimedOutException.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/LeaseStats.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,15 @@ * limitations under the License. */ -package io.rsocket.aeron.internal; +package io.rsocket.lease; -public class TimedOutException extends RuntimeException { +public interface LeaseStats { - private static final long serialVersionUID = 6252022225519863073L; + 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 6700c10d9..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,5 +14,14 @@ * 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 243c1ab52..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,4 +14,16 @@ * 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 2b10f8746..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,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 873f6babb..000000000 --- a/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 1ac147687..000000000 --- a/rsocket-core/src/main/java/io/rsocket/plugins/Plugins.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 0bad0faed..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,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/test/java/io/rsocket/frame/RequestNFrameFlyweightTest.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java similarity index 54% rename from rsocket-core/src/test/java/io/rsocket/frame/RequestNFrameFlyweightTest.java rename to rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java index 050dd74be..415a77f92 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/RequestNFrameFlyweightTest.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,21 +14,25 @@ * limitations under the License. */ -package io.rsocket.frame; - -import static org.junit.Assert.assertEquals; +package io.rsocket.resume; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import org.junit.Test; +import java.time.Duration; + +public class ClientResume { + private final Duration sessionDuration; + private final ByteBuf resumeToken; -public class RequestNFrameFlyweightTest { - private final ByteBuf byteBuf = Unpooled.buffer(1024); + public ClientResume(Duration sessionDuration, ByteBuf resumeToken) { + this.sessionDuration = sessionDuration; + this.resumeToken = resumeToken; + } + + public Duration sessionDuration() { + return sessionDuration; + } - @Test - public void testEncoding() { - int encoded = RequestNFrameFlyweight.encode(byteBuf, 1, 5); - assertEquals("00000a00000001200000000005", ByteBufUtil.hexDump(byteBuf, 0, encoded)); + 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-aeron/src/main/java/io/rsocket/aeron/internal/NotConnectedException.java b/rsocket-core/src/main/java/io/rsocket/resume/RequestListener.java similarity index 58% rename from rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/NotConnectedException.java rename to rsocket-core/src/main/java/io/rsocket/resume/RequestListener.java index f97413b15..6553e5ec5 100644 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/NotConnectedException.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/RequestListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,17 +14,19 @@ * limitations under the License. */ -package io.rsocket.aeron.internal; +package io.rsocket.resume; -public class NotConnectedException extends RuntimeException { +import reactor.core.publisher.Flux; +import reactor.core.publisher.ReplayProcessor; - private static final long serialVersionUID = -5521573868855763403L; +class RequestListener { + private final ReplayProcessor requests = ReplayProcessor.create(1); - public NotConnectedException() { - super(); + public Flux apply(Flux flux) { + return flux.doOnRequest(requests::onNext); } - public NotConnectedException(String message) { - super(message); + 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/ResumeCache.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeCache.java deleted file mode 100644 index 2ddfe8f4d..000000000 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumeCache.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.resume; - -import io.rsocket.Frame; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import reactor.core.publisher.Flux; - -public class ResumeCache { - private final ResumePositionCounter strategy; - private final int maxBufferSize; - - private final LinkedHashMap frames = new LinkedHashMap<>(); - private int lastRemotePosition = 0; - private int currentPosition = 0; - private int bufferSize; - - public ResumeCache(ResumePositionCounter strategy, int maxBufferSize) { - this.strategy = strategy; - this.maxBufferSize = maxBufferSize; - } - - public void updateRemotePosition(int remotePosition) { - if (remotePosition > currentPosition) { - throw new IllegalStateException( - "Remote ahead of " + lastRemotePosition + " , expected " + remotePosition); - } - - if (remotePosition == lastRemotePosition) { - return; - } - - if (remotePosition < lastRemotePosition) { - throw new IllegalStateException( - "Remote position moved back from " + lastRemotePosition + " to " + remotePosition); - } - - lastRemotePosition = remotePosition; - - Iterator> positions = frames.entrySet().iterator(); - - while (positions.hasNext()) { - Map.Entry cachePosition = positions.next(); - - if (cachePosition.getKey() <= remotePosition) { - positions.remove(); - bufferSize -= strategy.cost(cachePosition.getValue()); - cachePosition.getValue().release(); - } - - // TODO check for a bad position - } - } - - public void sent(Frame frame) { - if (ResumeUtil.isTracked(frame)) { - frames.put(currentPosition, frame.copy()); - bufferSize += strategy.cost(frame); - - currentPosition += ResumeUtil.offset(frame); - - if (frames.size() > maxBufferSize) { - Frame f = frames.remove(first(frames)); - bufferSize -= strategy.cost(f); - } - } - } - - private int first(LinkedHashMap frames) { - return frames.keySet().iterator().next(); - } - - public Flux resend(int remotePosition) { - updateRemotePosition(remotePosition); - - if (remotePosition == currentPosition) { - return Flux.empty(); - } - - List resend = new ArrayList<>(); - - for (Map.Entry cachePosition : frames.entrySet()) { - if (remotePosition < cachePosition.getKey()) { - resend.add(cachePosition.getValue()); - } - - // TODO error handling - } - - return Flux.fromIterable(resend); - } - - public int getCurrentPosition() { - return currentPosition; - } - - public int getRemotePosition() { - return lastRemotePosition; - } - - public int getEarliestResendPosition() { - if (frames.isEmpty()) { - return currentPosition; - } else { - return first(frames); - } - } - - public int size() { - return bufferSize; - } -} 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/test/java/io/rsocket/framing/MetadataAndDataFrameTest.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateHolder.java similarity index 73% rename from rsocket-core/src/test/java/io/rsocket/framing/MetadataAndDataFrameTest.java rename to rsocket-core/src/main/java/io/rsocket/resume/ResumeStateHolder.java index 5d42b6e2a..31687a24b 100644 --- a/rsocket-core/src/test/java/io/rsocket/framing/MetadataAndDataFrameTest.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,11 @@ * limitations under the License. */ -package io.rsocket.framing; +package io.rsocket.resume; -interface MetadataAndDataFrameTest - extends MetadataFrameTest, DataFrameTest {} +public interface ResumeStateHolder { + + long impliedPosition(); + + void onImpliedPosition(long remoteImpliedPos); +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumePositionCounter.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStrategy.java similarity index 55% rename from rsocket-core/src/main/java/io/rsocket/resume/ResumePositionCounter.java rename to rsocket-core/src/main/java/io/rsocket/resume/ResumeStrategy.java index f85b5f31b..d9dec9f54 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumePositionCounter.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,14 @@ package io.rsocket.resume; -import io.rsocket.Frame; +import java.util.function.BiFunction; +import org.reactivestreams.Publisher; +import reactor.util.retry.Retry; /** - * Calculates the cost of a Frame when stored in the ResumeCache. Two obvious and provided - * strategies are simple frame counts and size in bytes. + * @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}. */ -public interface ResumePositionCounter { - int cost(Frame f); - - static ResumePositionCounter size() { - return ResumeUtil::offset; - } - - static ResumePositionCounter frames() { - return f -> 1; - } -} +@Deprecated +@FunctionalInterface +public interface ResumeStrategy extends BiFunction> {} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeToken.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeToken.java deleted file mode 100644 index 8f33f7951..000000000 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumeToken.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.resume; - -import io.netty.buffer.ByteBufUtil; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.UUID; - -public final class ResumeToken { - // TODO consider best format to store this - private final byte[] resumeToken; - - protected ResumeToken(byte[] resumeToken) { - this.resumeToken = resumeToken; - } - - public static ResumeToken bytes(byte[] token) { - return new ResumeToken(token); - } - - public static ResumeToken generate() { - return new ResumeToken(getBytesFromUUID(UUID.randomUUID())); - } - - static byte[] getBytesFromUUID(UUID uuid) { - ByteBuffer bb = ByteBuffer.wrap(new byte[16]); - bb.putLong(uuid.getMostSignificantBits()); - bb.putLong(uuid.getLeastSignificantBits()); - - return bb.array(); - } - - @Override - public int hashCode() { - return Arrays.hashCode(resumeToken); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof ResumeToken) { - return Arrays.equals(resumeToken, ((ResumeToken) obj).resumeToken); - } - - return false; - } - - @Override - public String toString() { - return ByteBufUtil.hexDump(resumeToken); - } - - public byte[] toByteArray() { - return resumeToken; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeUtil.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeUtil.java deleted file mode 100644 index da6b3262a..000000000 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumeUtil.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.resume; - -import io.rsocket.Frame; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.framing.FrameType; - -public class ResumeUtil { - public static boolean isTracked(FrameType frameType) { - switch (frameType) { - case REQUEST_CHANNEL: - case REQUEST_STREAM: - case REQUEST_RESPONSE: - case REQUEST_FNF: - // case METADATA_PUSH: - case REQUEST_N: - case CANCEL: - case ERROR: - case PAYLOAD: - return true; - default: - return false; - } - } - - public static boolean isTracked(Frame frame) { - return isTracked(frame.getType()); - } - - public static int offset(Frame frame) { - int length = frame.content().readableBytes(); - - if (length < FrameHeaderFlyweight.FRAME_HEADER_LENGTH) { - throw new IllegalStateException("invalid frame"); - } - - return length - FrameHeaderFlyweight.FRAME_LENGTH_SIZE; - } -} 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/framing/package-info.java b/rsocket-core/src/main/java/io/rsocket/resume/package-info.java similarity index 75% rename from rsocket-core/src/main/java/io/rsocket/framing/package-info.java rename to rsocket-core/src/main/java/io/rsocket/resume/package-info.java index 7b6ed9d69..98744386a 100644 --- a/rsocket-core/src/main/java/io/rsocket/framing/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/package-info.java @@ -15,11 +15,13 @@ */ /** - * Protocol framing types + * Contains support classes for the RSocket resume capability. * - * @see Framing + * @see Resuming + * Operation */ @NonNullApi -package io.rsocket.framing; +package io.rsocket.resume; import reactor.util.annotation.NonNullApi; 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 d5a8fe775..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -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 28af3fd4c..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,11 +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. - * @throws NullPointerException if {@code acceptor} is {@code null} + * @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 df44aaeaa..39386337c 100644 --- a/rsocket-core/src/main/java/io/rsocket/transport/Transport.java +++ b/rsocket-core/src/main/java/io/rsocket/transport/Transport.java @@ -16,5 +16,22 @@ 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 index e1c54bd86..16b863d9e 100644 --- a/rsocket-core/src/main/java/io/rsocket/transport/TransportHeaderAware.java +++ b/rsocket-core/src/main/java/io/rsocket/transport/TransportHeaderAware.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,10 @@ /** * 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 { /** 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 86e7c311a..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,5 +14,8 @@ * 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 ec3d4ab3c..000000000 --- a/rsocket-core/src/main/java/io/rsocket/uri/UriHandler.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.uri; - -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import java.net.URI; -import java.util.Optional; -import java.util.ServiceLoader; - -/** Maps a {@link URI} to a {@link ClientTransport} or {@link ServerTransport}. */ -public interface UriHandler { - - /** - * Load all registered instances of {@code UriHandler}. - * - * @return all registered instances of {@code UriHandler} - */ - static ServiceLoader loadServices() { - return ServiceLoader.load(UriHandler.class); - } - - /** - * Returns an implementation of {@link ClientTransport} unambiguously mapped to a {@link URI}, - * otherwise {@link Optional#EMPTY}. - * - * @param uri the uri to map - * @return an implementation of {@link ClientTransport} unambiguously mapped to a {@link URI}, * - * otherwise {@link Optional#EMPTY} - * @throws NullPointerException if {@code uri} is {@code null} - */ - Optional buildClient(URI uri); - - /** - * Returns an implementation of {@link ServerTransport} unambiguously mapped to a {@link URI}, - * otherwise {@link Optional#EMPTY}. - * - * @param uri the uri to map - * @return an implementation of {@link ServerTransport} unambiguously mapped to a {@link URI}, * - * otherwise {@link Optional#EMPTY} - * @throws NullPointerException if {@code uri} is {@code null} - */ - Optional buildServer(URI uri); -} 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 5275d2304..000000000 --- a/rsocket-core/src/main/java/io/rsocket/uri/UriTransportRegistry.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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/AbstractionLeakingFrameUtils.java b/rsocket-core/src/main/java/io/rsocket/util/AbstractionLeakingFrameUtils.java deleted file mode 100644 index 7e5a5d771..000000000 --- a/rsocket-core/src/main/java/io/rsocket/util/AbstractionLeakingFrameUtils.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.util; - -import static io.netty.util.ReferenceCountUtil.release; -import static io.rsocket.framing.FrameLengthFrame.createFrameLengthFrame; -import static io.rsocket.framing.StreamIdFrame.createStreamIdFrame; -import static io.rsocket.util.DisposableUtils.disposeQuietly; - -import io.netty.buffer.ByteBufAllocator; -import io.rsocket.Frame; -import io.rsocket.framing.FrameFactory; -import io.rsocket.framing.FrameLengthFrame; -import io.rsocket.framing.StreamIdFrame; -import java.util.Objects; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -public final class AbstractionLeakingFrameUtils { - - private AbstractionLeakingFrameUtils() {} - - /** - * Returns a {@link Tuple2} of the stream id, and the frame. This strips the frame length and - * stream id header from the abstraction leaking frame. - * - * @param abstractionLeakingFrame the abstraction leaking frame - * @return a {@link Tuple2} of the stream id, and the frame - * @throws NullPointerException if {@code abstractionLeakingFrame} is {@code null} - */ - public static Tuple2 fromAbstractionLeakingFrame( - Frame abstractionLeakingFrame) { - - Objects.requireNonNull(abstractionLeakingFrame, "abstractionLeakingFrame must not be null"); - - FrameLengthFrame frameLengthFrame = null; - StreamIdFrame streamIdFrame = null; - - try { - frameLengthFrame = createFrameLengthFrame(abstractionLeakingFrame.content()); - streamIdFrame = - frameLengthFrame.mapFrameWithoutFrameLength(StreamIdFrame::createStreamIdFrame); - - io.rsocket.framing.Frame frame = - streamIdFrame.mapFrameWithoutStreamId(FrameFactory::createFrame); - - return Tuples.of(streamIdFrame.getStreamId(), frame); - } finally { - disposeQuietly(frameLengthFrame, streamIdFrame); - release(abstractionLeakingFrame); - } - } - - /** - * Returns an abstraction leaking frame with the stream id and frame. This adds the frame length - * and stream id header to the frame. - * - * @param byteBufAllocator the {@link ByteBufAllocator} to use - * @param streamId the stream id - * @param frame the frame - * @return an abstraction leaking frame with the stream id and frame - * @throws NullPointerException if {@code byteBufAllocator} or {@code frame} is {@code null} - */ - public static Frame toAbstractionLeakingFrame( - ByteBufAllocator byteBufAllocator, int streamId, io.rsocket.framing.Frame frame) { - - Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); - Objects.requireNonNull(frame, "frame must not be null"); - - StreamIdFrame streamIdFrame = null; - FrameLengthFrame frameLengthFrame = null; - - try { - streamIdFrame = createStreamIdFrame(byteBufAllocator, streamId, frame); - frameLengthFrame = createFrameLengthFrame(byteBufAllocator, streamIdFrame); - - return frameLengthFrame.mapFrame(byteBuf -> Frame.from(byteBuf.retain())); - } finally { - disposeQuietly(frame, streamIdFrame, frameLengthFrame); - } - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java b/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java index 0b33e5e22..4cf33fa86 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java @@ -21,13 +21,14 @@ 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 javax.annotation.Nullable; +import reactor.util.annotation.Nullable; public final class ByteBufPayload extends AbstractReferenceCounted implements Payload { private static final Recycler RECYCLER = @@ -45,62 +46,6 @@ private ByteBufPayload(final Handle handle) { this.handle = handle; } - @Override - public boolean hasMetadata() { - return metadata != null; - } - - @Override - public ByteBuf sliceMetadata() { - return metadata == null ? Unpooled.EMPTY_BUFFER : metadata.slice(); - } - - @Override - public ByteBuf sliceData() { - 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() { - data.touch(); - if (metadata != null) { - metadata.touch(); - } - return this; - } - - @Override - public ByteBufPayload touch(Object hint) { - 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); - } - /** * Static factory method for a text payload. Mainly looks better than "new ByteBufPayload(data)" * @@ -168,9 +113,10 @@ public static Payload create(ByteBuf data) { public static Payload create(ByteBuf data, @Nullable ByteBuf metadata) { ByteBufPayload payload = RECYCLER.get(); - payload.setRefCnt(1); payload.data = data; payload.metadata = metadata; + // unsure data and metadata is set before refCnt change + payload.setRefCnt(1); return payload; } @@ -179,4 +125,95 @@ public static Payload create(Payload payload) { 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/DefaultPayload.java b/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java index 71bbf3874..08b8b2fb7 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java @@ -23,7 +23,7 @@ import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** * An implementation of {@link Payload}. This implementation is not thread-safe, and hence @@ -40,66 +40,6 @@ private DefaultPayload(ByteBuffer data, @Nullable ByteBuffer metadata) { this.metadata = metadata; } - @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 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; - } - /** * Static factory method for a text payload. Mainly looks better than "new DefaultPayload(data)" * @@ -159,12 +99,96 @@ public static Payload create(ByteBuf data) { } public static Payload create(ByteBuf data, @Nullable ByteBuf metadata) { - return create(data.nioBuffer(), metadata == null ? null : metadata.nioBuffer()); + 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( - Unpooled.copiedBuffer(payload.sliceData()), - payload.hasMetadata() ? Unpooled.copiedBuffer(payload.sliceMetadata()) : null); + 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/DisposableUtils.java b/rsocket-core/src/main/java/io/rsocket/util/DisposableUtils.java deleted file mode 100644 index c87a08220..000000000 --- a/rsocket-core/src/main/java/io/rsocket/util/DisposableUtils.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.rsocket.util; - -import java.util.Arrays; -import reactor.core.Disposable; - -/** Utilities for working with the {@link Disposable} type. */ -public final class DisposableUtils { - - private DisposableUtils() {} - - /** - * Calls the {@link Disposable#dispose()} method if the instance is not null. If any exceptions - * are thrown during disposal, suppress them. - * - * @param disposables the {@link Disposable}s to dispose - */ - public static void disposeQuietly(Disposable... disposables) { - Arrays.stream(disposables) - .forEach( - disposable -> { - try { - if (disposable != null) { - disposable.dispose(); - } - } catch (RuntimeException e) { - // Suppress any exceptions during disposal - } - }); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/util/EmptyPayload.java b/rsocket-core/src/main/java/io/rsocket/util/EmptyPayload.java index d5eda1d6b..99df97d70 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/EmptyPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/util/EmptyPayload.java @@ -40,6 +40,16 @@ 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; diff --git a/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java b/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java index 12e3cee45..3ff720447 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java +++ b/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java @@ -16,6 +16,7 @@ package io.rsocket.util; +import io.netty.buffer.ByteBuf; import java.util.Objects; public final class NumberUtils { @@ -143,4 +144,21 @@ public static int requireUnsignedShort(int i) { 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/RecyclerFactory.java b/rsocket-core/src/main/java/io/rsocket/util/RecyclerFactory.java deleted file mode 100644 index 30385195c..000000000 --- a/rsocket-core/src/main/java/io/rsocket/util/RecyclerFactory.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.util; - -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import java.util.function.Function; - -/** A factory for creating {@link Recycler}s. */ -public final class RecyclerFactory { - - /** - * Creates a new {@link Recycler}. - * - * @param newObjectCreator the {@link Function} to create a new object - * @param the type being recycled. - * @return the {@link Recycler} - * @throws NullPointerException if {@code newObjectCreator} is {@code null} - */ - public static Recycler createRecycler(Function, T> newObjectCreator) { - Objects.requireNonNull(newObjectCreator, "newObjectCreator must not be null"); - - return new Recycler() { - - @Override - protected T newObject(Handle handle) { - return newObjectCreator.apply(handle); - } - }; - } -} 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 79123d3b2..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,5 +14,8 @@ * 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 index b5a5f9ef8..82af5f53c 100644 --- a/rsocket-core/src/test/java/io/rsocket/FrameTest.java +++ b/rsocket-core/src/test/java/io/rsocket/FrameTest.java @@ -16,18 +16,11 @@ package io.rsocket; -import static org.junit.Assert.assertEquals; - -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.framing.FrameType; -import io.rsocket.util.DefaultPayload; -import org.junit.Test; - public class FrameTest { - @Test + /*@Test public void testFrameToString() { - final Frame requestFrame = - Frame.Request.from( + 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\" ", @@ -36,8 +29,8 @@ public void testFrameToString() { @Test public void testFrameWithMetadataToString() { - final Frame requestFrame = - Frame.Request.from( + final io.rsocket.Frame requestFrame = + io.rsocket.Frame.Request.from( 1, FrameType.REQUEST_RESPONSE, DefaultPayload.create("streaming in -> 0", "metadata"), @@ -49,12 +42,12 @@ public void testFrameWithMetadataToString() { @Test public void testPayload() { - Frame frame = - Frame.PayloadFrame.from( + 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/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java deleted file mode 100644 index abcddd37d..000000000 --- a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java +++ /dev/null @@ -1,153 +0,0 @@ -package io.rsocket; - -import io.netty.buffer.Unpooled; -import io.rsocket.exceptions.ConnectionErrorException; -import io.rsocket.framing.FrameType; -import io.rsocket.test.util.TestDuplexConnection; -import io.rsocket.util.DefaultPayload; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Stream; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -public class KeepAliveTest { - private static final int CLIENT_REQUESTER_TICK_PERIOD = 100; - private static final int CLIENT_REQUESTER_TIMEOUT = 700; - private static final int CLIENT_REQUESTER_MISSED_ACKS = 3; - private static final int SERVER_RESPONDER_TICK_PERIOD = 100; - private static final int SERVER_RESPONDER_TIMEOUT = 1000; - - @ParameterizedTest - @MethodSource("testData") - void keepAlives(Supplier testDataSupplier) { - TestData testData = testDataSupplier.get(); - TestDuplexConnection connection = testData.connection(); - - Flux.interval(Duration.ofMillis(100)) - .subscribe( - n -> connection.addToReceivedBuffer(Frame.Keepalive.from(Unpooled.EMPTY_BUFFER, true))); - - Mono.delay(Duration.ofMillis(1500)).block(); - - RSocket rSocket = testData.rSocket(); - List errors = testData.errors().errors(); - - Assertions.assertThat(rSocket.isDisposed()).isFalse(); - Assertions.assertThat(errors).isEmpty(); - } - - @ParameterizedTest - @MethodSource("testData") - void keepAlivesMissing(Supplier testDataSupplier) { - TestData testData = testDataSupplier.get(); - RSocket rSocket = testData.rSocket(); - - Mono.delay(Duration.ofMillis(1500)).block(); - - List errors = testData.errors().errors(); - Assertions.assertThat(rSocket.isDisposed()).isTrue(); - Assertions.assertThat(errors).hasSize(1); - Throwable throwable = errors.get(0); - Assertions.assertThat(throwable).isInstanceOf(ConnectionErrorException.class); - } - - @Test - void clientRequesterRespondsToKeepAlives() { - TestData testData = requester(100, 700, 3).get(); - TestDuplexConnection connection = testData.connection(); - - Mono.delay(Duration.ofMillis(100)) - .subscribe( - l -> connection.addToReceivedBuffer(Frame.Keepalive.from(Unpooled.EMPTY_BUFFER, true))); - - Mono keepAliveResponse = - Flux.from(connection.getSentAsPublisher()) - .filter(f -> f.getType() == FrameType.KEEPALIVE && !Frame.Keepalive.hasRespondFlag(f)) - .next() - .then(); - - StepVerifier.create(keepAliveResponse).expectComplete().verify(Duration.ofSeconds(5)); - } - - static Stream> testData() { - return Stream.of( - requester( - CLIENT_REQUESTER_TICK_PERIOD, CLIENT_REQUESTER_TIMEOUT, CLIENT_REQUESTER_MISSED_ACKS), - responder(SERVER_RESPONDER_TICK_PERIOD, SERVER_RESPONDER_TIMEOUT)); - } - - static Supplier requester(int tickPeriod, int timeout, int missedAcks) { - return () -> { - TestDuplexConnection connection = new TestDuplexConnection(); - Errors errors = new Errors(); - RSocketClient rSocket = - new RSocketClient( - connection, - DefaultPayload::create, - errors, - StreamIdSupplier.clientSupplier(), - Duration.ofMillis(tickPeriod), - Duration.ofMillis(timeout), - missedAcks); - return new TestData(rSocket, errors, connection); - }; - } - - static Supplier responder(int tickPeriod, int timeout) { - return () -> { - TestDuplexConnection connection = new TestDuplexConnection(); - AbstractRSocket handler = new AbstractRSocket() {}; - Errors errors = new Errors(); - RSocketServer rSocket = - new RSocketServer( - connection, handler, DefaultPayload::create, errors, tickPeriod, timeout); - return new TestData(rSocket, errors, connection); - }; - } - - static class TestData { - private final RSocket rSocket; - private final Errors errors; - private final TestDuplexConnection connection; - - public TestData(RSocket rSocket, Errors errors, TestDuplexConnection connection) { - this.rSocket = rSocket; - this.errors = errors; - this.connection = connection; - } - - public TestDuplexConnection connection() { - return connection; - } - - public RSocket rSocket() { - return rSocket; - } - - public Errors errors() { - return errors; - } - } - - static class Errors implements Consumer { - private final List errors = new ArrayList<>(); - - @Override - public void accept(Throwable throwable) { - errors.add(throwable); - } - - public List errors() { - return new ArrayList<>(errors); - } - } -} 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 4fcb65751..000000000 --- a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import static io.rsocket.framing.FrameType.*; -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.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; - -import io.rsocket.exceptions.ApplicationErrorException; -import io.rsocket.exceptions.RejectedSetupException; -import io.rsocket.frame.RequestFrameFlyweight; -import io.rsocket.framing.FrameType; -import io.rsocket.test.util.TestSubscriber; -import io.rsocket.util.DefaultPayload; -import io.rsocket.util.EmptyPayload; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; -import org.junit.Rule; -import org.junit.Test; -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.Mono; -import reactor.core.publisher.MonoProcessor; - -public class RSocketClientTest { - - @Rule public final ClientSocketRule rule = new ClientSocketRule(); - - @Test(timeout = 2_000) - public void testKeepAlive() throws Exception { - assertThat("Unexpected frame sent.", rule.connection.awaitSend().getType(), is(KEEPALIVE)); - } - - @Test(timeout = 2_000) - public void testInvalidFrameOnStream0() { - 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 testStreamInitialN() { - Flux stream = rule.socket.requestStream(EmptyPayload.INSTANCE); - - BaseSubscriber subscriber = - new BaseSubscriber() { - @Override - protected void hookOnSubscribe(Subscription subscription) { - // don't request here - // subscription.request(3); - } - }; - stream.subscribe(subscriber); - - subscriber.request(5); - - List sent = - rule.connection - .getSent() - .stream() - .filter(f -> f.getType() != KEEPALIVE) - .collect(Collectors.toList()); - - assertThat("sent frame count", sent.size(), is(1)); - - Frame f = sent.get(0); - - assertThat("initial frame", f.getType(), is(REQUEST_STREAM)); - assertThat("initial request n", RequestFrameFlyweight.initialRequestN(f.content()), is(5)); - } - - @Test(timeout = 2_000) - public void testHandleSetupException() { - 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() { - 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( - Frame.Error.from(streamId, new ApplicationErrorException("error"))); - - verify(responseSub).onError(any(ApplicationErrorException.class)); - } - - @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( - Frame.PayloadFrame.from(streamId, NEXT_COMPLETE, EmptyPayload.INSTANCE)); - - verify(sub).onNext(anyPayload()); - verify(sub).onComplete(); - } - - @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 = - rule.connection - .getSent() - .stream() - .filter(f -> f.getType() != KEEPALIVE) - .collect(Collectors.toList()); - - assertThat( - "Unexpected frame sent on the connection.", sent.get(0).getType(), is(REQUEST_RESPONSE)); - assertThat("Unexpected frame sent on the connection.", sent.get(1).getType(), is(CANCEL)); - } - - @Test(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.assertNoConnectionErrors(); - - verify(responseSub).onSubscribe(any(Subscription.class)); - - // TODO this should get the error reported through the response subscription - // verify(responseSub).onError(any(RuntimeException.class)); - } - - @Test(timeout = 2_000) - public void testLazyRequestResponse() { - Publisher response = rule.socket.requestResponse(EmptyPayload.INSTANCE); - int streamId = sendRequestResponse(response); - rule.connection.clearSendReceiveBuffers(); - int streamId2 = sendRequestResponse(response); - assertThat("Stream ID reused.", streamId2, not(equalTo(streamId))); - } - - @Test - 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(); - } - - 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, EmptyPayload.INSTANCE)); - verify(sub).onNext(anyPayload()); - verify(sub).onComplete(); - return streamId; - } - - public static class ClientSocketRule extends AbstractSocketRule { - @Override - protected RSocketClient newRSocket() { - return new RSocketClient( - connection, - DefaultPayload::create, - throwable -> errors.add(throwable), - StreamIdSupplier.clientSupplier(), - Duration.ofMillis(100), - Duration.ofMillis(100), - 4); - } - - 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/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java deleted file mode 100644 index db1ca2d65..000000000 --- a/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import 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.framing.FrameType; -import io.rsocket.test.util.TestDuplexConnection; -import io.rsocket.test.util.TestSubscriber; -import io.rsocket.util.DefaultPayload; -import io.rsocket.util.EmptyPayload; -import java.util.Collection; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.Ignore; -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) - @Ignore - 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) - @Ignore - 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) - @Ignore - 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() { - 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, DefaultPayload::create, throwable -> errors.add(throwable)); - } - - private void sendRequest(int streamId, FrameType frameType) { - Frame request = Frame.Request.from(streamId, frameType, EmptyPayload.INSTANCE, 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 a8ab2e6ac..000000000 --- a/rsocket-core/src/test/java/io/rsocket/RSocketTest.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; - -import io.rsocket.exceptions.ApplicationErrorException; -import io.rsocket.test.util.LocalDuplexConnection; -import io.rsocket.test.util.TestSubscriber; -import io.rsocket.util.DefaultPayload; -import io.rsocket.util.EmptyPayload; -import java.util.ArrayList; -import java.util.concurrent.CountDownLatch; -import org.hamcrest.MatcherAssert; -import org.junit.Assert; -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(DefaultPayload.create("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(EmptyPayload.INSTANCE).subscribe(subscriber); - verify(subscriber).onError(any(ApplicationErrorException.class)); - - // Client sees error through normal API - rule.assertNoClientErrors(); - - rule.assertServerError("java.lang.NullPointerException: Deliberate exception."); - } - - @Test(timeout = 2000) - public void testChannel() throws Exception { - CountDownLatch latch = new CountDownLatch(10); - Flux requests = - Flux.range(0, 10).map(i -> DefaultPayload.create("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 requestStream(Payload payload) { - return Flux.never(); - } - - @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 RSocketServer( - serverConnection, - requestAcceptor, - DefaultPayload::create, - throwable -> serverErrors.add(throwable)); - - crs = - new RSocketClient( - clientConnection, - DefaultPayload::create, - throwable -> clientErrors.add(throwable), - StreamIdSupplier.clientSupplier()); - } - - public void setRequestAcceptor(RSocket requestAcceptor) { - this.requestAcceptor = requestAcceptor; - init(); - } - - public void assertNoErrors() { - assertNoClientErrors(); - assertNoServerErrors(); - } - - public void assertNoClientErrors() { - MatcherAssert.assertThat( - "Unexpected error on the client connection.", clientErrors, is(empty())); - } - - public void assertNoServerErrors() { - MatcherAssert.assertThat( - "Unexpected error on the server connection.", serverErrors, is(empty())); - } - - public void assertClientError(String s) { - assertError(s, "client", this.clientErrors); - } - - public void assertServerError(String s) { - assertError(s, "server", this.serverErrors); - } - } - - public static void assertError(String s, String mode, ArrayList errors) { - for (Throwable t : errors) { - if (t.toString().equals(s)) { - return; - } - } - - Assert.fail("Expected " + mode + " connection error: " + s + " other errors " + errors.size()); - } -} 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/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 67% rename from rsocket-core/src/test/java/io/rsocket/AbstractSocketRule.java rename to rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java index 22568bfcc..7398548be 100644 --- a/rsocket-core/src/test/java/io/rsocket/AbstractSocketRule.java +++ b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java @@ -14,12 +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.Assert; import org.junit.rules.ExternalResource; import org.junit.runner.Description; import org.junit.runners.model.Statement; @@ -30,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(); } @@ -50,11 +54,18 @@ protected void init() { socket = newRSocket(); } + public void setMaxFrameLength(int maxFrameLength) { + this.maxFrameLength = maxFrameLength; + init(); + } + protected abstract T newRSocket(); - public void assertNoConnectionErrors() { - if (errors.size() > 1) { - Assert.fail("No connection errors expected: " + errors.peek().toString()); - } + 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/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java similarity index 52% rename from rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java rename to rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java index c4d402c7c..fe53b7df4 100644 --- a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -1,17 +1,25 @@ -package io.rsocket; +package io.rsocket.core; -import static io.rsocket.transport.ServerTransport.*; -import static org.assertj.core.api.Assertions.*; +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.framing.FrameType; +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 java.util.ArrayList; -import java.util.List; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.core.publisher.UnicastProcessor; @@ -25,13 +33,13 @@ void responderRejectSetup() { String errorMsg = "error"; RejectingAcceptor acceptor = new RejectingAcceptor(errorMsg); - RSocketFactory.receive().acceptor(acceptor).transport(transport).start().block(); + RSocketServer.create().acceptor(acceptor).bind(transport).block(); transport.connect(); - Frame sentFrame = transport.awaitSent(); - assertThat(sentFrame.getType()).isEqualTo(FrameType.ERROR); - RuntimeException error = Exceptions.from(sentFrame); + 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(); @@ -39,38 +47,63 @@ void responderRejectSetup() { } @Test + @Disabled("FIXME: needs to be revised") void requesterStreamsTerminatedOnZeroErrorFrame() { - TestDuplexConnection conn = new TestDuplexConnection(); - List errors = new ArrayList<>(); - RSocketClient rSocket = - new RSocketClient( - conn, DefaultPayload::create, errors::add, StreamIdSupplier.clientSupplier()); + 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"; - Mono.delay(Duration.ofMillis(100)) - .doOnTerminate( - () -> - conn.addToReceivedBuffer(Frame.Error.from(0, new RejectedSetupException(errorMsg)))) - .subscribe(); - - StepVerifier.create(rSocket.requestResponse(DefaultPayload.create("test"))) + 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(errors).hasSize(1); assertThat(rSocket.isDisposed()).isTrue(); } @Test void requesterNewStreamsTerminatedAfterZeroErrorFrame() { - TestDuplexConnection conn = new TestDuplexConnection(); - RSocketClient rSocket = - new RSocketClient( - conn, DefaultPayload::create, err -> {}, StreamIdSupplier.clientSupplier()); - - conn.addToReceivedBuffer(Frame.Error.from(0, new RejectedSetupException("error"))); + 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 @@ -83,13 +116,12 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { private static class RejectingAcceptor implements SocketAcceptor { private final String errorMessage; + private final UnicastProcessor senderRSockets = UnicastProcessor.create(); public RejectingAcceptor(String errorMessage) { this.errorMessage = errorMessage; } - private final UnicastProcessor senderRSockets = UnicastProcessor.create(); - @Override public Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket) { senderRSockets.onNext(sendingSocket); @@ -103,14 +135,16 @@ public Mono senderRSocket() { private static class SingleConnectionTransport implements ServerTransport { - private final TestDuplexConnection conn = new TestDuplexConnection(); + 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 Frame awaitSent() { + public ByteBuf awaitSent() { try { return conn.awaitSend(); } catch (InterruptedException e) { @@ -119,9 +153,9 @@ public Frame awaitSent() { } public void connect() { - Frame setup = - Frame.Setup.from( - 0, 42, 1, "mdMime", "dMime", DefaultPayload.create(DefaultPayload.EMPTY_BUFFER)); + Payload payload = DefaultPayload.create(DefaultPayload.EMPTY_BUFFER); + ByteBuf setup = SetupFrameCodec.encode(allocator, false, 0, 42, "mdMime", "dMime", payload); + conn.addToReceivedBuffer(setup); } } 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 55% rename from rsocket-core/src/test/java/io/rsocket/StreamIdSupplierTest.java rename to rsocket-core/src/test/java/io/rsocket/core/StreamIdSupplierTest.java index 008e0f45a..00248b6d8 100644 --- a/rsocket-core/src/test/java/io/rsocket/StreamIdSupplierTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/StreamIdSupplierTest.java @@ -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/ExceptionsTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java index 31c387fca..db5c47179 100644 --- a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java @@ -16,152 +16,223 @@ package io.rsocket.exceptions; -import static io.rsocket.frame.ErrorFrameFlyweight.APPLICATION_ERROR; -import static io.rsocket.frame.ErrorFrameFlyweight.CANCELED; -import static io.rsocket.frame.ErrorFrameFlyweight.CONNECTION_CLOSE; -import static io.rsocket.frame.ErrorFrameFlyweight.CONNECTION_ERROR; -import static io.rsocket.frame.ErrorFrameFlyweight.INVALID; -import static io.rsocket.frame.ErrorFrameFlyweight.INVALID_SETUP; -import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED; -import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED_RESUME; -import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED_SETUP; -import static io.rsocket.frame.ErrorFrameFlyweight.UNSUPPORTED_SETUP; -import static java.nio.charset.StandardCharsets.UTF_8; +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.Unpooled; -import io.rsocket.Frame; -import io.rsocket.frame.ErrorFrameFlyweight; +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(APPLICATION_ERROR, "test-message"); + ByteBuf byteBuf = createErrorFrame(1, APPLICATION_ERROR, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(1, byteBuf)) .isInstanceOf(ApplicationErrorException.class) - .withFailMessage("test-message"); + .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(CANCELED, "test-message"); + ByteBuf byteBuf = createErrorFrame(1, CANCELED, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(1, byteBuf)) .isInstanceOf(CanceledException.class) - .withFailMessage("test-message"); + .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(CONNECTION_CLOSE, "test-message"); + ByteBuf byteBuf = createErrorFrame(0, CONNECTION_CLOSE, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(0, byteBuf)) .isInstanceOf(ConnectionCloseException.class) - .withFailMessage("test-message"); + .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(CONNECTION_ERROR, "test-message"); + ByteBuf byteBuf = createErrorFrame(0, CONNECTION_ERROR, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(0, byteBuf)) .isInstanceOf(ConnectionErrorException.class) - .withFailMessage("test-message"); + .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(0x00000000, "test-message"); + ByteBuf byteBuf = createErrorFrame(0, 0x00000000, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) - .isInstanceOf(IllegalArgumentException.class) - .withFailMessage("Invalid Error frame: %d, '%s'", 0, "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(INVALID, "test-message"); + ByteBuf byteBuf = createErrorFrame(1, INVALID, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(1, byteBuf)) .isInstanceOf(InvalidException.class) - .withFailMessage("test-message"); + .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(INVALID_SETUP, "test-message"); + ByteBuf byteBuf = createErrorFrame(0, INVALID_SETUP, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(0, byteBuf)) .isInstanceOf(InvalidSetupException.class) - .withFailMessage("test-message"); + .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(REJECTED, "test-message"); + ByteBuf byteBuf = createErrorFrame(1, REJECTED, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + 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(REJECTED_RESUME, "test-message"); + ByteBuf byteBuf = createErrorFrame(0, REJECTED_RESUME, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(0, byteBuf)) .isInstanceOf(RejectedResumeException.class) - .withFailMessage("test-message"); + .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(REJECTED_SETUP, "test-message"); + ByteBuf byteBuf = createErrorFrame(0, REJECTED_SETUP, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + 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(UNSUPPORTED_SETUP, "test-message"); + ByteBuf byteBuf = createErrorFrame(0, UNSUPPORTED_SETUP, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(0, byteBuf)) .isInstanceOf(UnsupportedSetupException.class) - .withFailMessage("test-message"); + .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(null)) + .isThrownBy(() -> Exceptions.from(0, null)) .withMessage("frame must not be null"); } - private ByteBuf createErrorFrame(int errorCode, String message) { - ByteBuf byteBuf = Unpooled.buffer(); - - ErrorFrameFlyweight.encode(byteBuf, 0, errorCode, Unpooled.copiedBuffer(message, UTF_8)); - - return byteBuf; + 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/RSocketExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/RSocketExceptionTest.java index 8c39e8250..ccf7649d2 100644 --- a/rsocket-core/src/test/java/io/rsocket/exceptions/RSocketExceptionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/RSocketExceptionTest.java @@ -17,27 +17,22 @@ package io.rsocket.exceptions; 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; interface RSocketExceptionTest { - @DisplayName("constructor throws NullPointerException with null message") + @DisplayName("constructor does not throw NullPointerException with null message") @Test default void constructorWithNullMessage() { - assertThatNullPointerException() - .isThrownBy(() -> getException(null)) - .withMessage("message must not be null"); + assertThat(getException(null)).hasMessage(null); } - @DisplayName("constructor throws NullPointerException with null message and cause") + @DisplayName("constructor does not throw NullPointerException with null message and cause") @Test default void constructorWithNullMessageAndCause() { - assertThatNullPointerException() - .isThrownBy(() -> getException(null, new Exception())) - .withMessage("message must not be null"); + assertThat(getException(null)).hasMessage(null); } @DisplayName("errorCode returns specified value") 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/fragmentation/FragmentationDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java index 3e6d2c2a7..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,391 +16,97 @@ package io.rsocket.fragmentation; -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.PayloadFrame.createPayloadFrame; -import static io.rsocket.framing.RequestStreamFrame.createRequestStreamFrame; -import static io.rsocket.framing.TestFrames.createTestCancelFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -import static io.rsocket.util.AbstractionLeakingFrameUtils.toAbstractionLeakingFrame; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; 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.verify; -import static org.mockito.Mockito.when; +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.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.frame.*; +import java.util.concurrent.ThreadLocalRandom; +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.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; final class FragmentationDuplexConnectionTest { + 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); + { + Mockito.when(delegate.onClose()).thenReturn(Mono.never()); + } + @SuppressWarnings("unchecked") - private final ArgumentCaptor> publishers = + private final ArgumentCaptor> publishers = ArgumentCaptor.forClass(Publisher.class); + private LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + @DisplayName("constructor throws IllegalArgumentException with negative maxFragmentLength") @Test void constructorInvalidMaxFragmentSize() { assertThatIllegalArgumentException() - .isThrownBy(() -> new FragmentationDuplexConnection(DEFAULT, delegate, Integer.MIN_VALUE)) - .withMessage("maxFragmentSize must be positive"); + .isThrownBy( + () -> + new FragmentationDuplexConnection( + delegate, Integer.MIN_VALUE, Integer.MAX_VALUE, "")) + .withMessage("The smallest allowed mtu size is 64 bytes, provided: -2147483648"); } - @DisplayName("constructor throws NullPointerException with null byteBufAllocator") + @DisplayName("constructor throws IllegalArgumentException with negative maxFragmentLength") @Test - void constructorNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> new FragmentationDuplexConnection(null, delegate, 2)) - .withMessage("byteBufAllocator must not be null"); + 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 void constructorNullDelegate() { assertThatNullPointerException() - .isThrownBy(() -> new FragmentationDuplexConnection(DEFAULT, null, 2)) + .isThrownBy(() -> new FragmentationDuplexConnection(null, 64, Integer.MAX_VALUE, "")) .withMessage("delegate must not be null"); } - @DisplayName("reassembles data") - @Test - void reassembleData() { - ByteBuf data = getRandomByteBuf(6); - - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, null, data)); - - Frame fragment1 = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, null, data.slice(0, 2))); - - Frame fragment2 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, false, null, data.slice(2, 2))); - - Frame fragment3 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, data.slice(4, 2))); - - when(delegate.receive()).thenReturn(Flux.just(fragment1, fragment2, fragment3)); - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2) - .receive() - .as(StepVerifier::create) - .expectNext(frame) - .verifyComplete(); - } - - @DisplayName("reassembles metadata") - @Test - void reassembleMetadata() { - ByteBuf metadata = getRandomByteBuf(6); - - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, metadata, null)); - - Frame fragment1 = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null)); - - Frame fragment2 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null)); - - Frame fragment3 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, true, metadata.slice(4, 2), null)); - - when(delegate.receive()).thenReturn(Flux.just(fragment1, fragment2, fragment3)); - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2) - .receive() - .as(StepVerifier::create) - .expectNext(frame) - .verifyComplete(); - } - - @DisplayName("reassembles metadata and data") - @Test - void reassembleMetadataAndData() { - ByteBuf metadata = getRandomByteBuf(5); - ByteBuf data = getRandomByteBuf(5); - - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, metadata, data)); - - Frame fragment1 = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null)); - - Frame fragment2 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null)); - - Frame fragment3 = - toAbstractionLeakingFrame( - DEFAULT, - 1, - createPayloadFrame(DEFAULT, true, false, metadata.slice(4, 1), data.slice(0, 1))); - - Frame fragment4 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, false, null, data.slice(1, 2))); - - Frame fragment5 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, data.slice(3, 2))); - - when(delegate.receive()) - .thenReturn(Flux.just(fragment1, fragment2, fragment3, fragment4, fragment5)); - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2) - .receive() - .as(StepVerifier::create) - .expectNext(frame) - .verifyComplete(); - } - - @DisplayName("does not reassemble a non-fragment frame") - @Test - void reassembleNonFragment() { - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, true, (ByteBuf) null, null)); - - when(delegate.receive()).thenReturn(Flux.just(frame.retain())); - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2) - .receive() - .as(StepVerifier::create) - .expectNext(frame) - .verifyComplete(); - } - - @DisplayName("does not reassemble non fragmentable frame") - @Test - void reassembleNonFragmentableFrame() { - Frame frame = toAbstractionLeakingFrame(DEFAULT, 1, createTestCancelFrame()); - - when(delegate.receive()).thenReturn(Flux.just(frame.retain())); - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2) - .receive() - .as(StepVerifier::create) - .expectNext(frame) - .verifyComplete(); - } - @DisplayName("fragments data") @Test void sendData() { - ByteBuf data = getRandomByteBuf(6); - - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, null, data)); - - Frame fragment1 = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, null, data.slice(0, 2))); - - Frame fragment2 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, false, null, data.slice(2, 2))); - - Frame fragment3 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, data.slice(4, 2))); - - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame.retain()); - verify(delegate).send(publishers.capture()); - - StepVerifier.create(Flux.from(publishers.getValue())) - .expectNext(fragment1) - .expectNext(fragment2) - .expectNext(fragment3) - .verifyComplete(); - } - - @DisplayName("does not fragment with size equal to maxFragmentLength") - @Test - void sendEqualToMaxFragmentLength() { - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(2))); - - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame.retain()); - verify(delegate).send(publishers.capture()); - - StepVerifier.create(Flux.from(publishers.getValue())).expectNext(frame).verifyComplete(); - } - - @DisplayName("does not fragment an already-fragmented frame") - @Test - void sendFragment() { - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, (ByteBuf) null, null)); - - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame.retain()); - verify(delegate).send(publishers.capture()); - - StepVerifier.create(Flux.from(publishers.getValue())).expectNext(frame).verifyComplete(); - } - - @DisplayName("does not fragment with size smaller than maxFragmentLength") - @Test - void sendLessThanMaxFragmentLength() { - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(1))); + 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(DEFAULT, delegate, 2).sendOne(frame.retain()); - verify(delegate).send(publishers.capture()); - - StepVerifier.create(Flux.from(publishers.getValue())).expectNext(frame).verifyComplete(); - } - - @DisplayName("fragments metadata") - @Test - void sendMetadata() { - ByteBuf metadata = getRandomByteBuf(6); - - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, metadata, null)); - - Frame fragment1 = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null)); - - Frame fragment2 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null)); - - Frame fragment3 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, true, metadata.slice(4, 2), null)); - - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame.retain()); - verify(delegate).send(publishers.capture()); - - StepVerifier.create(Flux.from(publishers.getValue())) - .expectNext(fragment1) - .expectNext(fragment2) - .expectNext(fragment3) - .verifyComplete(); - } - - @DisplayName("fragments metadata and data") - @Test - void sendMetadataAndData() { - ByteBuf metadata = getRandomByteBuf(5); - ByteBuf data = getRandomByteBuf(5); - - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, metadata, data)); - - Frame fragment1 = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null)); - - Frame fragment2 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null)); - - Frame fragment3 = - toAbstractionLeakingFrame( - DEFAULT, - 1, - createPayloadFrame(DEFAULT, true, false, metadata.slice(4, 1), data.slice(0, 1))); - - Frame fragment4 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, false, null, data.slice(1, 2))); + new FragmentationDuplexConnection(delegate, 64, Integer.MAX_VALUE, "").sendOne(encode.retain()); - Frame fragment5 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, data.slice(3, 2))); - - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame.retain()); verify(delegate).send(publishers.capture()); StepVerifier.create(Flux.from(publishers.getValue())) - .expectNext(fragment1) - .expectNext(fragment2) - .expectNext(fragment3) - .expectNext(fragment4) - .expectNext(fragment5) + .expectNextCount(17) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertFalse(FrameHeaderCodec.hasFollows(byteBuf)); + }) .verifyComplete(); } - - @DisplayName("does not fragment non-fragmentable frame") - @Test - void sendNonFragmentable() { - Frame frame = toAbstractionLeakingFrame(DEFAULT, 1, createTestCancelFrame()); - - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame.retain()); - verify(delegate).send(publishers.capture()); - - StepVerifier.create(Flux.from(publishers.getValue())).expectNext(frame).verifyComplete(); - } - - @DisplayName("send throws NullPointerException with null frames") - @Test - void sendNullFrames() { - when(delegate.onClose()).thenReturn(Mono.never()); - - assertThatNullPointerException() - .isThrownBy(() -> new FragmentationDuplexConnection(DEFAULT, delegate, 2).send(null)) - .withMessage("frames must not be null"); - } - - @DisplayName("does not fragment with zero maxFragmentLength") - @Test - void sendZeroMaxFragmentLength() { - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(2))); - - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 0).sendOne(frame.retain()); - verify(delegate).send(publishers.capture()); - - StepVerifier.create(Flux.from(publishers.getValue())).expectNext(frame).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 efa1b5357..4548e4696 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java @@ -16,173 +16,335 @@ package io.rsocket.fragmentation; -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.PayloadFrame.createPayloadFrame; -import static io.rsocket.framing.RequestStreamFrame.createRequestStreamFrame; -import static io.rsocket.framing.TestFrames.createTestCancelFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - import io.netty.buffer.ByteBuf; -import io.rsocket.framing.CancelFrame; -import io.rsocket.framing.PayloadFrame; -import io.rsocket.framing.RequestStreamFrame; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.frame.*; +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; import reactor.test.StepVerifier; final class FrameFragmenterTest { + private static byte[] data = new byte[4096]; + private static byte[] metadata = new byte[4096]; - @DisplayName("constructor throws NullPointerException with null ByteBufAllocator") - @Test - void constructorNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> new FrameFragmenter(null, 2)) - .withMessage("byteBufAllocator must not be null"); + static { + ThreadLocalRandom.current().nextBytes(data); + ThreadLocalRandom.current().nextBytes(metadata); } - @DisplayName("fragments data") - @Test - void fragmentData() { - ByteBuf data = getRandomByteBuf(6); - - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, null, data); + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; - RequestStreamFrame fragment1 = - createRequestStreamFrame(DEFAULT, true, 1, null, data.slice(0, 2)); - - PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, false, null, data.slice(2, 2)); - - PayloadFrame fragment3 = createPayloadFrame(DEFAULT, false, false, null, data.slice(4, 2)); - - new FrameFragmenter(DEFAULT, 2) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(fragment1) - .expectNext(fragment2) - .expectNext(fragment3) - .verifyComplete(); + @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(); } - @DisplayName("does not fragment with size equal to maxFragmentLength") @Test - void fragmentEqualToMaxFragmentLength() { - PayloadFrame frame = createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(2)); - - new FrameFragmenter(DEFAULT, 2) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(frame) - .verifyComplete(); + 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(); + + data = FrameFragmenter.getMetadata(fnf, FrameType.REQUEST_FNF); + Assert.assertEquals(data, Unpooled.wrappedBuffer(metadata)); + data.release(); + + data = FrameFragmenter.getMetadata(rs, FrameType.REQUEST_STREAM); + Assert.assertEquals(data, Unpooled.wrappedBuffer(metadata)); + data.release(); + + data = FrameFragmenter.getMetadata(rc, FrameType.REQUEST_CHANNEL); + Assert.assertEquals(data, Unpooled.wrappedBuffer(metadata)); + data.release(); } - @DisplayName("does not fragment an already-fragmented frame") @Test - void fragmentFragment() { - PayloadFrame frame = createPayloadFrame(DEFAULT, true, true, (ByteBuf) null, null); + void returnEmptBufferWhenNoMetadataPresent() { + ByteBuf rr = + RequestResponseFrameCodec.encode(allocator, 1, true, null, Unpooled.wrappedBuffer(data)); - new FrameFragmenter(DEFAULT, 2) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(frame) - .verifyComplete(); + ByteBuf data = FrameFragmenter.getMetadata(rr, FrameType.REQUEST_RESPONSE); + Assert.assertEquals(data, Unpooled.EMPTY_BUFFER); + data.release(); } - @DisplayName("does not fragment with size smaller than maxFragmentLength") + @DisplayName("encode first frame") @Test - void fragmentLessThanMaxFragmentLength() { - PayloadFrame frame = createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(1)); - - new FrameFragmenter(DEFAULT, 2) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(frame) - .verifyComplete(); + void encodeFirstFrameWithData() { + ByteBuf rr = + RequestResponseFrameCodec.encode(allocator, 1, true, null, Unpooled.wrappedBuffer(data)); + + ByteBuf fragment = + FrameFragmenter.encodeFirstFragment( + allocator, + 256, + rr, + FrameType.REQUEST_RESPONSE, + 1, + Unpooled.EMPTY_BUFFER, + Unpooled.wrappedBuffer(data)); + + 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("fragments metadata") + @DisplayName("encode first channel frame") @Test - void fragmentMetadata() { - ByteBuf metadata = getRandomByteBuf(6); - - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, metadata, null); - - RequestStreamFrame fragment1 = - createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null); - - PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null); - - PayloadFrame fragment3 = createPayloadFrame(DEFAULT, false, true, metadata.slice(4, 2), null); - - new FrameFragmenter(DEFAULT, 2) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(fragment1) - .expectNext(fragment2) - .expectNext(fragment3) - .verifyComplete(); + 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)); + + 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)); + + ByteBuf data = RequestChannelFrameCodec.data(fragment); + ByteBuf byteBuf = Unpooled.wrappedBuffer(this.data).readSlice(data.readableBytes()); + Assert.assertEquals(byteBuf, data); + + Assert.assertFalse(FrameHeaderCodec.hasMetadata(fragment)); } - @DisplayName("fragments metadata and data") + @DisplayName("encode first stream frame") @Test - void fragmentMetadataAndData() { - ByteBuf metadata = getRandomByteBuf(5); - ByteBuf data = getRandomByteBuf(5); - - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, metadata, data); - - RequestStreamFrame fragment1 = - createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null); - - PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null); - - PayloadFrame fragment3 = - createPayloadFrame(DEFAULT, true, false, metadata.slice(4, 1), data.slice(0, 1)); - - PayloadFrame fragment4 = createPayloadFrame(DEFAULT, true, false, null, data.slice(1, 2)); - - PayloadFrame fragment5 = createPayloadFrame(DEFAULT, false, false, null, data.slice(3, 2)); + void encodeFirstWithDataStream() { + ByteBuf rc = + RequestStreamFrameCodec.encode(allocator, 1, true, 50, null, Unpooled.wrappedBuffer(data)); + + ByteBuf fragment = + FrameFragmenter.encodeFirstFragment( + allocator, + 256, + rc, + FrameType.REQUEST_STREAM, + 1, + Unpooled.EMPTY_BUFFER, + 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); + ByteBuf byteBuf = Unpooled.wrappedBuffer(this.data).readSlice(data.readableBytes()); + Assert.assertEquals(byteBuf, data); + + Assert.assertFalse(FrameHeaderCodec.hasMetadata(fragment)); + } - new FrameFragmenter(DEFAULT, 2) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(fragment1) - .expectNext(fragment2) - .expectNext(fragment3) - .expectNext(fragment4) - .expectNext(fragment5) - .verifyComplete(); + @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("does not fragment non-fragmentable frame") + @DisplayName("encode first stream frame with data and metadata") @Test - void fragmentNonFragmentable() { - CancelFrame frame = createTestCancelFrame(); + 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)); + } - new FrameFragmenter(DEFAULT, 2) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(frame) + @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("fragment throws NullPointerException with null frame") + @DisplayName("fragments frame with only metadata") @Test - void fragmentWithNullFrame() { - assertThatNullPointerException() - .isThrownBy(() -> new FrameFragmenter(DEFAULT, 2).fragment(null)) - .withMessage("frame must not be null"); + 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("does not fragment with zero maxFragmentLength") + @DisplayName("fragments frame with data and metadata") @Test - void fragmentZeroMaxFragmentLength() { - PayloadFrame frame = createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(2)); - - new FrameFragmenter(DEFAULT, 0) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(frame) + 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 05be0aad4..deabd5c9c 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java @@ -16,124 +16,529 @@ package io.rsocket.fragmentation; -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.fragmentation.FrameReassembler.createFrameReassembler; -import static io.rsocket.framing.PayloadFrame.createPayloadFrame; -import static io.rsocket.framing.RequestStreamFrame.createRequestStreamFrame; -import static io.rsocket.framing.TestFrames.createTestCancelFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - import io.netty.buffer.ByteBuf; -import io.rsocket.framing.CancelFrame; -import io.rsocket.framing.PayloadFrame; -import io.rsocket.framing.RequestStreamFrame; +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.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]; - @DisplayName("createFrameReassembler throws NullPointerException") - @Test - void createFrameReassemblerNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createFrameReassembler(null)) - .withMessage("byteBufAllocator must not be null"); + static { + ThreadLocalRandom.current().nextBytes(data); + ThreadLocalRandom.current().nextBytes(metadata); } + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + @DisplayName("reassembles data") @Test void reassembleData() { - ByteBuf data = getRandomByteBuf(6); - - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, null, data); - - RequestStreamFrame fragment1 = - createRequestStreamFrame(DEFAULT, true, 1, null, data.slice(0, 2)); - - PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, false, null, data.slice(2, 2)); - - PayloadFrame fragment3 = createPayloadFrame(DEFAULT, false, false, null, data.slice(4, 2)); - - FrameReassembler frameReassembler = createFrameReassembler(DEFAULT); + 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); + } - assertThat(frameReassembler.reassemble(fragment1)).isNull(); - assertThat(frameReassembler.reassemble(fragment2)).isNull(); - assertThat(frameReassembler.reassemble(fragment3)).isEqualTo(frame); + @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); } @DisplayName("reassembles metadata") @Test void reassembleMetadata() { - ByteBuf metadata = getRandomByteBuf(6); - - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, metadata, null); - - RequestStreamFrame fragment1 = - createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null); - - PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null); - - PayloadFrame fragment3 = createPayloadFrame(DEFAULT, false, true, metadata.slice(4, 2), null); + 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(); + } - FrameReassembler frameReassembler = createFrameReassembler(DEFAULT); + @DisplayName("reassembles metadata request channel") + @Test + 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); + } - assertThat(frameReassembler.reassemble(fragment1)).isNull(); - assertThat(frameReassembler.reassemble(fragment2)).isNull(); - assertThat(frameReassembler.reassemble(fragment3)).isEqualTo(frame); + @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); } @DisplayName("reassembles metadata and data") @Test void reassembleMetadataAndData() { - ByteBuf metadata = getRandomByteBuf(5); - ByteBuf data = getRandomByteBuf(5); - - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, metadata, data); - - RequestStreamFrame fragment1 = - createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null); - - PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null); - PayloadFrame fragment3 = - createPayloadFrame(DEFAULT, true, false, metadata.slice(4, 1), data.slice(0, 1)); - - PayloadFrame fragment4 = createPayloadFrame(DEFAULT, true, false, null, data.slice(1, 2)); - - PayloadFrame fragment5 = createPayloadFrame(DEFAULT, false, false, null, data.slice(3, 2)); - - FrameReassembler frameReassembler = createFrameReassembler(DEFAULT); - - assertThat(frameReassembler.reassemble(fragment1)).isNull(); - assertThat(frameReassembler.reassemble(fragment2)).isNull(); - assertThat(frameReassembler.reassemble(fragment3)).isNull(); - assertThat(frameReassembler.reassemble(fragment4)).isNull(); - assertThat(frameReassembler.reassemble(fragment5)).isEqualTo(frame); + 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); } - @DisplayName("does not reassemble a non-fragment frame") + @DisplayName("cancel removes inflight frames") @Test - void reassembleNonFragment() { - PayloadFrame frame = createPayloadFrame(DEFAULT, false, true, (ByteBuf) null, null); + 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)); + } - assertThat(createFrameReassembler(DEFAULT).reassemble(frame)).isEqualTo(frame); + @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("does not reassemble non fragmentable frame") + @DisplayName("throws error on empty fragment") @Test - void reassembleNonFragmentableFrame() { - CancelFrame frame = createTestCancelFrame(); - - assertThat(createFrameReassembler(DEFAULT).reassemble(frame)).isEqualTo(frame); + 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("reassemble throws NullPointerException with null frame") + @DisplayName("dispose should clean up maps") @Test - void reassembleNullFrame() { - assertThatNullPointerException() - .isThrownBy(() -> createFrameReassembler(DEFAULT).reassemble(null)) - .withMessage("frame must not be null"); + 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/framing/ByteBufRepresentation.java b/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java similarity index 68% rename from rsocket-core/src/test/java/io/rsocket/framing/ByteBufRepresentation.java rename to rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java index 89b2c128a..63300c718 100644 --- a/rsocket-core/src/test/java/io/rsocket/framing/ByteBufRepresentation.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package io.rsocket.framing; +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 { @@ -25,7 +25,17 @@ public final class ByteBufRepresentation extends StandardRepresentation { @Override protected String fallbackToStringOf(Object object) { if (object instanceof ByteBuf) { - return ByteBufUtil.prettyHexDump((ByteBuf) object); + 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 6afa0a00e..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/ErrorFrameFlyweightTest.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.frame; - -import static io.rsocket.frame.ErrorFrameFlyweight.*; -import static org.junit.Assert.assertEquals; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import io.rsocket.exceptions.*; -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("d", StandardCharsets.UTF_8)); - assertEquals("00000b000000012c000000020164", ByteBufUtil.hexDump(byteBuf, 0, encoded)); - - assertEquals(ErrorFrameFlyweight.APPLICATION_ERROR, ErrorFrameFlyweight.errorCode(byteBuf)); - assertEquals("d", ErrorFrameFlyweight.message(byteBuf)); - } -} 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 c3ccb31ba..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderFlyweightTest.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.frame; - -import static io.rsocket.frame.FrameHeaderFlyweight.FLAGS_M; -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.framing.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, FLAGS_M, FrameType.SETUP, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); - assertEquals(length, 12); // 72 bits - } - - @Test - public void frameLengthNullMetadata() { - int length = - FrameHeaderFlyweight.encode(byteBuf, 0, 0, FrameType.SETUP, null, 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, FLAGS_M, FrameType.SETUP, metadata, Unpooled.EMPTY_BUFFER); - assertEquals( - 4, - FrameHeaderFlyweight.decodeMetadataLength(byteBuf, FrameHeaderFlyweight.FRAME_HEADER_LENGTH) - .longValue()); - } - - @Test - public void dataLength() { - ByteBuf data = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4, 5}); - int length = - FrameHeaderFlyweight.encode( - byteBuf, 0, FLAGS_M, 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, FLAGS_M, 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, FLAGS_M, 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, FLAGS_M, FrameType.SETUP, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); - assertEquals(streamId, FrameHeaderFlyweight.streamId(byteBuf)); - } - - @Test - public void typeAndFlag() { - FrameType frameType = FrameType.REQUEST_FNF; - 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 RESERVED: - 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, null, 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 be5fdb13b..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 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.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/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 d8dca2fc4..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/RequestFrameFlyweightTest.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.frame; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import io.rsocket.Frame; -import io.rsocket.Payload; -import io.rsocket.framing.FrameType; -import io.rsocket.util.DefaultPayload; -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, - FrameHeaderFlyweight.FLAGS_M, - FrameType.REQUEST_STREAM, - 1, - Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), - Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - assertEquals( - "000010000000011900000000010000026d6464", ByteBufUtil.hexDump(byteBuf, 0, encoded)); - - Payload payload = - DefaultPayload.create(Frame.from(stringToBuf("000010000000011900000000010000026d6464"))); - - assertEquals("md", StandardCharsets.UTF_8.decode(payload.getMetadata()).toString()); - } - - @Test - public void testEncodingWithEmptyMetadata() { - int encoded = - RequestFrameFlyweight.encode( - byteBuf, - 1, - FrameHeaderFlyweight.FLAGS_M, - FrameType.REQUEST_STREAM, - 1, - Unpooled.copiedBuffer("", StandardCharsets.UTF_8), - Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - assertEquals("00000e0000000119000000000100000064", ByteBufUtil.hexDump(byteBuf, 0, encoded)); - - Payload payload = - DefaultPayload.create(Frame.from(stringToBuf("00000e0000000119000000000100000064"))); - - assertEquals("", StandardCharsets.UTF_8.decode(payload.getMetadata()).toString()); - } - - @Test - public void testEncodingWithNullMetadata() { - int encoded = - RequestFrameFlyweight.encode( - byteBuf, - 1, - 0, - FrameType.REQUEST_STREAM, - 1, - null, - Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - assertEquals("00000b0000000118000000000164", ByteBufUtil.hexDump(byteBuf, 0, encoded)); - - Payload payload = - DefaultPayload.create(Frame.from(stringToBuf("00000b0000000118000000000164"))); - - assertFalse(payload.hasMetadata()); - } - - private String bufToString(int encoded) { - return ByteBufUtil.hexDump(byteBuf, 0, encoded); - } - - private ByteBuf stringToBuf(CharSequence s) { - return Unpooled.wrappedBuffer(ByteBufUtil.decodeHexDump(s)); - } -} 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/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 68d9940a2..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.frame; - -import static org.junit.Assert.*; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import io.rsocket.framing.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/VersionFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/VersionCodecTest.java similarity index 53% rename from rsocket-core/src/test/java/io/rsocket/frame/VersionFlyweightTest.java rename to rsocket-core/src/test/java/io/rsocket/frame/VersionCodecTest.java index 181c8b44b..be7fb837b 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/VersionFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/VersionCodecTest.java @@ -16,33 +16,33 @@ package io.rsocket.frame; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; -import org.junit.Test; +import org.junit.jupiter.api.Test; -public class VersionFlyweightTest { +public class VersionCodecTest { @Test public void simple() { - int version = VersionFlyweight.encode(1, 0); - assertEquals(1, VersionFlyweight.major(version)); - assertEquals(0, VersionFlyweight.minor(version)); + int version = VersionCodec.encode(1, 0); + assertEquals(1, VersionCodec.major(version)); + assertEquals(0, VersionCodec.minor(version)); assertEquals(0x00010000, version); - assertEquals("1.0", VersionFlyweight.toString(version)); + assertEquals("1.0", VersionCodec.toString(version)); } @Test public void complex() { - int version = VersionFlyweight.encode(0x1234, 0x5678); - assertEquals(0x1234, VersionFlyweight.major(version)); - assertEquals(0x5678, VersionFlyweight.minor(version)); + int version = VersionCodec.encode(0x1234, 0x5678); + assertEquals(0x1234, VersionCodec.major(version)); + assertEquals(0x5678, VersionCodec.minor(version)); assertEquals(0x12345678, version); - assertEquals("4660.22136", VersionFlyweight.toString(version)); + assertEquals("4660.22136", VersionCodec.toString(version)); } @Test public void noShortOverflow() { - int version = VersionFlyweight.encode(43210, 43211); - assertEquals(43210, VersionFlyweight.major(version)); - assertEquals(43211, VersionFlyweight.minor(version)); + 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/LeaseFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/old/LeaseFrameFlyweightTest.java similarity index 80% rename from rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameFlyweightTest.java rename to rsocket-core/src/test/java/io/rsocket/frame/old/LeaseFrameFlyweightTest.java index 87fbe1a9b..ef4fcc6b0 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/old/LeaseFrameFlyweightTest.java @@ -14,18 +14,10 @@ * 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; +package io.rsocket.frame.old; public class LeaseFrameFlyweightTest { - private final ByteBuf byteBuf = Unpooled.buffer(1024); + /*private final ByteBuf byteBuf = Unpooled.buffer(1024); @Test public void size() { @@ -41,5 +33,5 @@ public void testEncoding() { 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/framing/CancelFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/CancelFrameTest.java deleted file mode 100644 index 325ee9f97..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/CancelFrameTest.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.CancelFrame.createCancelFrame; -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.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class CancelFrameTest implements FrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return CancelFrame::createCancelFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = Unpooled.buffer(2).writeShort(0b00100100_00000000); - CancelFrame frame = createCancelFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @DisplayName("creates CANCEL frame with ByteBufAllocator") - @Test - void createCancelFrameByteBufAllocator() { - ByteBuf expected = Unpooled.buffer(2).writeShort(0b00100100_00000000); - - assertThat(createCancelFrame(DEFAULT).mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("createCancelFrame throws NullPointerException with null byteBufAllocator") - @Test - void createCancelFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createCancelFrame((ByteBufAllocator) null)) - .withMessage("byteBufAllocator must not be null"); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/DataFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/DataFrameTest.java deleted file mode 100644 index 87c291a9b..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/DataFrameTest.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.Unpooled.EMPTY_BUFFER; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -import io.netty.buffer.ByteBuf; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; - -interface DataFrameTest extends FrameTest { - - @DisplayName("return data as UTF-8") - @Test - default void getDataAsUtf8() { - Tuple2 tuple = getFrameWithData(); - T frame = tuple.getT1(); - ByteBuf data = tuple.getT2(); - - assertThat(frame.getDataAsUtf8()).isEqualTo(data.toString(UTF_8)); - } - - @DisplayName("returns empty data as UTF-8") - @Test - default void getDataAsUtf8Empty() { - T frame = getFrameWithEmptyData(); - - assertThat(frame.getDataAsUtf8()).isEqualTo(""); - } - - @DisplayName("returns data length") - @Test - default void getDataLength() { - Tuple2 tuple = getFrameWithData(); - T frame = tuple.getT1(); - ByteBuf data = tuple.getT2(); - - assertThat(frame.getDataLength()).isEqualTo(data.readableBytes()); - } - - @DisplayName("returns empty data length") - @Test - default void getDataLengthEmpty() { - T frame = getFrameWithEmptyData(); - - assertThat(frame.getDataLength()).isEqualTo(0); - } - - Tuple2 getFrameWithData(); - - T getFrameWithEmptyData(); - - @DisplayName("returns unsafe data") - @Test - default void getUnsafeData() { - Tuple2 tuple = getFrameWithData(); - T frame = tuple.getT1(); - ByteBuf data = tuple.getT2(); - - assertThat(frame.getUnsafeData()).isEqualTo(data); - } - - @DisplayName("returns unsafe empty data") - @Test - default void getUnsafeDataEmpty() { - T frame = getFrameWithEmptyData(); - - assertThat(frame.getUnsafeData()).isEqualTo(EMPTY_BUFFER); - } - - @DisplayName("maps data") - @Test - default void mapData() { - Tuple2 tuple = getFrameWithData(); - T frame = tuple.getT1(); - ByteBuf data = tuple.getT2(); - - assertThat(frame.mapData(Function.identity())).isEqualTo(data); - } - - @DisplayName("maps empty data") - @Test - default void mapDataEmpty() { - T frame = getFrameWithEmptyData(); - - assertThat(frame.mapData(Function.identity())).isEqualTo(EMPTY_BUFFER); - } - - @DisplayName("mapData throws NullPointerException with null function") - @Test - default void mapDataNullFunction() { - T frame = getFrameWithEmptyData(); - - assertThatNullPointerException() - .isThrownBy(() -> frame.mapData(null)) - .withMessage("function must not be null"); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/ErrorFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/ErrorFrameTest.java deleted file mode 100644 index 450f4db33..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/ErrorFrameTest.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.Unpooled.EMPTY_BUFFER; -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.ErrorFrame.createErrorFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -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.Unpooled; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class ErrorFrameTest implements DataFrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return ErrorFrame::createErrorFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = - Unpooled.buffer(8) - .writeShort(0b00101100_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeBytes(getRandomByteBuf(2)); - - ErrorFrame frame = createErrorFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @Override - public Tuple2 getFrameWithData() { - ByteBuf data = getRandomByteBuf(2); - - ErrorFrame frame = - createErrorFrame( - Unpooled.buffer(8) - .writeShort(0b00101100_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeBytes(data, 0, data.readableBytes())); - - return Tuples.of(frame, data); - } - - @Override - public ErrorFrame getFrameWithEmptyData() { - return createErrorFrame( - Unpooled.buffer(6) - .writeShort(0b00101100_00000000) - .writeInt(0b00000000_00000000_00000000_01100100)); - } - - @DisplayName("creates KEEPALIVE frame with data") - @Test - void createErrorFrameData() { - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(8) - .writeShort(0b00101100_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeBytes(data, 0, data.readableBytes()); - - assertThat(createErrorFrame(DEFAULT, 100, data).mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("creates KEEPALIVE frame without data") - @Test - void createErrorFrameDataNull() { - ByteBuf expected = - Unpooled.buffer(6) - .writeShort(0b00101100_00000000) - .writeInt(0b00000000_00000000_00000000_01100100); - - assertThat(createErrorFrame(DEFAULT, 100, (ByteBuf) null).mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("createErrorFrame throws NullPointerException with null byteBufAllocator") - @Test - void createErrorFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createErrorFrame(null, 0, EMPTY_BUFFER)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("returns error code") - @Test - void getErrorCode() { - ErrorFrame frame = - createErrorFrame( - Unpooled.buffer(6) - .writeShort(0b00001100_00000000) - .writeInt(0b00000000_00000000_00000000_01100100)); - - assertThat(frame.getErrorCode()).isEqualTo(100); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/ErrorTypeTest.java b/rsocket-core/src/test/java/io/rsocket/framing/ErrorTypeTest.java deleted file mode 100644 index b8983db3a..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/ErrorTypeTest.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -final class ErrorTypeTest { - - @DisplayName("APPLICATION_ERROR characteristics") - @Test - void applicationError() { - assertThat(ErrorType.APPLICATION_ERROR).isEqualTo(0x00000201); - } - - @DisplayName("CANCELED characteristics") - @Test - void canceled() { - assertThat(ErrorType.CANCELED).isEqualTo(0x00000203); - } - - @DisplayName("CONNECTION_CLOSE characteristics") - @Test - void connectionClose() { - assertThat(ErrorType.CONNECTION_CLOSE).isEqualTo(0x00000102); - } - - @DisplayName("INVALID_SETUP characteristics") - @Test - void connectionError() { - assertThat(ErrorType.CONNECTION_ERROR).isEqualTo(0x00000101); - } - - @DisplayName("INVALID characteristics") - @Test - void invalid() { - assertThat(ErrorType.INVALID).isEqualTo(0x00000204); - } - - @DisplayName("INVALID_SETUP characteristics") - @Test - void invalidSetup() { - assertThat(ErrorType.INVALID_SETUP).isEqualTo(0x00000001); - } - - @DisplayName("REJECTED characteristics") - @Test - void rejected() { - assertThat(ErrorType.REJECTED).isEqualTo(0x00000202); - } - - @DisplayName("REJECTED_RESUME characteristics") - @Test - void rejectedResume() { - assertThat(ErrorType.REJECTED_RESUME).isEqualTo(0x00000004); - } - - @DisplayName("REJECTED_SETUP characteristics") - @Test - void rejectedSetup() { - assertThat(ErrorType.REJECTED_SETUP).isEqualTo(0x00000003); - } - - @DisplayName("RESERVED characteristics") - @Test - void reserved() { - assertThat(ErrorType.RESERVED).isEqualTo(0x00000000); - } - - @DisplayName("UNSUPPORTED_SETUP characteristics") - @Test - void unsupportedSetup() { - assertThat(ErrorType.UNSUPPORTED_SETUP).isEqualTo(0x00000002); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/ExtensionFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/ExtensionFrameTest.java deleted file mode 100644 index a7d4fcdec..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/ExtensionFrameTest.java +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.ExtensionFrame.createExtensionFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -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.Unpooled; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class ExtensionFrameTest implements MetadataAndDataFrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return ExtensionFrame::createExtensionFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = - Unpooled.buffer(13) - .writeShort(0b11111101_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(getRandomByteBuf(2)) - .writeBytes(getRandomByteBuf(2)); - - ExtensionFrame frame = createExtensionFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @Override - public Tuple2 getFrameWithData() { - ByteBuf metadata = getRandomByteBuf(2); - ByteBuf data = getRandomByteBuf(2); - - ExtensionFrame frame = - createExtensionFrame( - Unpooled.buffer(13) - .writeShort(0b11111101_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()) - .writeBytes(data, 0, data.readableBytes())); - - return Tuples.of(frame, data); - } - - @Override - public ExtensionFrame getFrameWithEmptyData() { - ByteBuf metadata = getRandomByteBuf(2); - - return createExtensionFrame( - Unpooled.buffer(11) - .writeShort(0b11111101_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes())); - } - - @Override - public ExtensionFrame getFrameWithEmptyMetadata() { - ByteBuf data = getRandomByteBuf(2); - - return createExtensionFrame( - Unpooled.buffer(11) - .writeShort(0b11111101_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes())); - } - - @Override - public Tuple2 getFrameWithMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - ByteBuf data = getRandomByteBuf(2); - - ExtensionFrame frame = - createExtensionFrame( - Unpooled.buffer(13) - .writeShort(0b11111101_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()) - .writeBytes(data, 0, data.readableBytes())); - - return Tuples.of(frame, metadata); - } - - @Override - public ExtensionFrame getFrameWithoutMetadata() { - ByteBuf data = getRandomByteBuf(2); - - return createExtensionFrame( - Unpooled.buffer(11) - .writeShort(0b11111100_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes())); - } - - @DisplayName("creates EXT frame with data") - @Test - void createExtensionFrameData() { - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(11) - .writeShort(0b11111100_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes()); - - assertThat(createExtensionFrame(DEFAULT, false, 100, null, data).mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("creates EXT frame with ignore flag") - @Test - void createExtensionFrameIgnore() { - ByteBuf expected = - Unpooled.buffer(9) - .writeShort(0b11111110_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000000); - - assertThat( - createExtensionFrame(DEFAULT, true, 100, (ByteBuf) null, null) - .mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("creates EXT frame with metadata") - @Test - void createExtensionFrameMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(11) - .writeShort(0b11111101_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()); - - assertThat( - createExtensionFrame(DEFAULT, false, 100, metadata, null).mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("creates EXT frame with metadata and data") - @Test - void createExtensionFrameMetadataAndData() { - ByteBuf metadata = getRandomByteBuf(2); - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(13) - .writeShort(0b11111101_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()) - .writeBytes(data, 0, data.readableBytes()); - - assertThat( - createExtensionFrame(DEFAULT, false, 100, metadata, data).mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("createExtensionFrame throws NullPointerException with null byteBufAllocator") - @Test - void createExtensionFrameNullByteBuf() { - assertThatNullPointerException() - .isThrownBy(() -> createExtensionFrame(null, true, 100, (ByteBuf) null, null)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("returns extended type") - @Test - void getExtendedType() { - ExtensionFrame frame = - createExtensionFrame( - Unpooled.buffer(9) - .writeShort(0b11111100_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.getExtendedType()).isEqualTo(100); - } - - @DisplayName("tests ignore flag not set") - @Test - void isIgnoreFlagSetFalse() { - ExtensionFrame frame = - createExtensionFrame( - Unpooled.buffer(11) - .writeShort(0b11111100_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.isIgnoreFlagSet()).isFalse(); - } - - @DisplayName("tests ignore flag set") - @Test - void isIgnoreFlagSetTrue() { - ExtensionFrame frame = - createExtensionFrame( - Unpooled.buffer(11) - .writeShort(0b11111110_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.isIgnoreFlagSet()).isTrue(); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/FragmentableFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/FragmentableFrameTest.java deleted file mode 100644 index caad306f2..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/FragmentableFrameTest.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -import io.netty.buffer.ByteBuf; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -interface FragmentableFrameTest extends MetadataAndDataFrameTest { - - @DisplayName("creates fragment") - @Test - default void createFragment() { - ByteBuf metadata = getRandomByteBuf(2); - ByteBuf data = getRandomByteBuf(2); - - FragmentableFrame frame = - getFrameWithoutFollowsFlagSet().createFragment(DEFAULT, metadata, data); - - assertThat(frame.isFollowsFlagSet()).isTrue(); - assertThat(frame.mapMetadata(Function.identity())).hasValue(metadata); - assertThat(frame.mapData(Function.identity())).isEqualTo(data); - } - - @DisplayName("createFragment throws NullPointerException with null ByteBufAllocator") - @Test - default void createInitialFragmentNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> getFrameWithoutFollowsFlagSet().createFragment(null, null, null)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("creates non-fragment") - @Test - default void createNonFragment() { - ByteBuf metadata = getRandomByteBuf(2); - ByteBuf data = getRandomByteBuf(2); - - FragmentableFrame frame = - getFrameWithoutFollowsFlagSet().createNonFragment(DEFAULT, metadata, data); - - assertThat(frame.isFollowsFlagSet()).isFalse(); - assertThat(frame.mapMetadata(Function.identity())).hasValue(metadata); - assertThat(frame.mapData(Function.identity())).isEqualTo(data); - } - - T getFrameWithFollowsFlagSet(); - - T getFrameWithoutFollowsFlagSet(); - - @DisplayName("tests follows flag not set") - @Test - default void isFollowFlagSetFalse() { - assertThat(getFrameWithoutFollowsFlagSet().isFollowsFlagSet()).isFalse(); - } - - @DisplayName("tests follows flag set") - @Test - default void isFollowFlagSetTrue() { - assertThat(getFrameWithFollowsFlagSet().isFollowsFlagSet()).isTrue(); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/FrameFactoryTest.java b/rsocket-core/src/test/java/io/rsocket/framing/FrameFactoryTest.java deleted file mode 100644 index 805707177..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/FrameFactoryTest.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.rsocket.framing.FrameFactory.createFrame; -import static io.rsocket.framing.TestFrames.createTestCancelFrame; -import static io.rsocket.framing.TestFrames.createTestErrorFrame; -import static io.rsocket.framing.TestFrames.createTestExtensionFrame; -import static io.rsocket.framing.TestFrames.createTestKeepaliveFrame; -import static io.rsocket.framing.TestFrames.createTestLeaseFrame; -import static io.rsocket.framing.TestFrames.createTestMetadataPushFrame; -import static io.rsocket.framing.TestFrames.createTestPayloadFrame; -import static io.rsocket.framing.TestFrames.createTestRequestChannelFrame; -import static io.rsocket.framing.TestFrames.createTestRequestFireAndForgetFrame; -import static io.rsocket.framing.TestFrames.createTestRequestNFrame; -import static io.rsocket.framing.TestFrames.createTestRequestResponseFrame; -import static io.rsocket.framing.TestFrames.createTestRequestStreamFrame; -import static io.rsocket.framing.TestFrames.createTestResumeFrame; -import static io.rsocket.framing.TestFrames.createTestResumeOkFrame; -import static io.rsocket.framing.TestFrames.createTestSetupFrame; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -final class FrameFactoryTest { - - @DisplayName("creates CANCEL frame") - @Test - void createFrameCancel() { - createTestCancelFrame() - .consumeFrame(byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(CancelFrame.class)); - } - - @DisplayName("creates ERROR frame") - @Test - void createFrameError() { - createTestErrorFrame() - .consumeFrame(byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(ErrorFrame.class)); - } - - @DisplayName("creates EXT frame") - @Test - void createFrameExtension() { - createTestExtensionFrame() - .consumeFrame( - byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(ExtensionFrame.class)); - } - - @DisplayName("creates KEEPALIVE frame") - @Test - void createFrameKeepalive() { - createTestKeepaliveFrame() - .consumeFrame( - byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(KeepaliveFrame.class)); - } - - @DisplayName("creates METADATA_PUSH frame") - @Test - void createFrameMetadataPush() { - createTestMetadataPushFrame() - .consumeFrame( - byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(MetadataPushFrame.class)); - } - - @DisplayName("creates PAYLOAD frame") - @Test - void createFramePayload() { - createTestPayloadFrame() - .consumeFrame(byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(PayloadFrame.class)); - } - - @DisplayName("creates REQUEST_CHANNEL frame") - @Test - void createFrameRequestChannel() { - createTestRequestChannelFrame() - .consumeFrame( - byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(RequestChannelFrame.class)); - } - - @DisplayName("creates REQUEST_FNF frame") - @Test - void createFrameRequestFireAndForget() { - createTestRequestFireAndForgetFrame() - .consumeFrame( - byteBuf -> - assertThat(createFrame(byteBuf)).isInstanceOf(RequestFireAndForgetFrame.class)); - } - - @DisplayName("creates REQUEST_N frame") - @Test - void createFrameRequestN() { - createTestRequestNFrame() - .consumeFrame( - byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(RequestNFrame.class)); - } - - @DisplayName("creates REQUEST_RESPONSE frame") - @Test - void createFrameRequestResponse() { - createTestRequestResponseFrame() - .consumeFrame( - byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(RequestResponseFrame.class)); - } - - @DisplayName("creates REQUEST_STREAM frame") - @Test - void createFrameRequestStream() { - createTestRequestStreamFrame() - .consumeFrame( - byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(RequestStreamFrame.class)); - } - - @DisplayName("creates RESUME frame") - @Test - void createFrameResume() { - createTestResumeFrame() - .consumeFrame(byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(ResumeFrame.class)); - } - - @DisplayName("creates RESUME_OK frame") - @Test - void createFrameResumeOk() { - createTestResumeOkFrame() - .consumeFrame( - byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(ResumeOkFrame.class)); - } - - @DisplayName("creates SETUP frame") - @Test - void createFrameSetup() { - createTestSetupFrame() - .consumeFrame(byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(SetupFrame.class)); - } - - @DisplayName("creates LEASE frame") - @Test - void createLeaseSetup() { - createTestLeaseFrame() - .consumeFrame(byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(LeaseFrame.class)); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/FrameLengthFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/FrameLengthFrameTest.java deleted file mode 100644 index 7bcd56396..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/FrameLengthFrameTest.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.FrameLengthFrame.createFrameLengthFrame; -import static io.rsocket.framing.FrameType.CANCEL; -import static io.rsocket.framing.TestFrames.createTestCancelFrame; -import static io.rsocket.framing.TestFrames.createTestFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -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.Unpooled; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class FrameLengthFrameTest implements FrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return FrameLengthFrame::createFrameLengthFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = - Unpooled.buffer(5) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(getRandomByteBuf(2)); - - FrameLengthFrame frame = createFrameLengthFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @DisplayName("creates frame length frame with ByteBufAllocator") - @Test - void createFrameLengthFrameByteBufAllocator() { - ByteBuf frame = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(5) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(frame, 0, frame.readableBytes()); - - assertThat( - createFrameLengthFrame(DEFAULT, createTestFrame(CANCEL, frame)) - .mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("createFrameLengthFrame throws NullPointerException with null byteBufAllocator") - @Test - void createFrameLengthFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createFrameLengthFrame(null, createTestCancelFrame())) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("returns frame length") - @Test - void getFrameLength() { - FrameLengthFrame frame = - createFrameLengthFrame(Unpooled.buffer(3).writeMedium(0b00000000_00000000_01100100)); - - assertThat(frame.getFrameLength()).isEqualTo(100); - } - - @DisplayName("maps byteBuf without frame length") - @Test - void mapFrameWithoutFrameLength() { - ByteBuf frame = getRandomByteBuf(2); - - FrameLengthFrame frameLengthFrame = - createFrameLengthFrame( - Unpooled.buffer(5) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(frame, 0, frame.readableBytes())); - - assertThat(frameLengthFrame.mapFrameWithoutFrameLength(Function.identity())).isEqualTo(frame); - } - - @DisplayName("mapFrameWithoutFrameLength throws NullPointerException with null function") - @Test - void mapFrameWithoutFrameLengthNullFunction() { - ByteBuf frame = getRandomByteBuf(2); - - FrameLengthFrame frameLengthFrame = - createFrameLengthFrame( - Unpooled.buffer(5) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(frame, 0, frame.readableBytes())); - - assertThatNullPointerException() - .isThrownBy(() -> frameLengthFrame.mapFrameWithoutFrameLength(null)) - .withMessage("function must not be null"); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/FrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/FrameTest.java deleted file mode 100644 index ef6f5335b..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/FrameTest.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -import io.netty.buffer.ByteBuf; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; - -interface FrameTest { - - @DisplayName("consumes frame") - @Test - default void consumeFrame() { - Tuple2 tuple = getFrame(); - T frame = tuple.getT1(); - ByteBuf byteBuf = tuple.getT2(); - - frame.consumeFrame(frameByteBuf -> assertThat(frameByteBuf).isEqualTo(byteBuf)); - } - - @DisplayName("consumeFrame throws NullPointerException with null function") - @Test - default void consumeFrameNullFunction() { - Tuple2 tuple = getFrame(); - T frame = tuple.getT1(); - - assertThatNullPointerException() - .isThrownBy(() -> frame.consumeFrame(null)) - .withMessage("consumer must not be null"); - } - - @DisplayName("creates frame from ByteBuf") - @Test - default void createFrameFromByteBuf() { - Tuple2 tuple = getFrame(); - T frame = tuple.getT1(); - ByteBuf byteBuf = tuple.getT2(); - - assertThat(getCreateFrameFromByteBuf().apply(byteBuf)).isEqualTo(frame); - } - - @DisplayName("create frame from ByteBuf throws NullPointerException with null ByteBuf") - @Test - default void createFrameFromByteBufNullByteBuf() { - assertThatNullPointerException() - .isThrownBy(() -> getCreateFrameFromByteBuf().apply(null)) - .withMessage("byteBuf must not be null"); - } - - Function getCreateFrameFromByteBuf(); - - Tuple2 getFrame(); - - @DisplayName("maps frame") - @Test - default void mapFrame() { - Tuple2 tuple = getFrame(); - T frame = tuple.getT1(); - ByteBuf byteBuf = tuple.getT2(); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(byteBuf); - } - - @DisplayName("mapFrame throws NullPointerException with null function") - @Test - default void mapFrameNullFunction() { - Tuple2 tuple = getFrame(); - T frame = tuple.getT1(); - - assertThatNullPointerException() - .isThrownBy(() -> frame.mapFrame(null)) - .withMessage("function must not be null"); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/FrameTypeTest.java b/rsocket-core/src/test/java/io/rsocket/framing/FrameTypeTest.java deleted file mode 100644 index 1364c9038..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/FrameTypeTest.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -final class FrameTypeTest { - - @DisplayName("CANCEL characteristics") - @Test - void cancel() { - assertThat(FrameType.CANCEL.canHaveData()).isFalse(); - assertThat(FrameType.CANCEL.canHaveMetadata()).isFalse(); - assertThat(FrameType.CANCEL.hasInitialRequestN()).isFalse(); - assertThat(FrameType.CANCEL.getEncodedType()).isEqualTo(0x09); - assertThat(FrameType.CANCEL.isFragmentable()).isFalse(); - assertThat(FrameType.CANCEL.isRequestType()).isFalse(); - } - - @DisplayName("COMPLETE characteristics") - @Test - void complete() { - assertThat(FrameType.COMPLETE.canHaveData()).isFalse(); - assertThat(FrameType.COMPLETE.canHaveMetadata()).isFalse(); - assertThat(FrameType.COMPLETE.hasInitialRequestN()).isFalse(); - assertThat(FrameType.COMPLETE.getEncodedType()).isEqualTo(0xB0); - assertThat(FrameType.COMPLETE.isFragmentable()).isFalse(); - assertThat(FrameType.COMPLETE.isRequestType()).isFalse(); - } - - @DisplayName("ERROR characteristics") - @Test - void error() { - assertThat(FrameType.ERROR.canHaveData()).isTrue(); - assertThat(FrameType.ERROR.canHaveMetadata()).isFalse(); - assertThat(FrameType.ERROR.getEncodedType()).isEqualTo(0x0B); - assertThat(FrameType.ERROR.hasInitialRequestN()).isFalse(); - assertThat(FrameType.ERROR.isFragmentable()).isFalse(); - assertThat(FrameType.ERROR.isRequestType()).isFalse(); - } - - @DisplayName("EXT characteristics") - @Test - void ext() { - assertThat(FrameType.EXT.canHaveData()).isTrue(); - assertThat(FrameType.EXT.canHaveMetadata()).isTrue(); - assertThat(FrameType.EXT.hasInitialRequestN()).isFalse(); - assertThat(FrameType.EXT.getEncodedType()).isEqualTo(0x3F); - assertThat(FrameType.EXT.isFragmentable()).isFalse(); - assertThat(FrameType.EXT.isRequestType()).isFalse(); - } - - @DisplayName("KEEPALIVE characteristics") - @Test - void keepAlive() { - assertThat(FrameType.KEEPALIVE.canHaveData()).isTrue(); - assertThat(FrameType.KEEPALIVE.canHaveMetadata()).isFalse(); - assertThat(FrameType.KEEPALIVE.getEncodedType()).isEqualTo(0x03); - assertThat(FrameType.KEEPALIVE.hasInitialRequestN()).isFalse(); - assertThat(FrameType.KEEPALIVE.isFragmentable()).isFalse(); - assertThat(FrameType.KEEPALIVE.isRequestType()).isFalse(); - } - - @DisplayName("LEASE characteristics") - @Test - void lease() { - assertThat(FrameType.LEASE.canHaveData()).isFalse(); - assertThat(FrameType.LEASE.canHaveMetadata()).isTrue(); - assertThat(FrameType.LEASE.getEncodedType()).isEqualTo(0x02); - assertThat(FrameType.LEASE.hasInitialRequestN()).isFalse(); - assertThat(FrameType.LEASE.isFragmentable()).isFalse(); - assertThat(FrameType.LEASE.isRequestType()).isFalse(); - } - - @DisplayName("METADATA_PUSH characteristics") - @Test - void metadataPush() { - assertThat(FrameType.METADATA_PUSH.canHaveData()).isFalse(); - assertThat(FrameType.METADATA_PUSH.canHaveMetadata()).isTrue(); - assertThat(FrameType.METADATA_PUSH.hasInitialRequestN()).isFalse(); - assertThat(FrameType.METADATA_PUSH.getEncodedType()).isEqualTo(0x0C); - assertThat(FrameType.METADATA_PUSH.isFragmentable()).isFalse(); - assertThat(FrameType.METADATA_PUSH.isRequestType()).isFalse(); - } - - @DisplayName("NEXT characteristics") - @Test - void next() { - assertThat(FrameType.NEXT.canHaveData()).isTrue(); - assertThat(FrameType.NEXT.canHaveMetadata()).isTrue(); - assertThat(FrameType.NEXT.hasInitialRequestN()).isFalse(); - assertThat(FrameType.NEXT.getEncodedType()).isEqualTo(0xA0); - assertThat(FrameType.NEXT.isFragmentable()).isTrue(); - assertThat(FrameType.NEXT.isRequestType()).isFalse(); - } - - @DisplayName("NEXT_COMPLETE characteristics") - @Test - void nextComplete() { - assertThat(FrameType.NEXT_COMPLETE.canHaveData()).isTrue(); - assertThat(FrameType.NEXT_COMPLETE.canHaveMetadata()).isTrue(); - assertThat(FrameType.NEXT_COMPLETE.hasInitialRequestN()).isFalse(); - assertThat(FrameType.NEXT_COMPLETE.getEncodedType()).isEqualTo(0xC0); - assertThat(FrameType.NEXT_COMPLETE.isFragmentable()).isTrue(); - assertThat(FrameType.NEXT_COMPLETE.isRequestType()).isFalse(); - } - - @DisplayName("PAYLOAD characteristics") - @Test - void payload() { - assertThat(FrameType.PAYLOAD.canHaveData()).isTrue(); - assertThat(FrameType.PAYLOAD.canHaveMetadata()).isTrue(); - assertThat(FrameType.PAYLOAD.hasInitialRequestN()).isFalse(); - assertThat(FrameType.PAYLOAD.getEncodedType()).isEqualTo(0x0A); - assertThat(FrameType.PAYLOAD.isFragmentable()).isTrue(); - assertThat(FrameType.PAYLOAD.isRequestType()).isFalse(); - } - - @DisplayName("REQUEST_CHANNEL characteristics") - @Test - void requestChannel() { - assertThat(FrameType.REQUEST_CHANNEL.canHaveData()).isTrue(); - assertThat(FrameType.REQUEST_CHANNEL.canHaveMetadata()).isTrue(); - assertThat(FrameType.REQUEST_CHANNEL.getEncodedType()).isEqualTo(0x07); - assertThat(FrameType.REQUEST_CHANNEL.hasInitialRequestN()).isTrue(); - assertThat(FrameType.REQUEST_CHANNEL.isFragmentable()).isTrue(); - assertThat(FrameType.REQUEST_CHANNEL.isRequestType()).isTrue(); - } - - @DisplayName("REQUEST_FNF characteristics") - @Test - void requestFnf() { - assertThat(FrameType.REQUEST_FNF.canHaveData()).isTrue(); - assertThat(FrameType.REQUEST_FNF.canHaveMetadata()).isTrue(); - assertThat(FrameType.REQUEST_FNF.getEncodedType()).isEqualTo(0x05); - assertThat(FrameType.REQUEST_FNF.hasInitialRequestN()).isFalse(); - assertThat(FrameType.REQUEST_FNF.isFragmentable()).isTrue(); - assertThat(FrameType.REQUEST_FNF.isRequestType()).isTrue(); - } - - @DisplayName("REQUEST_N characteristics") - @Test - void requestN() { - assertThat(FrameType.REQUEST_N.canHaveData()).isFalse(); - assertThat(FrameType.REQUEST_N.canHaveMetadata()).isFalse(); - assertThat(FrameType.REQUEST_N.getEncodedType()).isEqualTo(0x08); - assertThat(FrameType.REQUEST_N.hasInitialRequestN()).isFalse(); - assertThat(FrameType.REQUEST_N.isFragmentable()).isFalse(); - assertThat(FrameType.REQUEST_N.isRequestType()).isFalse(); - } - - @DisplayName("REQUEST_RESPONSE characteristics") - @Test - void requestResponse() { - assertThat(FrameType.REQUEST_RESPONSE.canHaveData()).isTrue(); - assertThat(FrameType.REQUEST_RESPONSE.canHaveMetadata()).isTrue(); - assertThat(FrameType.REQUEST_RESPONSE.getEncodedType()).isEqualTo(0x04); - assertThat(FrameType.REQUEST_RESPONSE.hasInitialRequestN()).isFalse(); - assertThat(FrameType.REQUEST_RESPONSE.isFragmentable()).isTrue(); - assertThat(FrameType.REQUEST_RESPONSE.isRequestType()).isTrue(); - } - - @DisplayName("REQUEST_STREAM characteristics") - @Test - void requestStream() { - assertThat(FrameType.REQUEST_STREAM.canHaveData()).isTrue(); - assertThat(FrameType.REQUEST_STREAM.canHaveMetadata()).isTrue(); - assertThat(FrameType.REQUEST_STREAM.getEncodedType()).isEqualTo(0x06); - assertThat(FrameType.REQUEST_STREAM.hasInitialRequestN()).isTrue(); - assertThat(FrameType.REQUEST_STREAM.isFragmentable()).isTrue(); - assertThat(FrameType.REQUEST_STREAM.isRequestType()).isTrue(); - } - - @DisplayName("RESERVED characteristics") - @Test - void reserved() { - assertThat(FrameType.RESERVED.canHaveData()).isFalse(); - assertThat(FrameType.RESERVED.canHaveMetadata()).isFalse(); - assertThat(FrameType.RESERVED.hasInitialRequestN()).isFalse(); - assertThat(FrameType.RESERVED.getEncodedType()).isEqualTo(0x00); - assertThat(FrameType.RESERVED.isFragmentable()).isFalse(); - assertThat(FrameType.RESERVED.isRequestType()).isFalse(); - } - - @DisplayName("SETUP characteristics") - @Test - void setup() { - assertThat(FrameType.SETUP.canHaveData()).isTrue(); - assertThat(FrameType.SETUP.canHaveMetadata()).isTrue(); - assertThat(FrameType.SETUP.getEncodedType()).isEqualTo(0x01); - assertThat(FrameType.SETUP.hasInitialRequestN()).isFalse(); - assertThat(FrameType.SETUP.isFragmentable()).isFalse(); - assertThat(FrameType.SETUP.isRequestType()).isFalse(); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/KeepaliveFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/KeepaliveFrameTest.java deleted file mode 100644 index 455285a5e..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/KeepaliveFrameTest.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.KeepaliveFrame.createKeepaliveFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -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.Unpooled; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class KeepaliveFrameTest implements DataFrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return KeepaliveFrame::createKeepaliveFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = - Unpooled.buffer(12) - .writeShort(0b00001100_00000000) - .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100) - .writeBytes(getRandomByteBuf(2)); - - KeepaliveFrame frame = createKeepaliveFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @Override - public Tuple2 getFrameWithData() { - ByteBuf data = getRandomByteBuf(2); - - KeepaliveFrame frame = - createKeepaliveFrame( - Unpooled.buffer(12) - .writeShort(0b00001100_00000000) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100) - .writeBytes(data, 0, data.readableBytes())); - - return Tuples.of(frame, data); - } - - @Override - public KeepaliveFrame getFrameWithEmptyData() { - return createKeepaliveFrame( - Unpooled.buffer(10) - .writeShort(0b00001100_00000000) - .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100)); - } - - @DisplayName("creates KEEPALIVE frame with data") - @Test - void createKeepAliveFrameData() { - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(12) - .writeShort(0b00001100_00000000) - .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100) - .writeBytes(data, 0, data.readableBytes()); - - assertThat(createKeepaliveFrame(DEFAULT, false, 100, data).mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("creates KEEPALIVE frame without data") - @Test - void createKeepAliveFrameDataNull() { - ByteBuf expected = - Unpooled.buffer(10) - .writeShort(0b00001100_00000000) - .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100); - - assertThat(createKeepaliveFrame(DEFAULT, false, 100, null).mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("creates KEEPALIVE frame without respond flag set") - @Test - void createKeepAliveFrameRespondFalse() { - ByteBuf expected = - Unpooled.buffer(10) - .writeShort(0b00001100_00000000) - .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100); - - assertThat(createKeepaliveFrame(DEFAULT, false, 100, null).mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("creates KEEPALIVE frame with respond flag set") - @Test - void createKeepAliveFrameRespondTrue() { - ByteBuf expected = - Unpooled.buffer(10) - .writeShort(0b00001100_10000000) - .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100); - - assertThat(createKeepaliveFrame(DEFAULT, true, 100, null).mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("createKeepaliveFrame throws NullPointerException with null byteBufAllocator") - @Test - void createKeepaliveFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createKeepaliveFrame(null, true, 100, null)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("returns last received position") - @Test - void getLastReceivedPosition() { - KeepaliveFrame frame = - createKeepaliveFrame( - Unpooled.buffer(10) - .writeShort(0b00001100_00000000) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100)); - - assertThat(frame.getLastReceivedPosition()).isEqualTo(100); - } - - @DisplayName("tests respond flag not set") - @Test - void isRespondFlagSetFalse() { - KeepaliveFrame frame = - createKeepaliveFrame( - Unpooled.buffer(10) - .writeShort(0b00001100_00000000) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100)); - - assertThat(frame.isRespondFlagSet()).isFalse(); - } - - @DisplayName("tests respond flag set") - @Test - void isRespondFlagSetTrue() { - KeepaliveFrame frame = - createKeepaliveFrame( - Unpooled.buffer(10) - .writeShort(0b00001100_10000000) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100)); - - assertThat(frame.isRespondFlagSet()).isTrue(); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/LeaseFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/LeaseFrameTest.java deleted file mode 100644 index 25417f166..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/LeaseFrameTest.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.LeaseFrame.createLeaseFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import java.time.Duration; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class LeaseFrameTest implements MetadataFrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return LeaseFrame::createLeaseFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = - Unpooled.buffer(12) - .writeShort(0b00001001_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeInt(0b00000000_00000000_00000000_11001000) - .writeBytes(getRandomByteBuf(2)); - - LeaseFrame frame = createLeaseFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @Override - public LeaseFrame getFrameWithEmptyMetadata() { - return createLeaseFrame( - Unpooled.buffer(10) - .writeShort(0b00001001_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeInt(0b00000000_00000000_00000000_11001000)); - } - - @Override - public Tuple2 getFrameWithMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - - LeaseFrame frame = - createLeaseFrame( - Unpooled.buffer(12) - .writeShort(0b00001001_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeInt(0b00000000_00000000_00000000_11001000) - .writeBytes(metadata, 0, metadata.readableBytes())); - - return Tuples.of(frame, metadata); - } - - @Override - public LeaseFrame getFrameWithoutMetadata() { - return createLeaseFrame( - Unpooled.buffer(10) - .writeShort(0b00001000_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeInt(0b00000000_00000000_00000000_11001000)); - } - - @DisplayName("createLeaseFrame throws IllegalArgumentException with invalid numberOfRequests") - @Test - void createLeaseFrameFrameInvalidNumberOfRequests() { - assertThatIllegalArgumentException() - .isThrownBy(() -> createLeaseFrame(DEFAULT, Duration.ofMillis(1), 0, null)) - .withMessage("numberOfRequests must be positive"); - } - - @DisplayName("createLeaseFrame throws IllegalArgumentException with invalid timeToLive") - @Test - void createLeaseFrameInvalidTimeToLive() { - assertThatIllegalArgumentException() - .isThrownBy(() -> createLeaseFrame(DEFAULT, Duration.ofMillis(0), 1, null)) - .withMessage("timeToLive must be a positive duration"); - } - - @DisplayName("creates LEASE frame with metadata") - @Test - void createLeaseFrameMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(12) - .writeShort(0b00001001_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeInt(0b00000000_00000000_00000000_11001000) - .writeBytes(metadata, 0, metadata.readableBytes()); - - assertThat( - createLeaseFrame(DEFAULT, Duration.ofMillis(100), 200, metadata) - .mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("creates LEASE frame without metadata") - @Test - void createLeaseFrameNoMetadata() { - ByteBuf expected = - Unpooled.buffer(10) - .writeShort(0b00001000_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeInt(0b00000000_00000000_00000000_11001000); - - assertThat( - createLeaseFrame(DEFAULT, Duration.ofMillis(100), 200, null) - .mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("createLeaseFrame throws NullPointerException with null byteBufAllocator") - @Test - void createLeaseFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createLeaseFrame(null, Duration.ofMillis(1), 1, null)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("createLeaseFrame throws NullPointerException with null timeToLive") - @Test - void createLeaseFrameNullTimeToLive() { - assertThatNullPointerException() - .isThrownBy(() -> createLeaseFrame(DEFAULT, null, 1, null)) - .withMessage("timeToLive must not be null"); - } - - @DisplayName("returns number of requests") - @Test - void getNumberOfRequests() { - LeaseFrame frame = - createLeaseFrame( - Unpooled.buffer(10) - .writeShort(0b00001000_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeInt(0b00000000_00000000_00000000_11001000)); - - assertThat(frame.getNumberOfRequests()).isEqualTo(200); - } - - @DisplayName("returns time to live") - @Test - void getTimeToLive() { - LeaseFrame frame = - createLeaseFrame( - Unpooled.buffer(10) - .writeShort(0b00001000_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeInt(0b00000000_00000000_00000000_11001000)); - - assertThat(frame.getTimeToLive()).isEqualTo(Duration.ofMillis(100)); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/LengthUtilsTest.java b/rsocket-core/src/test/java/io/rsocket/framing/LengthUtilsTest.java deleted file mode 100644 index 184f785a4..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/LengthUtilsTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package io.rsocket.framing; - -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -final class LengthUtilsTest { - - @DisplayName("getLengthAsUnsignedByte returns length if 255") - @Test - void getLengthAsUnsignedByte() { - assertThat(LengthUtils.getLengthAsUnsignedByte(getRandomByteBuf((1 << 8) - 1))).isEqualTo(255); - } - - @DisplayName("getLengthAsUnsignedByte throws NullPointerException with null byteBuf") - @Test - void getLengthAsUnsignedByteNullByteBuf() { - assertThatNullPointerException() - .isThrownBy(() -> LengthUtils.getLengthAsUnsignedByte(null)) - .withMessage("byteBuf must not be null"); - } - - @DisplayName("getLengthAsUnsignedByte throws IllegalArgumentException if larger than 255") - @Test - void getLengthAsUnsignedByteOverFlow() { - assertThatIllegalArgumentException() - .isThrownBy(() -> LengthUtils.getLengthAsUnsignedByte(getRandomByteBuf(1 << 8))) - .withMessage("%d is larger than 8 bits", 1 << 8); - } - - @DisplayName("getLengthAsUnsignedShort throws NullPointerException with null byteBuf") - @Test - void getLengthAsUnsignedIntegerNullByteBuf() { - assertThatNullPointerException() - .isThrownBy(() -> LengthUtils.getLengthAsUnsignedShort(null)) - .withMessage("byteBuf must not be null"); - } - - @DisplayName("getLengthAsUnsignedMedium returns length if 16_777_215") - @Test - void getLengthAsUnsignedMedium() { - assertThat(LengthUtils.getLengthAsUnsignedMedium(getRandomByteBuf((1 << 24) - 1))) - .isEqualTo(16_777_215); - } - - @DisplayName("getLengthAsUnsignedMedium throws NullPointerException with null byteBuf") - @Test - void getLengthAsUnsignedMediumNullByteBuf() { - assertThatNullPointerException() - .isThrownBy(() -> LengthUtils.getLengthAsUnsignedMedium(null)) - .withMessage("byteBuf must not be null"); - } - - @DisplayName( - "getLengthAsUnsignedMedium throws IllegalArgumentException if larger than 16_777_215") - @Test - void getLengthAsUnsignedMediumOverFlow() { - assertThatIllegalArgumentException() - .isThrownBy(() -> LengthUtils.getLengthAsUnsignedMedium(getRandomByteBuf(1 << 24))) - .withMessage("%d is larger than 24 bits", 1 << 24); - } - - @DisplayName("getLengthAsUnsignedShort returns length if 65_535") - @Test - void getLengthAsUnsignedShort() { - assertThat(LengthUtils.getLengthAsUnsignedShort(getRandomByteBuf((1 << 16) - 1))) - .isEqualTo(65_535); - } - - @DisplayName("getLengthAsUnsignedShort throws IllegalArgumentException if larger than 65_535") - @Test - void getLengthAsUnsignedShortOverFlow() { - assertThatIllegalArgumentException() - .isThrownBy(() -> LengthUtils.getLengthAsUnsignedShort(getRandomByteBuf(1 << 16))) - .withMessage("%d is larger than 16 bits", 1 << 16); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/MetadataFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/MetadataFrameTest.java deleted file mode 100644 index a45391294..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/MetadataFrameTest.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.Unpooled.EMPTY_BUFFER; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -import io.netty.buffer.ByteBuf; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; - -interface MetadataFrameTest extends FrameTest { - - T getFrameWithEmptyMetadata(); - - Tuple2 getFrameWithMetadata(); - - T getFrameWithoutMetadata(); - - @DisplayName("returns metadata as UTF-8") - @Test - default void getMetadataAsUtf8() { - Tuple2 tuple = getFrameWithMetadata(); - T frame = tuple.getT1(); - ByteBuf metadata = tuple.getT2(); - - assertThat(frame.getMetadataAsUtf8()).hasValue(metadata.toString(UTF_8)); - } - - @DisplayName("returns empty optional metadata as UTF-8") - @Test - default void getMetadataAsUtf8Empty() { - T frame = getFrameWithEmptyMetadata(); - - assertThat(frame.getMetadataAsUtf8()).hasValue(""); - } - - @DisplayName("returns empty optional for metadata as UTF-8") - @Test - default void getMetadataAsUtf8NoFlag() { - T frame = getFrameWithoutMetadata(); - - assertThat(frame.getMetadataAsUtf8()).isEmpty(); - } - - @DisplayName("returns metadata length") - @Test - default void getMetadataLength() { - Tuple2 tuple = getFrameWithMetadata(); - T frame = tuple.getT1(); - ByteBuf metadata = tuple.getT2(); - - assertThat(frame.getMetadataLength()).hasValue(metadata.readableBytes()); - } - - @DisplayName("returns empty optional metadata length") - @Test - default void getMetadataLengthEmpty() { - T frame = getFrameWithEmptyMetadata(); - - assertThat(frame.getMetadataLength()).hasValue(0); - } - - @DisplayName("returns empty optional for metadata length") - @Test - default void getMetadataLengthNoFlag() { - T frame = getFrameWithoutMetadata(); - - assertThat(frame.getMetadataLength()).isEmpty(); - } - - @DisplayName("returns unsafe metadata") - @Test - default void getUnsafeMetadata() { - Tuple2 tuple = getFrameWithMetadata(); - T frame = tuple.getT1(); - ByteBuf metadata = tuple.getT2(); - - assertThat(frame.getUnsafeMetadata()).isEqualTo(metadata); - } - - @DisplayName("returns unsafe metadata as UTF-8") - @Test - default void getUnsafeMetadataAsUtf8() { - Tuple2 tuple = getFrameWithMetadata(); - T frame = tuple.getT1(); - ByteBuf metadata = tuple.getT2(); - - assertThat(frame.getUnsafeMetadataAsUtf8()).isEqualTo(metadata.toString(UTF_8)); - } - - @DisplayName("returns empty unsafe metadata as UTF-8") - @Test - default void getUnsafeMetadataAsUtf8Empty() { - T frame = getFrameWithEmptyMetadata(); - - assertThat(frame.getUnsafeMetadataAsUtf8()).isEqualTo(""); - } - - @DisplayName("returns null for unsafe metadata as UTF-8") - @Test - default void getUnsafeMetadataAsUtf8NoFlag() { - T frame = getFrameWithoutMetadata(); - - assertThat(frame.getUnsafeMetadataAsUtf8()).isNull(); - } - - @DisplayName("returns unsafe empty metadata") - @Test - default void getUnsafeMetadataEmpty() { - T frame = getFrameWithEmptyMetadata(); - - assertThat(frame.getUnsafeMetadata()).isEqualTo(EMPTY_BUFFER); - } - - @DisplayName("returns unsafe metadata length") - @Test - default void getUnsafeMetadataLength() { - Tuple2 tuple = getFrameWithMetadata(); - T frame = tuple.getT1(); - ByteBuf metadata = tuple.getT2(); - - assertThat(frame.getUnsafeMetadataLength()).isEqualTo(metadata.readableBytes()); - } - - @DisplayName("returns unsafe empty metadata length") - @Test - default void getUnsafeMetadataLengthEmpty() { - T frame = getFrameWithEmptyMetadata(); - - assertThat(frame.getUnsafeMetadataLength()).isEqualTo(0); - } - - @DisplayName("returns null for unsafe metadata length") - @Test - default void getUnsafeMetadataLengthNoFlag() { - T frame = getFrameWithoutMetadata(); - - assertThat(frame.getUnsafeMetadataLength()).isNull(); - } - - @DisplayName("returns null for unsafe metadata") - @Test - default void getUnsafeMetadataNoFlag() { - T frame = getFrameWithoutMetadata(); - - assertThat(frame.getUnsafeMetadata()).isNull(); - } - - @DisplayName("maps metadata") - @Test - default void mapMetadata() { - Tuple2 tuple = getFrameWithMetadata(); - T frame = tuple.getT1(); - ByteBuf metadata = tuple.getT2(); - - assertThat(frame.mapMetadata(Function.identity())).hasValue(metadata); - } - - @DisplayName("maps empty metadata") - @Test - default void mapMetadataEmpty() { - T frame = getFrameWithEmptyMetadata(); - - assertThat(frame.mapMetadata(Function.identity())).hasValue(EMPTY_BUFFER); - } - - @DisplayName("maps empty optional for metadata") - @Test - default void mapMetadataNoFlag() { - T frame = getFrameWithoutMetadata(); - - assertThat(frame.mapMetadata(Function.identity())).isEmpty(); - } - - @DisplayName("mapMetadata throws NullPointerException with null function") - @Test - default void mapMetadataNullFunction() { - T frame = getFrameWithEmptyMetadata(); - - assertThatNullPointerException() - .isThrownBy(() -> frame.mapMetadata(null)) - .withMessage("function must not be null"); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/MetadataPushFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/MetadataPushFrameTest.java deleted file mode 100644 index ec9420f3a..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/MetadataPushFrameTest.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.MetadataPushFrame.createMetadataPushFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -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.Unpooled; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class MetadataPushFrameTest implements MetadataFrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return MetadataPushFrame::createMetadataPushFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = - Unpooled.buffer(4).writeShort(0b00110001_00000000).writeBytes(getRandomByteBuf(2)); - - MetadataPushFrame frame = createMetadataPushFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @Override - public MetadataPushFrame getFrameWithEmptyMetadata() { - return createMetadataPushFrame(Unpooled.buffer(2).writeShort(0b00110001_00000000)); - } - - @Override - public Tuple2 getFrameWithMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - - MetadataPushFrame frame = - createMetadataPushFrame( - Unpooled.buffer(4) - .writeShort(0b00110001_00000000) - .writeBytes(metadata, 0, metadata.readableBytes())); - - return Tuples.of(frame, metadata); - } - - @Override - public MetadataPushFrame getFrameWithoutMetadata() { - return createMetadataPushFrame(Unpooled.buffer(2).writeShort(0b00110000_00000000)); - } - - @DisplayName("creates METADATA_PUSH frame with ByteBufAllocator") - @Test - void createMetadataPushFrameByteBufAllocator() { - ByteBuf metadata = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(4) - .writeShort(0b00110001_00000000) - .writeBytes(metadata, 0, metadata.readableBytes()); - - assertThat(createMetadataPushFrame(DEFAULT, metadata).mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("createMetadataPushFrame throws NullPointerException with null metadata") - @Test - void createMetadataPushFrameNullMetadata() { - assertThatNullPointerException() - .isThrownBy(() -> createMetadataPushFrame(DEFAULT, (ByteBuf) null)) - .withMessage("metadata must not be null"); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/PayloadFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/PayloadFrameTest.java deleted file mode 100644 index 67b46be9a..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/PayloadFrameTest.java +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.Unpooled.EMPTY_BUFFER; -import static io.netty.buffer.Unpooled.buffer; -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.PayloadFrame.createPayloadFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -import io.netty.buffer.ByteBuf; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class PayloadFrameTest implements FragmentableFrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return PayloadFrame::createPayloadFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = - buffer(9) - .writeShort(0b00101001_00000000) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(getRandomByteBuf(2)) - .writeBytes(getRandomByteBuf(2)); - - PayloadFrame frame = createPayloadFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @Override - public Tuple2 getFrameWithData() { - ByteBuf data = getRandomByteBuf(2); - - PayloadFrame frame = - createPayloadFrame( - buffer(7) - .writeShort(0b00101000_00000000) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes())); - - return Tuples.of(frame, data); - } - - @Override - public PayloadFrame getFrameWithEmptyData() { - return createPayloadFrame( - buffer(5).writeShort(0b00101000_00000000).writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public PayloadFrame getFrameWithEmptyMetadata() { - return createPayloadFrame( - buffer(5).writeShort(0b00101001_00000000).writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public PayloadFrame getFrameWithFollowsFlagSet() { - return createPayloadFrame( - buffer(5).writeShort(0b00101000_10000000).writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public Tuple2 getFrameWithMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - - PayloadFrame frame = - createPayloadFrame( - buffer(7) - .writeShort(0b00101001_00000000) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes())); - - return Tuples.of(frame, metadata); - } - - @Override - public PayloadFrame getFrameWithoutFollowsFlagSet() { - return createPayloadFrame( - buffer(5).writeShort(0b00101000_01000000).writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public PayloadFrame getFrameWithoutMetadata() { - return createPayloadFrame( - buffer(5).writeShort(0b00101000_10000000).writeMedium(0b00000000_00000000_00000000)); - } - - @DisplayName("createPayloadFrame throws IllegalArgumentException without complete flag or data") - @Test - void createPayloadFrameNonCompleteNullData() { - assertThatIllegalArgumentException() - .isThrownBy(() -> createPayloadFrame(DEFAULT, false, false, (ByteBuf) null, null)) - .withMessage("Payload frame must either be complete, have data, or both"); - } - - @DisplayName("createPayloadFrame throws NullPointerException with null byteBufAllocator") - @Test - void createPayloadFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createPayloadFrame(null, false, false, null, EMPTY_BUFFER)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("creates PAYLOAD frame with Complete flag") - @Test - void createPayloadFrameWithComplete() { - ByteBuf expected = - buffer(5).writeShort(0b00101000_01000000).writeMedium(0b00000000_00000000_00000000); - - PayloadFrame frame = createPayloadFrame(DEFAULT, false, true, (ByteBuf) null, null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates PAYLOAD frame with data") - @Test - void createPayloadFrameWithData() { - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - buffer(9) - .writeShort(0b00101000_00100000) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes()); - - PayloadFrame frame = createPayloadFrame(DEFAULT, false, false, null, data); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates PAYLOAD frame with Follows flag") - @Test - void createPayloadFrameWithFollows() { - ByteBuf expected = - buffer(7).writeShort(0b00101000_10100000).writeMedium(0b00000000_00000000_00000000); - - PayloadFrame frame = createPayloadFrame(DEFAULT, true, false, null, EMPTY_BUFFER); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates PAYLOAD frame with metadata") - @Test - void createPayloadFrameWithMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - - ByteBuf expected = - buffer(9) - .writeShort(0b00101001_00100000) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()); - - PayloadFrame frame = createPayloadFrame(DEFAULT, false, false, metadata, EMPTY_BUFFER); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates PAYLOAD frame with metadata and data") - @Test - void createPayloadFrameWithMetadataAnData() { - ByteBuf metadata = getRandomByteBuf(2); - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - buffer(11) - .writeShort(0b00101001_00100000) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()) - .writeBytes(data, 0, data.readableBytes()); - - PayloadFrame frame = createPayloadFrame(DEFAULT, false, false, metadata, data); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("tests complete flag not set") - @Test - void isCompleteFlagSetFalse() { - PayloadFrame frame = - createPayloadFrame( - buffer(5).writeShort(0b00101000_00000000).writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.isCompleteFlagSet()).isFalse(); - } - - @DisplayName("tests complete flag set") - @Test - void isCompleteFlagSetTrue() { - PayloadFrame frame = - createPayloadFrame( - buffer(5).writeShort(0b00101000_01000000).writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.isCompleteFlagSet()).isTrue(); - } - - @DisplayName("tests next flag set") - @Test - void isNextFlagNotSet() { - PayloadFrame frame = - createPayloadFrame( - buffer(5).writeShort(0b00101000_00000000).writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.isNextFlagSet()).isFalse(); - } - - @DisplayName("tests next flag set") - @Test - void isNextFlagSetTrue() { - PayloadFrame frame = - createPayloadFrame( - buffer(5).writeShort(0b00101000_00100000).writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.isNextFlagSet()).isTrue(); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/RequestChannelFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/RequestChannelFrameTest.java deleted file mode 100644 index 4b0beb67d..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/RequestChannelFrameTest.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.RequestChannelFrame.createRequestChannelFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -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.Unpooled; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class RequestChannelFrameTest implements FragmentableFrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return RequestChannelFrame::createRequestChannelFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = - Unpooled.buffer(11) - .writeShort(0b00011101_00000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(getRandomByteBuf(2)) - .writeBytes(getRandomByteBuf(2)); - - RequestChannelFrame frame = createRequestChannelFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @Override - public Tuple2 getFrameWithData() { - ByteBuf data = getRandomByteBuf(2); - - RequestChannelFrame frame = - createRequestChannelFrame( - Unpooled.buffer(9) - .writeShort(0b00011100_00000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes())); - - return Tuples.of(frame, data); - } - - @Override - public RequestChannelFrame getFrameWithEmptyData() { - return createRequestChannelFrame( - Unpooled.buffer(7) - .writeShort(0b00011100_00000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public RequestChannelFrame getFrameWithEmptyMetadata() { - return createRequestChannelFrame( - Unpooled.buffer(7) - .writeShort(0b00011101_00000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public RequestChannelFrame getFrameWithFollowsFlagSet() { - return createRequestChannelFrame( - Unpooled.buffer(7) - .writeShort(0b00011100_10000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public Tuple2 getFrameWithMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - - RequestChannelFrame frame = - createRequestChannelFrame( - Unpooled.buffer(9) - .writeShort(0b00011101_00000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes())); - - return Tuples.of(frame, metadata); - } - - @Override - public RequestChannelFrame getFrameWithoutFollowsFlagSet() { - return createRequestChannelFrame( - Unpooled.buffer(7) - .writeShort(0b00011100_00000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public RequestChannelFrame getFrameWithoutMetadata() { - return createRequestChannelFrame( - Unpooled.buffer(7) - .writeShort(0b00011100_10000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000)); - } - - @DisplayName("createRequestChannelFrame throws NullPointerException with null byteBufAllocator") - @Test - void createRequestChannelFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createRequestChannelFrame(null, false, false, 100, (ByteBuf) null, null)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("creates REQUEST_CHANNEL frame with Complete flag") - @Test - void createRequestChannelFrameWithComplete() { - ByteBuf expected = - Unpooled.buffer(9) - .writeShort(0b00011100_01000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000000); - - RequestChannelFrame frame = - createRequestChannelFrame(DEFAULT, false, true, 100, (ByteBuf) null, null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates REQUEST_CHANNEL frame with data") - @Test - void createRequestChannelFrameWithData() { - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(11) - .writeShort(0b00011100_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes()); - - RequestChannelFrame frame = createRequestChannelFrame(DEFAULT, false, false, 100, null, data); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates REQUEST_CHANNEL frame with Follows flag") - @Test - void createRequestChannelFrameWithFollows() { - ByteBuf expected = - Unpooled.buffer(9) - .writeShort(0b00011100_10000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000000); - - RequestChannelFrame frame = - createRequestChannelFrame(DEFAULT, true, false, 100, (ByteBuf) null, null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates REQUEST_CHANNEL frame with metadata") - @Test - void createRequestChannelFrameWithMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(11) - .writeShort(0b00011101_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()); - - RequestChannelFrame frame = - createRequestChannelFrame(DEFAULT, false, false, 100, metadata, null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates REQUEST_CHANNEL frame with metadata and data") - @Test - void createRequestChannelFrameWithMetadataAnData() { - ByteBuf metadata = getRandomByteBuf(2); - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(13) - .writeShort(0b00011101_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()) - .writeBytes(data, 0, data.readableBytes()); - - RequestChannelFrame frame = - createRequestChannelFrame(DEFAULT, false, false, 100, metadata, data); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("returns the initial requestN") - @Test - void getInitialRequestN() { - RequestChannelFrame frame = - createRequestChannelFrame( - Unpooled.buffer(7) - .writeShort(0b00011100_10000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.getInitialRequestN()).isEqualTo(1); - } - - @DisplayName("tests complete flag not set") - @Test - void isCompleteFlagSetFalse() { - RequestChannelFrame frame = - createRequestChannelFrame( - Unpooled.buffer(7) - .writeShort(0b00011100_10000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.isCompleteFlagSet()).isFalse(); - } - - @DisplayName("tests complete flag set") - @Test - void isCompleteFlagSetTrue() { - RequestChannelFrame frame = - createRequestChannelFrame( - Unpooled.buffer(7) - .writeShort(0b00011100_11000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.isCompleteFlagSet()).isTrue(); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/RequestFireAndForgetFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/RequestFireAndForgetFrameTest.java deleted file mode 100644 index baf409b45..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/RequestFireAndForgetFrameTest.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.RequestFireAndForgetFrame.createRequestFireAndForgetFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -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.Unpooled; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class RequestFireAndForgetFrameTest - implements FragmentableFrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return RequestFireAndForgetFrame::createRequestFireAndForgetFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = - Unpooled.buffer(9) - .writeShort(0b00010101_00000000) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(getRandomByteBuf(2)) - .writeBytes(getRandomByteBuf(2)); - - RequestFireAndForgetFrame frame = createRequestFireAndForgetFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @Override - public Tuple2 getFrameWithData() { - ByteBuf data = getRandomByteBuf(2); - - RequestFireAndForgetFrame frame = - createRequestFireAndForgetFrame( - Unpooled.buffer(7) - .writeShort(0b00010100_00000000) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes())); - - return Tuples.of(frame, data); - } - - @Override - public RequestFireAndForgetFrame getFrameWithEmptyData() { - return createRequestFireAndForgetFrame( - Unpooled.buffer(5) - .writeShort(0b00010100_00000000) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public RequestFireAndForgetFrame getFrameWithEmptyMetadata() { - return createRequestFireAndForgetFrame( - Unpooled.buffer(5) - .writeShort(0b00010101_00000000) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public RequestFireAndForgetFrame getFrameWithFollowsFlagSet() { - return createRequestFireAndForgetFrame( - Unpooled.buffer(5) - .writeShort(0b00010100_10000000) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public Tuple2 getFrameWithMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - - RequestFireAndForgetFrame frame = - createRequestFireAndForgetFrame( - Unpooled.buffer(7) - .writeShort(0b00010101_00000000) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes())); - - return Tuples.of(frame, metadata); - } - - @Override - public RequestFireAndForgetFrame getFrameWithoutFollowsFlagSet() { - return createRequestFireAndForgetFrame( - Unpooled.buffer(5) - .writeShort(0b00010100_00000000) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public RequestFireAndForgetFrame getFrameWithoutMetadata() { - return createRequestFireAndForgetFrame( - Unpooled.buffer(5) - .writeShort(0b00010100_10000000) - .writeMedium(0b00000000_00000000_00000000)); - } - - @DisplayName( - "createRequestFireAndForgetFrame throws NullPointerException with null byteBufAllocator") - @Test - void createRequestFireAndForgetFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createRequestFireAndForgetFrame(null, false, (ByteBuf) null, null)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("creates REQUEST_FNF frame with data") - @Test - void createRequestFireAndForgetFrameWithData() { - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(7) - .writeShort(0b00010100_00000000) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes()); - - RequestFireAndForgetFrame frame = createRequestFireAndForgetFrame(DEFAULT, false, null, data); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates REQUEST_FNF frame with Follows flag") - @Test - void createRequestFireAndForgetFrameWithFollows() { - ByteBuf expected = - Unpooled.buffer(5) - .writeShort(0b00010100_10000000) - .writeMedium(0b00000000_00000000_00000000); - - RequestFireAndForgetFrame frame = - createRequestFireAndForgetFrame(DEFAULT, true, (ByteBuf) null, null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates REQUEST_FNF frame with metadata") - @Test - void createRequestFireAndForgetFrameWithMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(7) - .writeShort(0b00010101_00000000) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()); - - RequestFireAndForgetFrame frame = - createRequestFireAndForgetFrame(DEFAULT, false, metadata, null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates REQUEST_FNF frame with metadata and data") - @Test - void createRequestFireAndForgetFrameWithMetadataAnData() { - ByteBuf metadata = getRandomByteBuf(2); - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(9) - .writeShort(0b00010101_00000000) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()) - .writeBytes(data, 0, data.readableBytes()); - - RequestFireAndForgetFrame frame = - createRequestFireAndForgetFrame(DEFAULT, false, metadata, data); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/RequestNFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/RequestNFrameTest.java deleted file mode 100644 index b81af3958..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/RequestNFrameTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.RequestNFrame.createRequestNFrame; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class RequestNFrameTest implements FrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return RequestNFrame::createRequestNFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = - Unpooled.buffer(6) - .writeShort(0b00100000_00000000) - .writeInt(0b00000000_00000000_00000000_01100100); - - RequestNFrame frame = createRequestNFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @DisplayName("creates REQUEST_N frame with ByteBufAllocator") - @Test - void createRequestNFrameByteBufAllocator() { - ByteBuf expected = - Unpooled.buffer(6) - .writeShort(0b00100000_00000000) - .writeInt(0b00000000_00000000_00000000_01100100); - - assertThat(createRequestNFrame(DEFAULT, 100).mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("createRequestNFrame throws NullPointerException with null byteBufAllocator") - @Test - void createRequestNFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createRequestNFrame(null, 1)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("createRequestNFrame throws IllegalArgumentException with requestN less then 1") - @Test - void createRequestNFrameZeroRequestN() { - assertThatIllegalArgumentException() - .isThrownBy(() -> createRequestNFrame(DEFAULT, 0)) - .withMessage("requestN must be positive"); - } - - @DisplayName("returns requestN") - @Test - void getRequestN() { - RequestNFrame frame = - createRequestNFrame( - Unpooled.buffer(6) - .writeShort(0b00100000_00000000) - .writeInt(0b00000000_00000000_00000000_01100100)); - - assertThat(frame.getRequestN()).isEqualTo(100); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/RequestResponseFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/RequestResponseFrameTest.java deleted file mode 100644 index a93b7cef9..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/RequestResponseFrameTest.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.RequestResponseFrame.createRequestResponseFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -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.Unpooled; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class RequestResponseFrameTest implements FragmentableFrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return RequestResponseFrame::createRequestResponseFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = - Unpooled.buffer(9) - .writeShort(0b00010001_00000000) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(getRandomByteBuf(2)) - .writeBytes(getRandomByteBuf(2)); - - RequestResponseFrame frame = createRequestResponseFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @Override - public Tuple2 getFrameWithData() { - ByteBuf data = getRandomByteBuf(2); - - RequestResponseFrame frame = - createRequestResponseFrame( - Unpooled.buffer(7) - .writeShort(0b00010000_00000000) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes())); - - return Tuples.of(frame, data); - } - - @Override - public RequestResponseFrame getFrameWithEmptyData() { - return createRequestResponseFrame( - Unpooled.buffer(5) - .writeShort(0b00010000_00000000) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public RequestResponseFrame getFrameWithEmptyMetadata() { - return createRequestResponseFrame( - Unpooled.buffer(5) - .writeShort(0b00010001_00000000) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public RequestResponseFrame getFrameWithFollowsFlagSet() { - return createRequestResponseFrame( - Unpooled.buffer(5) - .writeShort(0b00010000_10000000) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public Tuple2 getFrameWithMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - - RequestResponseFrame frame = - createRequestResponseFrame( - Unpooled.buffer(7) - .writeShort(0b00010001_00000000) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes())); - - return Tuples.of(frame, metadata); - } - - @Override - public RequestResponseFrame getFrameWithoutFollowsFlagSet() { - return createRequestResponseFrame( - Unpooled.buffer(5) - .writeShort(0b00010000_00000000) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public RequestResponseFrame getFrameWithoutMetadata() { - return createRequestResponseFrame( - Unpooled.buffer(5) - .writeShort(0b00010000_10000000) - .writeMedium(0b00000000_00000000_00000000)); - } - - @DisplayName("createRequestResponseFrame throws NullPointerException with null byteBufAllocator") - @Test - void createRequestResponseFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createRequestResponseFrame(null, false, (ByteBuf) null, null)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("creates REQUEST_FNF frame with data") - @Test - void createRequestResponseFrameWithData() { - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(7) - .writeShort(0b00010000_00000000) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes()); - - RequestResponseFrame frame = createRequestResponseFrame(DEFAULT, false, null, data); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates REQUEST_FNF frame with Follows flag") - @Test - void createRequestResponseFrameWithFollows() { - ByteBuf expected = - Unpooled.buffer(5) - .writeShort(0b00010000_10000000) - .writeMedium(0b00000000_00000000_00000000); - - RequestResponseFrame frame = createRequestResponseFrame(DEFAULT, true, (ByteBuf) null, null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates REQUEST_FNF frame with metadata") - @Test - void createRequestResponseFrameWithMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(7) - .writeShort(0b00010001_00000000) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()); - - RequestResponseFrame frame = createRequestResponseFrame(DEFAULT, false, metadata, null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates REQUEST_FNF frame with metadata and data") - @Test - void createRequestResponseFrameWithMetadataAnData() { - ByteBuf metadata = getRandomByteBuf(2); - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(9) - .writeShort(0b00010001_00000000) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()) - .writeBytes(data, 0, data.readableBytes()); - - RequestResponseFrame frame = createRequestResponseFrame(DEFAULT, false, metadata, data); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/RequestStreamFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/RequestStreamFrameTest.java deleted file mode 100644 index 664d5358e..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/RequestStreamFrameTest.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.RequestStreamFrame.createRequestStreamFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class RequestStreamFrameTest implements FragmentableFrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return RequestStreamFrame::createRequestStreamFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = - Unpooled.buffer(11) - .writeShort(0b00011001_00000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(getRandomByteBuf(2)) - .writeBytes(getRandomByteBuf(2)); - - RequestStreamFrame frame = createRequestStreamFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @Override - public Tuple2 getFrameWithData() { - ByteBuf data = getRandomByteBuf(2); - - RequestStreamFrame frame = - createRequestStreamFrame( - Unpooled.buffer(9) - .writeShort(0b00011000_00000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes())); - - return Tuples.of(frame, data); - } - - @Override - public RequestStreamFrame getFrameWithEmptyData() { - return createRequestStreamFrame( - Unpooled.buffer(7) - .writeShort(0b00011000_00000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public RequestStreamFrame getFrameWithEmptyMetadata() { - return createRequestStreamFrame( - Unpooled.buffer(7) - .writeShort(0b00011001_00000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public RequestStreamFrame getFrameWithFollowsFlagSet() { - return createRequestStreamFrame( - Unpooled.buffer(7) - .writeShort(0b00011000_10000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public Tuple2 getFrameWithMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - - RequestStreamFrame frame = - createRequestStreamFrame( - Unpooled.buffer(9) - .writeShort(0b00011001_00000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes())); - - return Tuples.of(frame, metadata); - } - - @Override - public RequestStreamFrame getFrameWithoutFollowsFlagSet() { - return createRequestStreamFrame( - Unpooled.buffer(7) - .writeShort(0b00011000_00000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public RequestStreamFrame getFrameWithoutMetadata() { - return createRequestStreamFrame( - Unpooled.buffer(7) - .writeShort(0b00001100_10000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000)); - } - - @DisplayName( - "createRequestStreamFrame throws IllegalArgumentException with invalid initialRequestN") - @Test - void createRequestStreamFrameInvalidInitialRequestN() { - assertThatIllegalArgumentException() - .isThrownBy(() -> createRequestStreamFrame(DEFAULT, false, 0, (ByteBuf) null, null)) - .withMessage("initialRequestN must be positive"); - } - - @DisplayName("createRequestStreamFrame throws NullPointerException with null byteBufAllocator") - @Test - void createRequestStreamFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createRequestStreamFrame(null, false, 100, (ByteBuf) null, null)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("creates REQUEST_STREAM frame with data") - @Test - void createRequestStreamFrameWithData() { - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(11) - .writeShort(0b00011000_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes()); - - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 100, null, data); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates REQUEST_STREAM frame with Follows flag") - @Test - void createRequestStreamFrameWithFollows() { - ByteBuf expected = - Unpooled.buffer(9) - .writeShort(0b00011000_10000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000000); - - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, true, 100, (ByteBuf) null, null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates REQUEST_STREAM frame with metadata") - @Test - void createRequestStreamFrameWithMetadata() { - ByteBuf metadata = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(11) - .writeShort(0b00011001_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()); - - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 100, metadata, null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates REQUEST_STREAM frame with metadata and data") - @Test - void createRequestStreamFrameWithMetadataAnData() { - ByteBuf metadata = getRandomByteBuf(2); - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(13) - .writeShort(0b00011001_00000000) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()) - .writeBytes(data, 0, data.readableBytes()); - - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 100, metadata, data); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("returns the initial requestN") - @Test - void getInitialRequestN() { - RequestStreamFrame frame = - createRequestStreamFrame( - Unpooled.buffer(7) - .writeShort(0b00011000_10000000) - .writeInt(0b00000000_00000000_00000000_00000001) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.getInitialRequestN()).isEqualTo(1); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/ResumeFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/ResumeFrameTest.java deleted file mode 100644 index 8a467eccf..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/ResumeFrameTest.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.Unpooled.EMPTY_BUFFER; -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.ResumeFrame.createResumeFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -import static java.nio.charset.StandardCharsets.UTF_8; -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.Unpooled; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class ResumeFrameTest implements FrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return ResumeFrame::createResumeFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - - ByteBuf byteBuf = - Unpooled.buffer(26) - .writeShort(0b00110100_00000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) - .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000); - - ResumeFrame frame = createResumeFrame(DEFAULT, 100, 200, resumeIdentificationToken, 300, 400); - - return Tuples.of(frame, byteBuf); - } - - @DisplayName("creates RESUME frame with ByteBufAllocator") - @Test - void createResumeByteBufAllocator() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(26) - .writeShort(0b00110100_00000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) - .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000); - - assertThat( - createResumeFrame(DEFAULT, 100, 200, resumeIdentificationToken, 300, 400) - .mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("createResumeFrame throws NullPointerException with null byteBufAllocator") - @Test - void createResumeFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createResumeFrame(null, 100, 200, EMPTY_BUFFER, 300, 400)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("createResumeFrame throws NullPointerException with null resumeIdentificationToken") - @Test - void createResumeFrameNullResumeIdentificationToken() { - assertThatNullPointerException() - .isThrownBy(() -> createResumeFrame(DEFAULT, 100, 200, null, 300, 400)) - .withMessage("resumeIdentificationToken must not be null"); - } - - @DisplayName("returns first available client position") - @Test - void getFirstAvailableClientPosition() { - ResumeFrame frame = - createResumeFrame( - Unpooled.buffer(24) - .writeShort(0b00110100_00000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeShort(0b00000000_00000000) - .writeBytes(EMPTY_BUFFER) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); - - assertThat(frame.getFirstAvailableClientPosition()).isEqualTo(400); - } - - @DisplayName("returns last received server position") - @Test - void getLastReceivedServerPosition() { - ResumeFrame frame = - createResumeFrame( - Unpooled.buffer(24) - .writeShort(0b00110100_00000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeShort(0b00000000_00000000) - .writeBytes(EMPTY_BUFFER) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); - - assertThat(frame.getLastReceivedServerPosition()).isEqualTo(300); - } - - @DisplayName("returns major version") - @Test - void getMajorVersion() { - ResumeFrame frame = - createResumeFrame( - Unpooled.buffer(24) - .writeShort(0b00110100_00000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeShort(0b00000000_00000000) - .writeBytes(EMPTY_BUFFER) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); - - assertThat(frame.getMajorVersion()).isEqualTo(100); - } - - @DisplayName("returns minor version") - @Test - void getMinorVersion() { - ResumeFrame frame = - createResumeFrame( - Unpooled.buffer(24) - .writeShort(0b00110100_00000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeShort(0b00000000_00000000) - .writeBytes(EMPTY_BUFFER) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); - - assertThat(frame.getMinorVersion()).isEqualTo(200); - } - - @DisplayName("returns resume identification token as UTF-8") - @Test - void getResumeIdentificationTokenAsUtf8() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - - ResumeFrame frame = - createResumeFrame( - Unpooled.buffer(26) - .writeShort(0b00110100_00000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); - - assertThat(frame.getResumeIdentificationTokenAsUtf8()) - .isEqualTo(resumeIdentificationToken.toString(UTF_8)); - } - - @DisplayName("returns unsafe resume identification token") - @Test - void getUnsafeResumeIdentificationToken() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - - ResumeFrame frame = - createResumeFrame( - Unpooled.buffer(26) - .writeShort(0b00110100_00000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); - - assertThat(frame.getUnsafeResumeIdentificationToken()).isEqualTo(resumeIdentificationToken); - } - - @DisplayName("maps resume identification token") - @Test - void mapResumeIdentificationToken() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - - ResumeFrame frame = - createResumeFrame( - Unpooled.buffer(26) - .writeShort(0b00110100_00000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); - - assertThat(frame.mapResumeIdentificationToken(Function.identity())) - .isEqualTo(resumeIdentificationToken); - } - - @DisplayName("mapResumeIdentificationToken throws NullPointerException with null function") - @Test - void mapResumeIdentificationTokenNullFunction() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - - ResumeFrame frame = - createResumeFrame( - Unpooled.buffer(26) - .writeShort(0b00110100_00000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); - - assertThatNullPointerException() - .isThrownBy(() -> frame.mapResumeIdentificationToken(null)) - .withMessage("function must not be null"); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/ResumeOkFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/ResumeOkFrameTest.java deleted file mode 100644 index 6262d5546..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/ResumeOkFrameTest.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.ResumeOkFrame.createResumeOkFrame; -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.Unpooled; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class ResumeOkFrameTest implements FrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return ResumeOkFrame::createResumeOkFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = - Unpooled.buffer(10) - .writeShort(0b00111000_00000000) - .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100); - - ResumeOkFrame frame = createResumeOkFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @DisplayName("creates RESUME_OK frame with ByteBufAllocator") - @Test - void createResumeOkFrameByteBufAllocator() { - ByteBuf expected = - Unpooled.buffer(10) - .writeShort(0b00111000_00000000) - .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100); - - assertThat(createResumeOkFrame(DEFAULT, 100).mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("createResumeOkFrame throws NullPointerException with null byteBufAllocator") - @Test - void createResumeOkFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createResumeOkFrame(null, 100)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("returns last received client position") - @Test - void getLastReceivedClientPosition() { - ResumeOkFrame frame = - createResumeOkFrame( - Unpooled.buffer(10) - .writeShort(0b001110_0000000000) - .writeLong( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100)); - - assertThat(frame.getLastReceivedClientPosition()).isEqualTo(100); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/SetupFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/SetupFrameTest.java deleted file mode 100644 index 83a6b534c..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/SetupFrameTest.java +++ /dev/null @@ -1,859 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.Unpooled.EMPTY_BUFFER; -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.SetupFrame.createSetupFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -import static io.rsocket.test.util.StringUtils.getRandomString; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import java.time.Duration; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class SetupFrameTest implements MetadataAndDataFrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return SetupFrame::createSetupFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - String metadataMimeType = getRandomString(2); - ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); - String dataMimeType = getRandomString(3); - ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); - ByteBuf metadata = getRandomByteBuf(2); - ByteBuf data = getRandomByteBuf(2); - - ByteBuf byteBuf = - Unpooled.buffer(32) - .writeShort(0b00000101_11000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeByte(0b00000010) - .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()) - .writeBytes(data, 0, data.readableBytes()); - - SetupFrame frame = - createSetupFrame( - DEFAULT, - true, - 100, - 200, - Duration.ofMillis(300), - Duration.ofMillis(400), - resumeIdentificationToken, - metadataMimeType, - dataMimeType, - metadata, - data); - - return Tuples.of(frame, byteBuf); - } - - @Override - public Tuple2 getFrameWithData() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - ByteBuf data = getRandomByteBuf(2); - - SetupFrame frame = - createSetupFrame( - Unpooled.buffer(30) - .writeShort(0b00000101_11000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes())); - - return Tuples.of(frame, data); - } - - @Override - public SetupFrame getFrameWithEmptyData() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - - return createSetupFrame( - Unpooled.buffer(28) - .writeShort(0b00000100_11000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_0000000)); - } - - @Override - public SetupFrame getFrameWithEmptyMetadata() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - - return createSetupFrame( - Unpooled.buffer(28) - .writeShort(0b00000101_11000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_00000000)); - } - - @Override - public Tuple2 getFrameWithMetadata() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - ByteBuf metadata = getRandomByteBuf(2); - - SetupFrame frame = - createSetupFrame( - Unpooled.buffer(30) - .writeShort(0b00000101_11000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes())); - - return Tuples.of(frame, metadata); - } - - @Override - public SetupFrame getFrameWithoutMetadata() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - - return createSetupFrame( - Unpooled.buffer(27) - .writeShort(0b00000100_11000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_00000000)); - } - - @DisplayName("createSetup throws IllegalArgumentException with invalid keepAliveInterval") - @Test - void createSetupFrameInvalidKeepAliveInterval() { - assertThatIllegalArgumentException() - .isThrownBy( - () -> - createSetupFrame( - DEFAULT, - true, - Duration.ZERO, - Duration.ofMillis(1), - null, - "", - "", - (ByteBuf) null, - null)) - .withMessage("keepAliveInterval must be a positive duration"); - } - - @DisplayName("createSetup throws IllegalArgumentException with invalid maxLifetime") - @Test - void createSetupFrameInvalidMaxLifetime() { - assertThatIllegalArgumentException() - .isThrownBy( - () -> - createSetupFrame( - DEFAULT, - true, - Duration.ofMillis(1), - Duration.ZERO, - null, - "", - "", - (ByteBuf) null, - null)) - .withMessage("maxLifetime must be a positive duration"); - } - - @DisplayName("createSetup throws NullPointerException with null byteBufAllocator") - @Test - void createSetupFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy( - () -> - createSetupFrame( - null, - true, - Duration.ofMillis(1), - Duration.ofMillis(1), - null, - "", - "", - (ByteBuf) null, - null)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("createSetup throws NullPointerException with null dataMimeType") - @Test - void createSetupFrameNullDataMimeType() { - assertThatNullPointerException() - .isThrownBy( - () -> - createSetupFrame( - DEFAULT, - true, - Duration.ofMillis(1), - Duration.ofMillis(1), - null, - "", - null, - (ByteBuf) null, - null)) - .withMessage("dataMimeType must not be null"); - } - - @DisplayName("createSetup throws NullPointerException with null keepAliveInterval") - @Test - void createSetupFrameNullKeepAliveInterval() { - assertThatNullPointerException() - .isThrownBy( - () -> - createSetupFrame( - DEFAULT, true, null, Duration.ofMillis(1), null, "", "", (ByteBuf) null, null)) - .withMessage("keepAliveInterval must not be null"); - } - - @DisplayName("createSetup throws NullPointerException with null maxLifetime") - @Test - void createSetupFrameNullMaxLifetime() { - assertThatNullPointerException() - .isThrownBy( - () -> - createSetupFrame( - DEFAULT, true, Duration.ofMillis(1), null, null, "", "", (ByteBuf) null, null)) - .withMessage("maxLifetime must not be null"); - } - - @DisplayName("createSetup throws NullPointerException with null metadataMimeType") - @Test - void createSetupFrameNullMetadataMimeType() { - assertThatNullPointerException() - .isThrownBy( - () -> - createSetupFrame( - DEFAULT, - true, - Duration.ofMillis(1), - Duration.ofMillis(1), - null, - null, - "", - (ByteBuf) null, - null)) - .withMessage("metadataMimeType must not be null"); - } - - @DisplayName("creates SETUP frame with data") - @Test - void createSetupFrameWithData() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - String metadataMimeType = getRandomString(2); - ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); - String dataMimeType = getRandomString(3); - ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); - ByteBuf data = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(30) - .writeShort(0b00000100_11000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeByte(0b00000010) - .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) - .writeMedium(0b00000000_00000000_00000000) - .writeBytes(data, 0, data.readableBytes()); - - SetupFrame frame = - createSetupFrame( - DEFAULT, - true, - 100, - 200, - Duration.ofMillis(300), - Duration.ofMillis(400), - resumeIdentificationToken, - metadataMimeType, - dataMimeType, - null, - data); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates SETUP frame with metadata") - @Test - void createSetupFrameWithMetadata() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - String metadataMimeType = getRandomString(2); - ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); - String dataMimeType = getRandomString(3); - ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); - ByteBuf metadata = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(30) - .writeShort(0b00000101_11000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeByte(0b00000010) - .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) - .writeMedium(0b00000000_00000000_00000010) - .writeBytes(metadata, 0, metadata.readableBytes()); - - SetupFrame frame = - createSetupFrame( - DEFAULT, - true, - 100, - 200, - Duration.ofMillis(300), - Duration.ofMillis(400), - resumeIdentificationToken, - metadataMimeType, - dataMimeType, - metadata, - null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates SETUP frame with resume identification token") - @Test - void createSetupFrameWithResumeIdentificationToken() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - String metadataMimeType = getRandomString(2); - ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); - String dataMimeType = getRandomString(3); - ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); - - ByteBuf expected = - Unpooled.buffer(32) - .writeShort(0b00000100_11000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeByte(0b00000010) - .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) - .writeMedium(0b00000000_00000000_00000000); - - SetupFrame frame = - createSetupFrame( - DEFAULT, - true, - 100, - 200, - Duration.ofMillis(300), - Duration.ofMillis(400), - resumeIdentificationToken, - metadataMimeType, - dataMimeType, - null, - null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates SETUP frame without data") - @Test - void createSetupFrameWithoutData() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - String metadataMimeType = getRandomString(2); - ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); - String dataMimeType = getRandomString(3); - ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); - - ByteBuf expected = - Unpooled.buffer(30) - .writeShort(0b00000100_11000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeByte(0b00000010) - .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) - .writeMedium(0b00000000_00000000_00000000); - - SetupFrame frame = - createSetupFrame( - DEFAULT, - true, - 100, - 200, - Duration.ofMillis(300), - Duration.ofMillis(400), - resumeIdentificationToken, - metadataMimeType, - dataMimeType, - null, - null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates SETUP frame without metadata") - @Test - void createSetupFrameWithoutMetadata() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - String metadataMimeType = getRandomString(2); - ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); - String dataMimeType = getRandomString(3); - ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); - - ByteBuf expected = - Unpooled.buffer(30) - .writeShort(0b00000100_11000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeByte(0b00000010) - .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) - .writeMedium(0b00000000_00000000_00000000); - - SetupFrame frame = - createSetupFrame( - DEFAULT, - true, - 100, - 200, - Duration.ofMillis(300), - Duration.ofMillis(400), - resumeIdentificationToken, - metadataMimeType, - dataMimeType, - null, - null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("creates SETUP frame without resume identification token") - @Test - void createSetupFrameWithoutResumeIdentificationToken() { - String metadataMimeType = getRandomString(2); - ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); - String dataMimeType = getRandomString(3); - ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); - - ByteBuf expected = - Unpooled.buffer(32) - .writeShort(0b00000100_01000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeByte(0b00000010) - .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) - .writeMedium(0b00000000_00000000_00000000); - - SetupFrame frame = - createSetupFrame( - DEFAULT, - true, - 100, - 200, - Duration.ofMillis(300), - Duration.ofMillis(400), - null, - metadataMimeType, - dataMimeType, - null, - null); - - assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); - } - - @DisplayName("returns data mime type") - @Test - void getDataMimeType() { - String metadataMimeType = getRandomString(2); - ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); - String dataMimeType = getRandomString(3); - ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); - - SetupFrame frame = - createSetupFrame( - Unpooled.buffer(23) - .writeShort(0b00000100_01000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeByte(0b00000010) - .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.getDataMimeType()).isEqualTo(dataMimeType); - } - - @DisplayName("returns the keepalive interval") - @Test - void getKeepaliveInterval() { - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - - SetupFrame frame = - createSetupFrame( - Unpooled.buffer(23) - .writeShort(0b00000100_01000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.getKeepAliveInterval()).isEqualTo(Duration.ofMillis(300)); - } - - @DisplayName("returns major version") - @Test - void getMajorVersion() { - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - - SetupFrame frame = - createSetupFrame( - Unpooled.buffer(23) - .writeShort(0b00000100_01000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.getMajorVersion()).isEqualTo(100); - } - - @DisplayName("returns the max lifetime") - @Test - void getMaxLifetime() { - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - - SetupFrame frame = - createSetupFrame( - Unpooled.buffer(23) - .writeShort(0b00000100_01000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.getMaxLifetime()).isEqualTo(Duration.ofMillis(400)); - } - - @DisplayName("returns metadata mime type") - @Test - void getMetadataMimeType() { - String metadataMimeType = getRandomString(2); - ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); - String dataMimeType = getRandomString(3); - ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); - - SetupFrame frame = - createSetupFrame( - Unpooled.buffer(23) - .writeShort(0b00000100_01000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeByte(0b00000010) - .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.getMetadataMimeType()).isEqualTo(metadataMimeType); - } - - @DisplayName("returns minor version") - @Test - void getMinorVersion() { - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - - SetupFrame frame = - createSetupFrame( - Unpooled.buffer(23) - .writeShort(0b00000100_01000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.getMinorVersion()).isEqualTo(200); - } - - @DisplayName("tests lease flag not set") - @Test - void isLeaseFlagSetFalse() { - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - - SetupFrame frame = - createSetupFrame( - Unpooled.buffer(23) - .writeShort(0b00000100_00000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.isLeaseFlagSet()).isFalse(); - } - - @DisplayName("test lease flag set") - @Test - void isLeaseFlagSetTrue() { - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - - SetupFrame frame = - createSetupFrame( - Unpooled.buffer(23) - .writeShort(0b00000100_01000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.isLeaseFlagSet()).isTrue(); - } - - @DisplayName("maps empty optional for resume identification token") - @Test - void mapResumeIdentificationNoFlag() { - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - - SetupFrame frame = - createSetupFrame( - Unpooled.buffer(28) - .writeShort(0b00000101_01000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.mapResumeIdentificationToken(Function.identity())).isEmpty(); - } - - @DisplayName("maps resume identification token") - @Test - void mapResumeIdentificationToken() { - ByteBuf resumeIdentificationToken = getRandomByteBuf(2); - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - - SetupFrame frame = - createSetupFrame( - Unpooled.buffer(28) - .writeShort(0b00000101_11000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeShort(0b00000000_00000010) - .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.mapResumeIdentificationToken(Function.identity())) - .hasValue(resumeIdentificationToken); - } - - @DisplayName("maps empty resume identification token") - @Test - void mapResumeIdentificationTokenEmpty() { - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - - SetupFrame frame = - createSetupFrame( - Unpooled.buffer(28) - .writeShort(0b00000101_11000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeShort(0b00000000_00000000) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_00000000)); - - assertThat(frame.mapResumeIdentificationToken(Function.identity())).hasValue(EMPTY_BUFFER); - } - - @DisplayName("mapResumeIdentificationToken throws NullPointerException with null function") - @Test - void mapResumeIdentificationTokenNullFunction() { - ByteBuf metadataMimeType = getRandomByteBuf(2); - ByteBuf dataMimeType = getRandomByteBuf(3); - - SetupFrame frame = - createSetupFrame( - Unpooled.buffer(24) - .writeShort(0b00000101_01000000) - .writeShort(0b00000000_01100100) - .writeShort(0b00000000_11001000) - .writeInt(0b00000000_00000000_0000001_00101100) - .writeInt(0b00000000_00000000_0000001_10010000) - .writeByte(0b00000010) - .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) - .writeByte(0b00000011) - .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) - .writeMedium(0b00000000_00000000_00000000)); - - assertThatNullPointerException() - .isThrownBy(() -> frame.mapResumeIdentificationToken(null)) - .withMessage("function must not be null"); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/StreamIdFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/StreamIdFrameTest.java deleted file mode 100644 index b7781f8c5..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/StreamIdFrameTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.FrameType.CANCEL; -import static io.rsocket.framing.StreamIdFrame.createStreamIdFrame; -import static io.rsocket.framing.TestFrames.createTestCancelFrame; -import static io.rsocket.framing.TestFrames.createTestFrame; -import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; -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.Unpooled; -import java.util.function.Function; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -final class StreamIdFrameTest implements FrameTest { - - @Override - public Function getCreateFrameFromByteBuf() { - return StreamIdFrame::createStreamIdFrame; - } - - @Override - public Tuple2 getFrame() { - ByteBuf byteBuf = - Unpooled.buffer(6) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeBytes(getRandomByteBuf(2)); - - StreamIdFrame frame = createStreamIdFrame(byteBuf); - - return Tuples.of(frame, byteBuf); - } - - @DisplayName("creates stream id frame with ByteBufAllocator") - @Test - void createStreamIdFrameByteBufAllocator() { - ByteBuf frame = getRandomByteBuf(2); - - ByteBuf expected = - Unpooled.buffer(6) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeBytes(frame, 0, frame.readableBytes()); - - assertThat( - createStreamIdFrame(DEFAULT, 100, createTestFrame(CANCEL, frame)) - .mapFrame(Function.identity())) - .isEqualTo(expected); - } - - @DisplayName("createStreamIdFrame throws NullPointerException with null byteBufAllocator") - @Test - void createStreamIdFrameNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createStreamIdFrame(null, 0, createTestCancelFrame())) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("createStreamIdFrame throws NullPointerException with null frame") - @Test - void createStreamIdFrameNullFrame() { - assertThatNullPointerException() - .isThrownBy(() -> createStreamIdFrame(DEFAULT, 0, null)) - .withMessage("frame must not be null"); - } - - @DisplayName("returns stream id") - @Test - void getStreamId() { - StreamIdFrame frame = - createStreamIdFrame(Unpooled.buffer(4).writeInt(0b00000000_00000000_00000000_01100100)); - - assertThat(frame.getStreamId()).isEqualTo(100); - } - - @DisplayName("maps byteBuf without stream id") - @Test - void mapFrameWithoutStreamId() { - ByteBuf frame = getRandomByteBuf(2); - - StreamIdFrame streamIdFrame = - createStreamIdFrame( - Unpooled.buffer(6) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeBytes(frame, 0, frame.readableBytes())); - - assertThat(streamIdFrame.mapFrameWithoutStreamId(Function.identity())).isEqualTo(frame); - } - - @DisplayName("mapFrameWithoutStreamId throws NullPointerException with null function") - @Test - void mapFrameWithoutStreamIdNullFunction() { - ByteBuf frame = getRandomByteBuf(2); - - StreamIdFrame streamIdFrame = - createStreamIdFrame( - Unpooled.buffer(6) - .writeInt(0b00000000_00000000_00000000_01100100) - .writeBytes(frame, 0, frame.readableBytes())); - - assertThatNullPointerException() - .isThrownBy(() -> streamIdFrame.mapFrameWithoutStreamId(null)) - .withMessage("function must not be null"); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/TestFrames.java b/rsocket-core/src/test/java/io/rsocket/framing/TestFrames.java deleted file mode 100644 index bf8882e0e..000000000 --- a/rsocket-core/src/test/java/io/rsocket/framing/TestFrames.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.framing; - -import static io.netty.buffer.Unpooled.EMPTY_BUFFER; -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.CancelFrame.createCancelFrame; -import static io.rsocket.framing.ErrorFrame.createErrorFrame; -import static io.rsocket.framing.ExtensionFrame.createExtensionFrame; -import static io.rsocket.framing.FrameLengthFrame.createFrameLengthFrame; -import static io.rsocket.framing.KeepaliveFrame.createKeepaliveFrame; -import static io.rsocket.framing.LeaseFrame.createLeaseFrame; -import static io.rsocket.framing.MetadataPushFrame.createMetadataPushFrame; -import static io.rsocket.framing.PayloadFrame.createPayloadFrame; -import static io.rsocket.framing.RequestChannelFrame.createRequestChannelFrame; -import static io.rsocket.framing.RequestFireAndForgetFrame.createRequestFireAndForgetFrame; -import static io.rsocket.framing.RequestNFrame.createRequestNFrame; -import static io.rsocket.framing.RequestResponseFrame.createRequestResponseFrame; -import static io.rsocket.framing.RequestStreamFrame.createRequestStreamFrame; -import static io.rsocket.framing.ResumeFrame.createResumeFrame; -import static io.rsocket.framing.ResumeOkFrame.createResumeOkFrame; -import static io.rsocket.framing.SetupFrame.createSetupFrame; -import static io.rsocket.framing.StreamIdFrame.createStreamIdFrame; - -import io.netty.buffer.ByteBuf; -import java.time.Duration; - -public final class TestFrames { - - private TestFrames() {} - - public static CancelFrame createTestCancelFrame() { - return createCancelFrame(DEFAULT); - } - - public static ErrorFrame createTestErrorFrame() { - return createErrorFrame(DEFAULT, 1, (ByteBuf) null); - } - - public static ExtensionFrame createTestExtensionFrame() { - return createExtensionFrame(DEFAULT, true, 1, (ByteBuf) null, null); - } - - public static Frame createTestFrame(FrameType frameType, ByteBuf byteBuf) { - return new TestFrame(frameType, byteBuf); - } - - public static FrameLengthFrame createTestFrameLengthFrame() { - return createFrameLengthFrame(DEFAULT, createTestStreamIdFrame()); - } - - public static KeepaliveFrame createTestKeepaliveFrame() { - return createKeepaliveFrame(DEFAULT, false, 1, null); - } - - public static LeaseFrame createTestLeaseFrame() { - return createLeaseFrame(DEFAULT, Duration.ofMillis(1), 1, null); - } - - public static MetadataPushFrame createTestMetadataPushFrame() { - return createMetadataPushFrame(DEFAULT, EMPTY_BUFFER); - } - - public static PayloadFrame createTestPayloadFrame() { - return createPayloadFrame(DEFAULT, false, true, (ByteBuf) null, null); - } - - public static RequestChannelFrame createTestRequestChannelFrame() { - return createRequestChannelFrame(DEFAULT, false, false, 1, (ByteBuf) null, null); - } - - public static RequestFireAndForgetFrame createTestRequestFireAndForgetFrame() { - return createRequestFireAndForgetFrame(DEFAULT, false, (ByteBuf) null, null); - } - - public static RequestNFrame createTestRequestNFrame() { - return createRequestNFrame(DEFAULT, 1); - } - - public static RequestResponseFrame createTestRequestResponseFrame() { - return createRequestResponseFrame(DEFAULT, false, (ByteBuf) null, null); - } - - public static RequestStreamFrame createTestRequestStreamFrame() { - return createRequestStreamFrame(DEFAULT, false, 1, (ByteBuf) null, null); - } - - public static ResumeFrame createTestResumeFrame() { - return createResumeFrame(DEFAULT, 1, 0, EMPTY_BUFFER, 1, 1); - } - - public static ResumeOkFrame createTestResumeOkFrame() { - return createResumeOkFrame(DEFAULT, 1); - } - - public static SetupFrame createTestSetupFrame() { - return createSetupFrame( - DEFAULT, true, 1, 1, Duration.ofMillis(1), Duration.ofMillis(1), null, "", "", null, null); - } - - public static StreamIdFrame createTestStreamIdFrame() { - return createStreamIdFrame(DEFAULT, 1, createTestCancelFrame()); - } - - private static final class TestFrame implements Frame { - - private final ByteBuf byteBuf; - - private final FrameType frameType; - - private TestFrame(FrameType frameType, ByteBuf byteBuf) { - this.frameType = frameType; - this.byteBuf = byteBuf; - } - - @Override - public void dispose() {} - - @Override - public FrameType getFrameType() { - return frameType; - } - - @Override - public ByteBuf getUnsafeFrame() { - return byteBuf.asReadOnly(); - } - } -} 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 f00507df0..341577b4c 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java @@ -16,60 +16,259 @@ package io.rsocket.internal; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; -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.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(); + + 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(Frame.Error.from(1, new Exception())); + 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 + 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/SwitchTransformFluxTest.java b/rsocket-core/src/test/java/io/rsocket/internal/SwitchTransformFluxTest.java deleted file mode 100644 index aaa1fa4f0..000000000 --- a/rsocket-core/src/test/java/io/rsocket/internal/SwitchTransformFluxTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package io.rsocket.internal; - -import java.time.Duration; - -import org.junit.Test; -import reactor.core.publisher.Flux; -import reactor.test.StepVerifier; -import reactor.test.publisher.TestPublisher; - -public class SwitchTransformFluxTest { - - @Test - public void backpressureTest() { - TestPublisher publisher = TestPublisher.createCold(); - - Flux switchTransformed = publisher - .flux() - .transform(flux -> new SwitchTransformFlux<>( - flux, - (first, innerFlux) -> innerFlux.map(String::valueOf) - )); - - publisher.next(1L); - - StepVerifier.create(switchTransformed, 0) - .thenRequest(1) - .expectNext("1") - .thenRequest(1) - .then(() -> publisher.next(2L)) - .expectNext("2") - .then(publisher::complete) - .expectComplete() - .verify(Duration.ofSeconds(10)); - - publisher.assertWasRequested(); - publisher.assertNoRequestOverflow(); - } - - @Test - public void shouldErrorOnOverflowTest() { - TestPublisher publisher = TestPublisher.createCold(); - - Flux switchTransformed = publisher - .flux() - .transform(flux -> new SwitchTransformFlux<>( - flux, - (first, innerFlux) -> innerFlux.map(String::valueOf) - )); - - publisher.next(1L); - - StepVerifier.create(switchTransformed, 0) - .thenRequest(1) - .expectNext("1") - .then(() -> publisher.next(2L)) - .expectError() - .verify(Duration.ofSeconds(10)); - - publisher.assertWasRequested(); - publisher.assertNoRequestOverflow(); - } - - @Test - public void shouldPropagateonCompleteCorrectly() { - Flux switchTransformed = Flux.empty() - .transform(flux -> new SwitchTransformFlux<>( - flux, - (first, innerFlux) -> innerFlux.map(String::valueOf) - )); - - StepVerifier.create(switchTransformed) - .expectComplete() - .verify(Duration.ofSeconds(10)); - } - - @Test - public void shouldPropagateErrorCorrectly() { - Flux switchTransformed = Flux.error(new RuntimeException("hello")) - .transform(flux -> new SwitchTransformFlux<>( - flux, - (first, innerFlux) -> innerFlux.map(String::valueOf) - )); - - StepVerifier.create(switchTransformed) - .expectErrorMessage("hello") - .verify(Duration.ofSeconds(10)); - } - - @Test - public void shouldBeAbleToBeCancelledProperly() { - TestPublisher publisher = TestPublisher.createCold(); - Flux switchTransformed = publisher - .flux() - .transform(flux -> new SwitchTransformFlux<>( - flux, - (first, innerFlux) -> innerFlux.map(String::valueOf) - )); - - publisher.emit(1, 2, 3, 4, 5); - - StepVerifier.create(switchTransformed, 0) - .thenCancel() - .verify(Duration.ofSeconds(10)); - - publisher.assertCancelled(); - publisher.assertWasRequested(); - - } -} \ No newline at end of file diff --git a/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java index 0dc7d9090..b0c6c6fd8 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,84 +16,353 @@ package io.rsocket.internal; -import io.rsocket.Payload; -import io.rsocket.util.EmptyPayload; -import java.util.concurrent.CountDownLatch; -import org.junit.Assert; -import org.junit.Test; +import static org.assertj.core.api.Assertions.assertThat; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCountUtil; +import io.rsocket.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 { - @Test - public void testOnNextBeforeSubscribe_10() { - testOnNextBeforeSubscribeN(10); - } - @Test - public void testOnNextBeforeSubscribe_100() { - testOnNextBeforeSubscribeN(100); + @BeforeAll + public static void setup() { + Hooks.onErrorDropped(__ -> {}); } - @Test - public void testOnNextBeforeSubscribe_10_000() { - testOnNextBeforeSubscribeN(10_000); + @AfterAll + public static void teardown() { + Hooks.resetOnErrorDropped(); } - @Test - public void testOnNextBeforeSubscribe_100_000() { - testOnNextBeforeSubscribeN(100_000); - } + @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<>(); - @Test - public void testOnNextBeforeSubscribe_1_000_000() { - testOnNextBeforeSubscribeN(1_000_000); - } + for (int i = 0; i < n; i++) { + processor.onNext(Unpooled.EMPTY_BUFFER); + } - @Test - public void testOnNextBeforeSubscribe_10_000_000() { - testOnNextBeforeSubscribeN(10_000_000); + processor.onComplete(); + + StepVerifier.create(processor.count()).expectNext(Long.valueOf(n)).verifyComplete(); } - public void testOnNextBeforeSubscribeN(int n) { - UnboundedProcessor processor = new UnboundedProcessor<>(); + @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(EmptyPayload.INSTANCE); + processor.onNext(Unpooled.EMPTY_BUFFER); } - processor.onComplete(); + 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<>(); - long count = processor.count().block(); + 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); - Assert.assertEquals(n, count); + 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(); + } } - @Test - public void testOnNextAfterSubscribe_10() throws Exception { - testOnNextAfterSubscribeN(10); + @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(); + } } - @Test - public void testOnNextAfterSubscribe_100() throws Exception { - testOnNextAfterSubscribeN(100); + @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(); + } } - @Test - public void testOnNextAfterSubscribe_1000() throws Exception { - testOnNextAfterSubscribeN(1000); + @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(); + } } - public void testOnNextAfterSubscribeN(int n) throws Exception { - CountDownLatch latch = new CountDownLatch(n); - UnboundedProcessor processor = new UnboundedProcessor<>(); - processor.log().doOnNext(integer -> latch.countDown()).subscribe(); + @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<>(); - for (int i = 0; i < n; i++) { - System.out.println("onNexting -> " + i); - processor.onNext(EmptyPayload.INSTANCE); + 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); - processor.drain(); + 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); + } - latch.await(); + 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/ResumeCacheTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ResumeCacheTest.java deleted file mode 100644 index 25d3f1696..000000000 --- a/rsocket-core/src/test/java/io/rsocket/resume/ResumeCacheTest.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.resume; - -import static org.junit.Assert.assertEquals; - -import io.rsocket.Frame; -import io.rsocket.framing.FrameType; -import io.rsocket.util.DefaultPayload; -import org.junit.Test; -import reactor.core.publisher.Flux; - -public class ResumeCacheTest { - private Frame CANCEL = Frame.Cancel.from(1); - private Frame STREAM = - Frame.Request.from(1, FrameType.REQUEST_STREAM, DefaultPayload.create("Test"), 100); - - private ResumeCache cache = new ResumeCache(ResumePositionCounter.frames(), 2); - - @Test - public void startsEmpty() { - Flux x = cache.resend(0); - assertEquals(0L, (long) x.count().block()); - cache.updateRemotePosition(0); - } - - @Test(expected = IllegalStateException.class) - public void failsForFutureUpdatePosition() { - cache.updateRemotePosition(1); - } - - @Test(expected = IllegalStateException.class) - public void failsForFutureResend() { - cache.resend(1); - } - - @Test - public void updatesPositions() { - assertEquals(0, cache.getRemotePosition()); - assertEquals(0, cache.getCurrentPosition()); - assertEquals(0, cache.getEarliestResendPosition()); - assertEquals(0, cache.size()); - - cache.sent(STREAM); - - assertEquals(0, cache.getRemotePosition()); - assertEquals(14, cache.getCurrentPosition()); - assertEquals(0, cache.getEarliestResendPosition()); - assertEquals(1, cache.size()); - - cache.updateRemotePosition(14); - - assertEquals(14, cache.getRemotePosition()); - assertEquals(14, cache.getCurrentPosition()); - assertEquals(14, cache.getEarliestResendPosition()); - assertEquals(0, cache.size()); - - cache.sent(CANCEL); - - assertEquals(14, cache.getRemotePosition()); - assertEquals(20, cache.getCurrentPosition()); - assertEquals(14, cache.getEarliestResendPosition()); - assertEquals(1, cache.size()); - - cache.updateRemotePosition(20); - - assertEquals(20, cache.getRemotePosition()); - assertEquals(20, cache.getCurrentPosition()); - assertEquals(20, cache.getEarliestResendPosition()); - assertEquals(0, cache.size()); - - cache.sent(STREAM); - - assertEquals(20, cache.getRemotePosition()); - assertEquals(34, cache.getCurrentPosition()); - assertEquals(20, cache.getEarliestResendPosition()); - assertEquals(1, cache.size()); - } - - @Test - public void supportsZeroBuffer() { - cache = new ResumeCache(ResumePositionCounter.frames(), 0); - - cache.sent(STREAM); - cache.sent(STREAM); - cache.sent(STREAM); - - assertEquals(0, cache.getRemotePosition()); - assertEquals(42, cache.getCurrentPosition()); - assertEquals(42, cache.getEarliestResendPosition()); - assertEquals(0, cache.size()); - } - - @Test - public void supportsFrameCountBuffers() { - cache = new ResumeCache(ResumePositionCounter.size(), 100); - - assertEquals(0, cache.getRemotePosition()); - assertEquals(0, cache.getCurrentPosition()); - assertEquals(0, cache.getEarliestResendPosition()); - assertEquals(0, cache.size()); - - cache.sent(STREAM); - - assertEquals(0, cache.getRemotePosition()); - assertEquals(14, cache.getCurrentPosition()); - assertEquals(0, cache.getEarliestResendPosition()); - assertEquals(14, cache.size()); - - cache.updateRemotePosition(14); - - assertEquals(14, cache.getRemotePosition()); - assertEquals(14, cache.getCurrentPosition()); - assertEquals(14, cache.getEarliestResendPosition()); - assertEquals(0, cache.size()); - - cache.sent(CANCEL); - - assertEquals(14, cache.getRemotePosition()); - assertEquals(20, cache.getCurrentPosition()); - assertEquals(14, cache.getEarliestResendPosition()); - assertEquals(6, cache.size()); - } -} 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/resume/ResumeTokenTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ResumeTokenTest.java deleted file mode 100644 index 7a2fafc8a..000000000 --- a/rsocket-core/src/test/java/io/rsocket/resume/ResumeTokenTest.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.resume; - -import static org.junit.Assert.assertEquals; - -import java.util.UUID; -import org.junit.Test; - -public class ResumeTokenTest { - @Test - public void testFromUuid() { - UUID x = UUID.fromString("3bac9870-3873-403a-99f4-9728aa8c7860"); - - ResumeToken t = ResumeToken.bytes(ResumeToken.getBytesFromUUID(x)); - ResumeToken t2 = ResumeToken.bytes(ResumeToken.getBytesFromUUID(x)); - - assertEquals("3bac98703873403a99f49728aa8c7860", t.toString()); - - assertEquals(t.hashCode(), t2.hashCode()); - assertEquals(t, t2); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ResumeUtilTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ResumeUtilTest.java deleted file mode 100644 index ea5486e14..000000000 --- a/rsocket-core/src/test/java/io/rsocket/resume/ResumeUtilTest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.resume; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import io.rsocket.Frame; -import io.rsocket.framing.FrameType; -import io.rsocket.util.DefaultPayload; -import org.junit.Test; - -public class ResumeUtilTest { - private Frame CANCEL = Frame.Cancel.from(1); - private Frame STREAM = - Frame.Request.from(1, FrameType.REQUEST_STREAM, DefaultPayload.create("Test"), 100); - - @Test - public void testSupportedTypes() { - assertTrue(ResumeUtil.isTracked(FrameType.REQUEST_STREAM)); - assertTrue(ResumeUtil.isTracked(FrameType.REQUEST_CHANNEL)); - assertTrue(ResumeUtil.isTracked(FrameType.REQUEST_RESPONSE)); - assertTrue(ResumeUtil.isTracked(FrameType.REQUEST_N)); - assertTrue(ResumeUtil.isTracked(FrameType.CANCEL)); - assertTrue(ResumeUtil.isTracked(FrameType.ERROR)); - assertTrue(ResumeUtil.isTracked(FrameType.REQUEST_FNF)); - assertTrue(ResumeUtil.isTracked(FrameType.PAYLOAD)); - } - - @Test - public void testUnsupportedTypes() { - assertFalse(ResumeUtil.isTracked(FrameType.METADATA_PUSH)); - assertFalse(ResumeUtil.isTracked(FrameType.RESUME)); - assertFalse(ResumeUtil.isTracked(FrameType.RESUME_OK)); - assertFalse(ResumeUtil.isTracked(FrameType.SETUP)); - assertFalse(ResumeUtil.isTracked(FrameType.EXT)); - assertFalse(ResumeUtil.isTracked(FrameType.KEEPALIVE)); - } - - @Test - public void testOffset() { - assertEquals(6, ResumeUtil.offset(CANCEL)); - assertEquals(14, ResumeUtil.offset(STREAM)); - } -} 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 93eebd8ab..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 @@ -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 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; - onClose = 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,10 +54,15 @@ public Mono send(Publisher frame) { } @Override - public Flux receive() { + public Flux receive() { return receive.doOnNext(f -> System.out.println(name + " - " + f.toString())); } + @Override + public ByteBufAllocator alloc() { + return allocator; + } + @Override public void dispose() { onClose.onComplete(); 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 358506fcf..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 @@ -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,34 +40,40 @@ 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 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 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<>(); - onClose = 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); + sendSink.next(frame); }) .doOnError(throwable -> logger.error("Error in send stream on test connection.", throwable)) .subscribe(subscriber); @@ -74,10 +82,15 @@ public Mono send(Publisher frames) { } @Override - public Flux receive() { + public Flux receive() { return received; } + @Override + public ByteBufAllocator alloc() { + return allocator; + } + @Override public double availability() { return availability; @@ -98,7 +111,7 @@ public Mono onClose() { return onClose; } - public Frame awaitSend() throws InterruptedException { + public ByteBuf awaitSend() throws InterruptedException { return sent.take(); } @@ -106,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); } } @@ -129,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/uri/TestUriHandler.java b/rsocket-core/src/test/java/io/rsocket/uri/TestUriHandler.java deleted file mode 100644 index 46634e94b..000000000 --- a/rsocket-core/src/test/java/io/rsocket/uri/TestUriHandler.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.uri; - -import io.rsocket.test.util.TestDuplexConnection; -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import java.net.URI; -import java.util.Objects; -import java.util.Optional; -import reactor.core.publisher.Mono; - -public final class TestUriHandler implements UriHandler { - - private static final String SCHEME = "test"; - - @Override - public Optional buildClient(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - if (!SCHEME.equals(uri.getScheme())) { - return Optional.empty(); - } - - return Optional.of(() -> Mono.just(new TestDuplexConnection())); - } - - @Override - public Optional buildServer(URI uri) { - return Optional.empty(); - } -} 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 9e7b92f65..000000000 --- a/rsocket-core/src/test/java/io/rsocket/uri/UriTransportRegistryTest.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 index 45ee4eacb..3f97ab9dc 100644 --- a/rsocket-core/src/test/java/io/rsocket/util/DefaultPayloadTest.java +++ b/rsocket-core/src/test/java/io/rsocket/util/DefaultPayloadTest.java @@ -16,10 +16,17 @@ package io.rsocket.util; -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; +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 { @@ -48,4 +55,55 @@ 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 index 6ce023783..46e0f77f4 100644 --- a/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java +++ b/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java @@ -16,10 +16,10 @@ package io.rsocket.util; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; +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; @@ -160,4 +160,28 @@ void requireUnsignedShortOverFlow() { .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/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 068667aa7..000000000 --- a/rsocket-core/src/test/resources/META-INF/services/io.rsocket.uri.UriHandler +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2015-2018 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -io.rsocket.uri.TestUriHandler diff --git a/rsocket-core/src/test/resources/META-INF/services/org.assertj.core.presentation.Representation b/rsocket-core/src/test/resources/META-INF/services/org.assertj.core.presentation.Representation index 723d87c20..9ac418a0c 100644 --- a/rsocket-core/src/test/resources/META-INF/services/org.assertj.core.presentation.Representation +++ b/rsocket-core/src/test/resources/META-INF/services/org.assertj.core.presentation.Representation @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -io.rsocket.framing.ByteBufRepresentation \ No newline at end of file +io.rsocket.frame.ByteBufRepresentation \ No newline at end of file diff --git a/rsocket-examples/build.gradle b/rsocket-examples/build.gradle index 04cd59c50..01e80cfa1 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -22,10 +22,13 @@ dependencies { implementation project(':rsocket-core') implementation project(':rsocket-transport-local') implementation project(':rsocket-transport-netty') + runtimeOnly 'ch.qos.logback:logback-classic' 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' // TODO: Remove after JUnit5 migration testCompileOnly 'junit:junit' 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 386154f20..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,59 +16,48 @@ 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.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 -> DefaultPayload.create("Hello"))) .map(Payload::getDataUtf8) - .doOnNext(System.out::println) + .doOnNext(logger::debug) .take(10) .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::getDataUtf8) - .map(s -> "Echo: " + s) - .map(DefaultPayload::create); - } - }); - } - } } 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 537485fa4..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,65 +16,54 @@ 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.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(DefaultPayload.create("Hello")) - .map(Payload::getDataUtf8) - .onErrorReturn("error") - .doOnNext(System.out::println) - .block(); + RSocketServer.create(SocketAcceptor.with(rsocket)) + .bind(TcpServerTransport.create("localhost", 7000)) + .subscribe(); - socket - .requestResponse(DefaultPayload.create("Hello")) - .map(Payload::getDataUtf8) - .onErrorReturn("error") - .doOnNext(System.out::println) - .block(); + RSocket socket = + RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000)).block(); - socket - .requestResponse(DefaultPayload.create("Hello")) - .map(Payload::getDataUtf8) - .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.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/StreamingClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java similarity index 53% rename from rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java rename to rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java index 57a659c1d..8116ad4ae 100644 --- 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/ClientStreamingToServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,51 +16,43 @@ package io.rsocket.examples.transport.tcp.stream; -import io.rsocket.*; +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; -import reactor.core.publisher.Mono; -public final class StreamingClient { +public final class ClientStreamingToServer { + + private static final Logger logger = LoggerFactory.getLogger(ClientStreamingToServer.class); public static void main(String[] args) { - RSocketFactory.receive() - .acceptor(new SocketAcceptorImpl()) - .transport(TcpServerTransport.create("localhost", 7000)) - .start() + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> + Flux.interval(Duration.ofMillis(100)) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)))) + .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 .requestStream(DefaultPayload.create("Hello")) .map(Payload::getDataUtf8) - .doOnNext(System.out::println) + .doOnNext(logger::debug) .take(10) .then() .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 requestStream(Payload payload) { - return Flux.interval(Duration.ofMillis(100)) - .map(aLong -> DefaultPayload.create("Interval: " + aLong)); - } - }); - } - } } 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/stream/ServerStreamingToClient.java similarity index 56% rename from rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/duplex/DuplexClient.java rename to rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ServerStreamingToClient.java index c0a271d66..f5b1e00e5 100644 --- 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/stream/ServerStreamingToClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,14 @@ * limitations under the License. */ -package io.rsocket.examples.transport.tcp.duplex; +package io.rsocket.examples.transport.tcp.stream; + +import static io.rsocket.SocketAcceptor.forRequestStream; -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.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; @@ -27,39 +29,33 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -public final class DuplexClient { +public final class ServerStreamingToClient { public static void main(String[] args) { - RSocketFactory.receive() - .acceptor( - (setup, reactiveSocket) -> { - reactiveSocket + + RSocketServer.create( + (setup, rsocket) -> { + rsocket .requestStream(DefaultPayload.create("Hello-Bidi")) .map(Payload::getDataUtf8) .log() .subscribe(); - return Mono.just(new AbstractRSocket() {}); + return Mono.just(new RSocket() {}); }) - .transport(TcpServerTransport.create("localhost", 7000)) - .start() + .bind(TcpServerTransport.create("localhost", 7000)) .subscribe(); - RSocket socket = - RSocketFactory.connect() + RSocket rsocket = + RSocketConnector.create() .acceptor( - rSocket -> - new AbstractRSocket() { - @Override - public Flux requestStream(Payload payload) { - return Flux.interval(Duration.ofSeconds(1)) - .map(aLong -> DefaultPayload.create("Bi-di Response => " + aLong)); - } - }) - .transport(TcpClientTransport.create("localhost", 7000)) - .start() + forRequestStream( + payload -> + Flux.interval(Duration.ofSeconds(1)) + .map(aLong -> DefaultPayload.create("Bi-di Response => " + aLong)))) + .connect(TcpClientTransport.create("localhost", 7000)) .block(); - socket.onClose().block(); + rsocket.onClose().block(); } } 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/log4j.properties b/rsocket-examples/src/main/resources/log4j.properties deleted file mode 100644 index 035f18ebd..000000000 --- a/rsocket-examples/src/main/resources/log4j.properties +++ /dev/null @@ -1,20 +0,0 @@ -# -# Copyright 2015-2018 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -log4j.rootLogger=DEBUG, 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] %c{1} - %m%n \ No newline at end of file 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 627b1d7da..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,12 +23,13 @@ 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.CloseableChannel; @@ -38,7 +39,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; @@ -48,35 +48,54 @@ public class IntegrationTest { - private static final RSocketInterceptor clientPlugin; - private static final RSocketInterceptor serverPlugin; - private static final DuplexConnectionInterceptor connectionPlugin; - 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 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; @@ -95,17 +114,8 @@ public void startup() { requestCount = new AtomicInteger(); disconnectionCounter = new CountDownLatch(1); - TcpServerTransport serverTransport = TcpServerTransport.create(0); - server = - RSocketFactory.receive() - .addServerPlugin(serverPlugin) - .addConnectionPlugin(connectionPlugin) - .errorConsumer( - t -> { - errorCount.incrementAndGet(); - }) - .acceptor( + RSocketServer.create( (setup, sendingSocket) -> { sendingSocket .onClose() @@ -113,7 +123,7 @@ public void startup() { .subscribe(); return Mono.just( - new AbstractRSocket() { + new RSocket() { @Override public Mono requestResponse(Payload payload) { return Mono.just(DefaultPayload.create("RESPONSE", "METADATA")) @@ -132,16 +142,24 @@ public Flux requestChannel(Publisher payloads) { } }); }) - .transport(serverTransport) - .start() + .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(); } @@ -154,8 +172,10 @@ public void teardown() { public void testRequest() { 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); } @@ -181,8 +201,6 @@ public void testCallRequestWithErrorAndThenRequest() { } catch (Throwable t) { } - Assert.assertEquals(1, errorCount.incrementAndGet()); - 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 index 6c8f0e8fa..48e5baaa7 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java @@ -1,9 +1,10 @@ package io.rsocket.integration; -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.test.SlowTest; import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.CloseableChannel; @@ -14,32 +15,26 @@ import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; public class InteractionsLoadTest { @Test @SlowTest public void channel() { - TcpServerTransport serverTransport = TcpServerTransport.create(0); - CloseableChannel server = - RSocketFactory.receive() - .acceptor((setup, rsocket) -> Mono.just(new EchoRSocket())) - .transport(serverTransport) - .start() + RSocketServer.create(SocketAcceptor.with(new EchoRSocket())) + .bind(TcpServerTransport.create("localhost", 0)) .block(Duration.ofSeconds(10)); - TcpClientTransport transport = TcpClientTransport.create(server.address()); - - RSocket client = - RSocketFactory.connect().transport(transport).start().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 -> - client + clientRSocket .requestChannel( input().onBackpressureDrop().map(iv -> DefaultPayload.create("foo"))) .limitRate(10000), @@ -70,7 +65,8 @@ private static Flux input() { return interval; } - private static class EchoRSocket extends AbstractRSocket { + private static class EchoRSocket implements RSocket { + @Override public Flux requestChannel(Publisher payloads) { return Flux.from(payloads) 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 f5d048508..de27bcb9b 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java @@ -19,10 +19,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -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.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; @@ -40,26 +40,20 @@ import reactor.core.scheduler.Schedulers; public class TcpIntegrationTest { - private AbstractRSocket handler; + private RSocket handler; private CloseableChannel server; @Before public void startup() { - TcpServerTransport serverTransport = TcpServerTransport.create(0); server = - RSocketFactory.receive() - .acceptor((setup, sendingSocket) -> Mono.just(new RSocketProxy(handler))) - .transport(serverTransport) - .start() + 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 @@ -67,10 +61,10 @@ public void cleanup() { server.dispose(); } - @Test(timeout = 5_000L) + @Test(timeout = 15_000L) public void testCompleteWithoutNext() { handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { return Flux.empty(); @@ -83,10 +77,10 @@ public Flux requestStream(Payload payload) { assertFalse(hasElements); } - @Test(timeout = 5_000L) + @Test(timeout = 15_000L) public void testSingleStream() { handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { return Flux.just(DefaultPayload.create("RESPONSE", "METADATA")); @@ -100,10 +94,10 @@ public Flux requestStream(Payload payload) { assertEquals("RESPONSE", result.getDataUtf8()); } - @Test(timeout = 5_000L) + @Test(timeout = 15_000L) public void testZeroPayload() { handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { return Flux.just(EmptyPayload.INSTANCE); @@ -117,10 +111,10 @@ public Flux requestStream(Payload payload) { assertEquals("", result.getDataUtf8()); } - @Test(timeout = 5_000L) + @Test(timeout = 15_000L) public void testRequestResponseErrors() { handler = - new AbstractRSocket() { + new RSocket() { boolean first = true; @Override @@ -151,7 +145,7 @@ public Mono requestResponse(Payload payload) { 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(); @@ -160,7 +154,7 @@ public void testTwoConcurrentStreams() throws InterruptedException { map.put("REQUEST2", processor2); handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { return map.get(payload.getDataUtf8()); diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java index ec1d41bf9..7d34ba478 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,60 +16,41 @@ package io.rsocket.integration; -import io.rsocket.*; +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.ClientTransport; -import io.rsocket.transport.ServerTransport; 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 java.util.function.Supplier; import org.junit.Test; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; public class TestingStreaming { - private Supplier> serverSupplier = - () -> LocalServerTransport.create("test"); - - private Supplier clientSupplier = () -> LocalClientTransport.create("test"); + LocalServerTransport serverTransport = LocalServerTransport.create("test"); @Test(expected = ApplicationErrorException.class) public void testRangeButThrowException() { Closeable server = null; try { server = - RSocketFactory.receive() - .errorConsumer(Throwable::printStackTrace) - .acceptor( - (connectionSetupPayload, rSocket) -> { - AbstractRSocket abstractRSocket = - new AbstractRSocket() { - @Override - public double availability() { - return 1.0; - } - - @Override - public Flux requestStream(Payload payload) { - return Flux.range(1, 1000) - .doOnNext( - i -> { - if (i > 3) { - throw new RuntimeException("BOOM!"); - } - }) - .map(l -> DefaultPayload.create("l -> " + l)) - .cast(Payload.class); - } - }; - - return Mono.just(abstractRSocket); - }) - .transport(serverSupplier.get()) - .start() + 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(); @@ -85,29 +66,13 @@ public void testRangeOfConsumers() { Closeable server = null; try { server = - RSocketFactory.receive() - .errorConsumer(Throwable::printStackTrace) - .acceptor( - (connectionSetupPayload, rSocket) -> { - AbstractRSocket abstractRSocket = - new AbstractRSocket() { - @Override - public double availability() { - return 1.0; - } - - @Override - public Flux requestStream(Payload payload) { - return Flux.range(1, 1000) - .map(l -> DefaultPayload.create("l -> " + l)) - .cast(Payload.class); - } - }; - - return Mono.just(abstractRSocket); - }) - .transport(serverSupplier.get()) - .start() + 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(); @@ -119,10 +84,7 @@ public Flux requestStream(Payload payload) { } private Flux consumer(String s) { - return RSocketFactory.connect() - .errorConsumer(Throwable::printStackTrace) - .transport(clientSupplier) - .start() + return RSocketConnector.connectWith(LocalClientTransport.create("test")) .flatMapMany( rSocket -> { AtomicInteger count = new AtomicInteger(); @@ -135,31 +97,15 @@ private Flux consumer(String s) { @Test public void testSingleConsumer() { Closeable server = null; - try { server = - RSocketFactory.receive() - .acceptor( - (connectionSetupPayload, rSocket) -> { - AbstractRSocket abstractRSocket = - new AbstractRSocket() { - @Override - public double availability() { - return 1.0; - } - - @Override - public Flux requestStream(Payload payload) { - return Flux.range(1, 10_000) - .map(l -> DefaultPayload.create("l -> " + l)) - .cast(Payload.class); - } - }; - - return Mono.just(abstractRSocket); - }) - .transport(serverSupplier.get()) - .start() + 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(); 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 51731fc15..000000000 --- a/rsocket-examples/src/test/resources/log4j.properties +++ /dev/null @@ -1,21 +0,0 @@ -# -# Copyright 2015-2018 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -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 a2c8b73c7..d70e5b2cc 100644 --- a/rsocket-load-balancer/build.gradle +++ b/rsocket-load-balancer/build.gradle @@ -17,8 +17,7 @@ plugins { id 'java-library' id 'maven-publish' - id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' + id 'signing' } dependencies { @@ -34,6 +33,7 @@ dependencies { 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 82c5b0cc0..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 @@ -16,24 +16,19 @@ 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.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; @@ -47,6 +42,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; +import reactor.util.retry.Retry; /** * An implementation of {@link Mono} that load balances across a pool of RSockets and emits one when @@ -57,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; @@ -68,38 +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 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 @@ -116,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, @@ -126,15 +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.minPendings = minPendings; this.maxPendings = maxPendings; @@ -148,10 +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.fromCallable(this::select); + onClose.doFinally(signalType -> pool.dispose()).subscribe(); } public static LoadBalancedRSocketMono create( @@ -168,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, @@ -187,72 +223,65 @@ public static LoadBalancedRSocketMono create( maxPendings, minAperture, maxAperture, - maxRefreshPeriodMs) { + maxRefreshPeriodMs, + 5, + Duration.ofMillis(500), + Duration.ofSeconds(5)) { @Override public void subscribe(CoreSubscriber s) { - started.then(rSocketMono).subscribe(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; } } } @@ -290,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); @@ -304,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; @@ -356,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.dispose(); - if (refresh) { - refreshSockets(); - } - } catch (Exception e) { - logger.warn("Exception while closing a RSocket", e); + logger.debug("Disposing slowest WeightedSocket {}", slowest); + slowest.dispose(); } } @@ -388,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) { @@ -413,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); } } @@ -460,7 +448,7 @@ public synchronized String toString() { return "LoadBalancer(a:" + activeSockets.size() + ", f: " - + activeFactories.size() + + pool.poolSize() + ", avgPendings=" + pendings.value() + ", targetAperture=" @@ -475,8 +463,6 @@ public synchronized String toString() { @Override public void dispose() { synchronized (this) { - factoryRefresher.close(); - activeFactories.clear(); activeSockets.forEach(WeightedSocket::dispose); activeSockets.clear(); onClose.onComplete(); @@ -493,147 +479,6 @@ public Mono onClose() { return onClose; } - /** - * 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.dispose(); - } 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: ") - .append(activeFactories.size()) - .append(")\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(); - } - } - - 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() {} - } - - 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. @@ -692,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 @@ -711,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; @@ -730,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 Mono.from( - 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 Flux.from( - 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 Mono.from( - 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 Mono.from( - 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 Flux.from( - 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() { @@ -845,14 +790,24 @@ private synchronized void observe(double rtt) { higherQuantile.insert(rtt); } + @Override + public double availability() { + return availability; + } + @Override public void dispose() { - source.dispose(); + onClose.onComplete(); } @Override public boolean isDisposed() { - return source.isDisposed(); + return onClose.isDisposed(); + } + + @Override + public Mono onClose() { + return onClose; } @Override @@ -872,8 +827,7 @@ public String toString() { + pending + " availability= " + availability() - + ")->" - + source; + + ")->"; } @Override @@ -953,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); } @@ -999,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/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-core/src/main/java/io/rsocket/framing/MetadataAndDataFrame.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/package-info.java similarity index 72% rename from rsocket-core/src/main/java/io/rsocket/framing/MetadataAndDataFrame.java rename to rsocket-load-balancer/src/main/java/io/rsocket/client/filter/package-info.java index a7f904b7b..55ce5646c 100644 --- a/rsocket-core/src/main/java/io/rsocket/framing/MetadataAndDataFrame.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.rsocket.framing; +@NonNullApi +package io.rsocket.client.filter; -/** An RSocket frame that only metadata and data. */ -public interface MetadataAndDataFrame extends MetadataFrame, DataFrame {} +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/package-info.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/package-info.java new file mode 100644 index 000000000..ec21dee96 --- /dev/null +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NonNullApi +package io.rsocket.client; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/stat/package-info.java b/rsocket-load-balancer/src/main/java/io/rsocket/stat/package-info.java new file mode 100644 index 000000000..cfb071175 --- /dev/null +++ b/rsocket-load-balancer/src/main/java/io/rsocket/stat/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NonNullApi +package io.rsocket.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 6806b9037..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 @@ -19,19 +19,15 @@ import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.client.filter.RSocketSupplier; -import io.rsocket.util.EmptyPayload; -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(EmptyPayload.INSTANCE) - .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/resources/log4j.properties b/rsocket-load-balancer/src/test/resources/log4j.properties deleted file mode 100644 index 8fc3a9cdd..000000000 --- a/rsocket-load-balancer/src/test/resources/log4j.properties +++ /dev/null @@ -1,20 +0,0 @@ -# -# Copyright 2015-2018 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -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 index 5f2aeb16f..fd89aeae0 100644 --- a/rsocket-micrometer/build.gradle +++ b/rsocket-micrometer/build.gradle @@ -17,8 +17,7 @@ plugins { id 'java-library' id 'maven-publish' - id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' + id 'signing' } dependencies { @@ -27,8 +26,6 @@ dependencies { implementation 'org.slf4j:slf4j-api' - compileOnly 'com.google.code.findbugs:jsr305' - testImplementation project(':rsocket-test') testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java index 97a7ea472..c8b22382a 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java @@ -16,33 +16,14 @@ package io.rsocket.micrometer; -import static io.rsocket.framing.FrameType.CANCEL; -import static io.rsocket.framing.FrameType.COMPLETE; -import static io.rsocket.framing.FrameType.ERROR; -import static io.rsocket.framing.FrameType.EXT; -import static io.rsocket.framing.FrameType.KEEPALIVE; -import static io.rsocket.framing.FrameType.LEASE; -import static io.rsocket.framing.FrameType.METADATA_PUSH; -import static io.rsocket.framing.FrameType.NEXT; -import static io.rsocket.framing.FrameType.NEXT_COMPLETE; -import static io.rsocket.framing.FrameType.PAYLOAD; -import static io.rsocket.framing.FrameType.REQUEST_CHANNEL; -import static io.rsocket.framing.FrameType.REQUEST_FNF; -import static io.rsocket.framing.FrameType.REQUEST_N; -import static io.rsocket.framing.FrameType.REQUEST_RESPONSE; -import static io.rsocket.framing.FrameType.REQUEST_STREAM; -import static io.rsocket.framing.FrameType.RESUME; -import static io.rsocket.framing.FrameType.RESUME_OK; -import static io.rsocket.framing.FrameType.SETUP; - -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 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; -import io.rsocket.framing.FrameType; +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; @@ -102,6 +83,11 @@ final class MicrometerDuplexConnection implements DuplexConnection { this.frameCounters = new FrameCounters(connectionType, meterRegistry, tags); } + @Override + public ByteBufAllocator alloc() { + return delegate.alloc(); + } + @Override public void dispose() { delegate.dispose(); @@ -114,18 +100,18 @@ public Mono onClose() { } @Override - public Flux receive() { + public Flux receive() { return delegate.receive().doOnNext(frameCounters); } @Override - public Mono send(Publisher frames) { + 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 static final class FrameCounters implements Consumer { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @@ -189,9 +175,23 @@ private FrameCounters(Type connectionType, MeterRegistry meterRegistry, Tag... t 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(Frame frame) { - FrameType frameType = frame.getType(); + public void accept(ByteBuf frame) { + FrameType frameType = FrameHeaderCodec.frameType(frame); switch (frameType) { case SETUP: @@ -253,19 +253,5 @@ public void accept(Frame frame) { this.unknown.increment(); } } - - 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)); - } } } diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnectionInterceptor.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnectionInterceptor.java index 92f4a0889..b94e969ec 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnectionInterceptor.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnectionInterceptor.java @@ -20,7 +20,7 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; import io.rsocket.DuplexConnection; -import io.rsocket.framing.FrameType; +import io.rsocket.frame.FrameType; import io.rsocket.plugins.DuplexConnectionInterceptor; import java.util.Objects; diff --git a/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerDuplexConnectionTest.java b/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerDuplexConnectionTest.java index 36bec8020..03abd2084 100644 --- a/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerDuplexConnectionTest.java +++ b/rsocket-micrometer/src/test/java/io/rsocket/micrometer/MicrometerDuplexConnectionTest.java @@ -16,50 +16,20 @@ package io.rsocket.micrometer; -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.FrameType.CANCEL; -import static io.rsocket.framing.FrameType.COMPLETE; -import static io.rsocket.framing.FrameType.ERROR; -import static io.rsocket.framing.FrameType.KEEPALIVE; -import static io.rsocket.framing.FrameType.LEASE; -import static io.rsocket.framing.FrameType.METADATA_PUSH; -import static io.rsocket.framing.FrameType.REQUEST_CHANNEL; -import static io.rsocket.framing.FrameType.REQUEST_FNF; -import static io.rsocket.framing.FrameType.REQUEST_N; -import static io.rsocket.framing.FrameType.REQUEST_RESPONSE; -import static io.rsocket.framing.FrameType.REQUEST_STREAM; -import static io.rsocket.framing.FrameType.RESUME; -import static io.rsocket.framing.FrameType.RESUME_OK; -import static io.rsocket.framing.FrameType.SETUP; +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.createTestCancelFrame; -import static io.rsocket.test.TestFrames.createTestErrorFrame; -import static io.rsocket.test.TestFrames.createTestKeepaliveFrame; -import static io.rsocket.test.TestFrames.createTestLeaseFrame; -import static io.rsocket.test.TestFrames.createTestMetadataPushFrame; -import static io.rsocket.test.TestFrames.createTestPayloadFrame; -import static io.rsocket.test.TestFrames.createTestRequestChannelFrame; -import static io.rsocket.test.TestFrames.createTestRequestFireAndForgetFrame; -import static io.rsocket.test.TestFrames.createTestRequestNFrame; -import static io.rsocket.test.TestFrames.createTestRequestResponseFrame; -import static io.rsocket.test.TestFrames.createTestRequestStreamFrame; -import static io.rsocket.test.TestFrames.createTestResumeFrame; -import static io.rsocket.test.TestFrames.createTestResumeOkFrame; -import static io.rsocket.test.TestFrames.createTestSetupFrame; -import static io.rsocket.util.AbstractionLeakingFrameUtils.toAbstractionLeakingFrame; +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.RETURNS_SMART_NULLS; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +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; -import io.rsocket.framing.FrameType; +import io.rsocket.frame.FrameType; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -70,7 +40,6 @@ import reactor.core.publisher.Operators; import reactor.test.StepVerifier; -// TODO: Flyweight Frames don't support EXT frames, so can't be tested today final class MicrometerDuplexConnectionTest { private final DuplexConnection delegate = mock(DuplexConnection.class, RETURNS_SMART_NULLS); @@ -142,22 +111,20 @@ void onClose() { @DisplayName("receive gathers metrics") @Test void receive() { - Flux frames = + Flux frames = Flux.just( - toAbstractionLeakingFrame(DEFAULT, 1, createTestCancelFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestErrorFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestKeepaliveFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestLeaseFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestMetadataPushFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestPayloadFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestRequestChannelFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestRequestFireAndForgetFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestRequestNFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestRequestResponseFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestRequestStreamFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestResumeFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestResumeOkFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestSetupFrame())); + createTestCancelFrame(), + createTestErrorFrame(), + createTestKeepaliveFrame(), + createTestLeaseFrame(), + createTestMetadataPushFrame(), + createTestPayloadFrame(), + createTestRequestChannelFrame(), + createTestRequestFireAndForgetFrame(), + createTestRequestNFrame(), + createTestRequestResponseFrame(), + createTestRequestStreamFrame(), + createTestSetupFrame()); when(delegate.receive()).thenReturn(frames); @@ -165,7 +132,7 @@ void receive() { CLIENT, delegate, meterRegistry, Tag.of("test-key", "test-value")) .receive() .as(StepVerifier::create) - .expectNextCount(14) + .expectNextCount(12) .verifyComplete(); assertThat(findCounter(CLIENT, CANCEL).count()).isEqualTo(1); @@ -179,8 +146,6 @@ void receive() { 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, RESUME).count()).isEqualTo(1); - assertThat(findCounter(CLIENT, RESUME_OK).count()).isEqualTo(1); assertThat(findCounter(CLIENT, SETUP).count()).isEqualTo(1); } @@ -188,25 +153,23 @@ void receive() { @SuppressWarnings("unchecked") @Test void send() { - ArgumentCaptor> captor = ArgumentCaptor.forClass(Publisher.class); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Publisher.class); when(delegate.send(captor.capture())).thenReturn(Mono.empty()); - Flux frames = + Flux frames = Flux.just( - toAbstractionLeakingFrame(DEFAULT, 1, createTestCancelFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestErrorFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestKeepaliveFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestLeaseFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestMetadataPushFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestPayloadFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestRequestChannelFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestRequestFireAndForgetFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestRequestNFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestRequestResponseFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestRequestStreamFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestResumeFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestResumeOkFrame()), - toAbstractionLeakingFrame(DEFAULT, 1, createTestSetupFrame())); + createTestCancelFrame(), + createTestErrorFrame(), + createTestKeepaliveFrame(), + createTestLeaseFrame(), + createTestMetadataPushFrame(), + createTestPayloadFrame(), + createTestRequestChannelFrame(), + createTestRequestFireAndForgetFrame(), + createTestRequestNFrame(), + createTestRequestResponseFrame(), + createTestRequestStreamFrame(), + createTestSetupFrame()); new MicrometerDuplexConnection( SERVER, delegate, meterRegistry, Tag.of("test-key", "test-value")) @@ -214,7 +177,7 @@ void send() { .as(StepVerifier::create) .verifyComplete(); - StepVerifier.create(captor.getValue()).expectNextCount(14).verifyComplete(); + StepVerifier.create(captor.getValue()).expectNextCount(12).verifyComplete(); assertThat(findCounter(SERVER, CANCEL).count()).isEqualTo(1); assertThat(findCounter(SERVER, COMPLETE).count()).isEqualTo(1); @@ -227,8 +190,6 @@ void send() { 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, RESUME).count()).isEqualTo(1); - assertThat(findCounter(SERVER, RESUME_OK).count()).isEqualTo(1); assertThat(findCounter(SERVER, SETUP).count()).isEqualTo(1); } diff --git a/rsocket-test/build.gradle b/rsocket-test/build.gradle index 3009b5135..282a65829 100644 --- a/rsocket-test/build.gradle +++ b/rsocket-test/build.gradle @@ -17,8 +17,7 @@ plugins { id 'java-library' id 'maven-publish' - id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' + id 'signing' } dependencies { @@ -26,8 +25,6 @@ dependencies { api 'org.hdrhistogram:HdrHistogram' api 'org.junit.jupiter:junit-jupiter-api' - compileOnly 'com.google.code.findbugs:jsr305' - implementation 'io.projectreactor:reactor-test' implementation 'org.assertj:assertj-core' implementation 'org.mockito:mockito-core' 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 766455705..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,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,17 +48,13 @@ public ClientSetupRule( this.serverInit = address -> - RSocketFactory.receive() - .acceptor((setup, sendingSocket) -> Mono.just(new TestRSocket())) - .transport(serverTransportSupplier.apply(address)) - .start() + 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() + RSocketConnector.connectWith(clientTransportSupplier.apply(address, server)) .doOnError(Throwable::printStackTrace) .block(); } @@ -77,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/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 d1ff04c79..9017e854b 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/PingClient.java +++ b/rsocket-test/src/main/java/io/rsocket/test/PingClient.java @@ -20,7 +20,9 @@ import io.rsocket.RSocket; 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; @@ -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,8 +70,7 @@ public Flux startPingPong(int count, final Recorder histogram) { .flatMap( i -> { long start = System.nanoTime(); - return rsocket - .requestResponse(payload.retain()) + return Flux.from(interaction.apply(rsocket, payload.retain())) .doOnNext(Payload::release) .doFinally( signalType -> { 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 9ef1f394b..47f40a59d 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/PingHandler.java +++ b/rsocket-test/src/main/java/io/rsocket/test/PingHandler.java @@ -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.ByteBufPayload; import java.util.concurrent.ThreadLocalRandom; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class PingHandler implements SocketAcceptor { @@ -41,14 +41,19 @@ public PingHandler(byte[] data) { @Override public Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket) { - setup.release(); return Mono.just( - new AbstractRSocket() { + new RSocket() { @Override public Mono requestResponse(Payload payload) { 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/TestFrames.java b/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java index f59cb0915..1e66abc5e 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java @@ -16,240 +16,93 @@ package io.rsocket.test; -import static io.netty.buffer.Unpooled.EMPTY_BUFFER; -import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; -import static io.rsocket.framing.CancelFrame.createCancelFrame; -import static io.rsocket.framing.ErrorFrame.createErrorFrame; -import static io.rsocket.framing.ExtensionFrame.createExtensionFrame; -import static io.rsocket.framing.FrameLengthFrame.createFrameLengthFrame; -import static io.rsocket.framing.KeepaliveFrame.createKeepaliveFrame; -import static io.rsocket.framing.LeaseFrame.createLeaseFrame; -import static io.rsocket.framing.MetadataPushFrame.createMetadataPushFrame; -import static io.rsocket.framing.PayloadFrame.createPayloadFrame; -import static io.rsocket.framing.RequestChannelFrame.createRequestChannelFrame; -import static io.rsocket.framing.RequestFireAndForgetFrame.createRequestFireAndForgetFrame; -import static io.rsocket.framing.RequestNFrame.createRequestNFrame; -import static io.rsocket.framing.RequestResponseFrame.createRequestResponseFrame; -import static io.rsocket.framing.RequestStreamFrame.createRequestStreamFrame; -import static io.rsocket.framing.ResumeFrame.createResumeFrame; -import static io.rsocket.framing.ResumeOkFrame.createResumeOkFrame; -import static io.rsocket.framing.SetupFrame.createSetupFrame; -import static io.rsocket.framing.StreamIdFrame.createStreamIdFrame; - import io.netty.buffer.ByteBuf; -import io.rsocket.framing.CancelFrame; -import io.rsocket.framing.ErrorFrame; -import io.rsocket.framing.ExtensionFrame; -import io.rsocket.framing.Frame; -import io.rsocket.framing.FrameLengthFrame; -import io.rsocket.framing.FrameType; -import io.rsocket.framing.KeepaliveFrame; -import io.rsocket.framing.LeaseFrame; -import io.rsocket.framing.MetadataPushFrame; -import io.rsocket.framing.PayloadFrame; -import io.rsocket.framing.RequestChannelFrame; -import io.rsocket.framing.RequestFireAndForgetFrame; -import io.rsocket.framing.RequestNFrame; -import io.rsocket.framing.RequestResponseFrame; -import io.rsocket.framing.RequestStreamFrame; -import io.rsocket.framing.ResumeFrame; -import io.rsocket.framing.ResumeOkFrame; -import io.rsocket.framing.SetupFrame; -import io.rsocket.framing.StreamIdFrame; -import java.time.Duration; +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() {} - /** - * Returns a test instance of {@link CancelFrame}. - * - * @return a test instance of {@link CancelFrame} - */ - public static CancelFrame createTestCancelFrame() { - return createCancelFrame(DEFAULT); - } - - /** - * Returns a test instance of {@link ErrorFrame}. - * - * @return a test instance of {@link ErrorFrame} - */ - public static ErrorFrame createTestErrorFrame() { - return createErrorFrame(DEFAULT, 1, (ByteBuf) null); - } - - /** - * Returns a test instance of {@link ExtensionFrame}. - * - * @return a test instance of {@link ExtensionFrame} - */ - public static ExtensionFrame createTestExtensionFrame() { - return createExtensionFrame(DEFAULT, true, 1, (ByteBuf) null, null); + /** @return {@link ByteBuf} representing test instance of Cancel frame */ + public static ByteBuf createTestCancelFrame() { + return CancelFrameCodec.encode(allocator, 1); } - /** - * Returns a custom test {@link Frame}. - * - * @param frameType the type of frame - * @param byteBuf the {@link ByteBuf} of content for this frame - * @return a custom test {@link Frame} - */ - public static Frame createTestFrame(FrameType frameType, ByteBuf byteBuf) { - return new TestFrame(frameType, byteBuf); + /** @return {@link ByteBuf} representing test instance of Error frame */ + public static ByteBuf createTestErrorFrame() { + return ErrorFrameCodec.encode(allocator, 1, new RuntimeException()); } - /** - * Returns a test instance of {@link FrameLengthFrame}. - * - * @return a test instance of {@link FrameLengthFrame} - */ - public static FrameLengthFrame createTestFrameLengthFrame() { - return createFrameLengthFrame(DEFAULT, createTestStreamIdFrame()); + /** @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); } - /** - * Returns a test instance of {@link KeepaliveFrame}. - * - * @return a test instance of {@link KeepaliveFrame} - */ - public static KeepaliveFrame createTestKeepaliveFrame() { - return createKeepaliveFrame(DEFAULT, false, 1, null); + /** @return {@link ByteBuf} representing test instance of Keep-Alive frame */ + public static ByteBuf createTestKeepaliveFrame() { + return KeepAliveFrameCodec.encode(allocator, false, 1, Unpooled.EMPTY_BUFFER); } - /** - * Returns a test instance of {@link LeaseFrame}. - * - * @return a test instance of {@link LeaseFrame} - */ - public static LeaseFrame createTestLeaseFrame() { - return createLeaseFrame(DEFAULT, Duration.ofMillis(1), 1, null); + /** @return {@link ByteBuf} representing test instance of Lease frame */ + public static ByteBuf createTestLeaseFrame() { + return LeaseFrameCodec.encode(allocator, 1, 1, null); } - /** - * Returns a test instance of {@link MetadataPushFrame}. - * - * @return a test instance of {@link MetadataPushFrame} - */ - public static MetadataPushFrame createTestMetadataPushFrame() { - return createMetadataPushFrame(DEFAULT, EMPTY_BUFFER); + /** @return {@link ByteBuf} representing test instance of Metadata-Push frame */ + public static ByteBuf createTestMetadataPushFrame() { + return MetadataPushFrameCodec.encode(allocator, Unpooled.EMPTY_BUFFER); } - /** - * Returns a test instance of {@link PayloadFrame}. - * - * @return a test instance of {@link PayloadFrame} - */ - public static PayloadFrame createTestPayloadFrame() { - return createPayloadFrame(DEFAULT, false, true, (ByteBuf) null, null); + /** @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); } - /** - * Returns a test instance of {@link RequestChannelFrame}. - * - * @return a test instance of {@link RequestChannelFrame} - */ - public static RequestChannelFrame createTestRequestChannelFrame() { - return createRequestChannelFrame(DEFAULT, false, false, 1, (ByteBuf) null, null); + /** @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); } - /** - * Returns a test instance of {@link RequestFireAndForgetFrame}. - * - * @return a test instance of {@link RequestFireAndForgetFrame} - */ - public static RequestFireAndForgetFrame createTestRequestFireAndForgetFrame() { - return createRequestFireAndForgetFrame(DEFAULT, false, (ByteBuf) null, null); + /** @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); } - /** - * Returns a test instance of {@link RequestNFrame}. - * - * @return a test instance of {@link RequestNFrame} - */ - public static RequestNFrame createTestRequestNFrame() { - return createRequestNFrame(DEFAULT, 1); + /** @return {@link ByteBuf} representing test instance of Request-N frame */ + public static ByteBuf createTestRequestNFrame() { + return RequestNFrameCodec.encode(allocator, 1, 1); } - /** - * Returns a test instance of {@link RequestResponseFrame}. - * - * @return a test instance of {@link RequestResponseFrame} - */ - public static RequestResponseFrame createTestRequestResponseFrame() { - return createRequestResponseFrame(DEFAULT, false, (ByteBuf) null, null); + /** @return {@link ByteBuf} representing test instance of Request-Response frame */ + public static ByteBuf createTestRequestResponseFrame() { + return RequestResponseFrameCodec.encodeReleasingPayload(allocator, 1, emptyPayload); } - /** - * Returns a test instance of {@link RequestStreamFrame}. - * - * @return a test instance of {@link RequestStreamFrame} - */ - public static RequestStreamFrame createTestRequestStreamFrame() { - return createRequestStreamFrame(DEFAULT, false, 1, (ByteBuf) null, null); + /** @return {@link ByteBuf} representing test instance of Request-Stream frame */ + public static ByteBuf createTestRequestStreamFrame() { + return RequestStreamFrameCodec.encodeReleasingPayload(allocator, 1, 1L, emptyPayload); } - /** - * Returns a test instance of {@link ResumeFrame}. - * - * @return a test instance of {@link ResumeFrame} - */ - public static ResumeFrame createTestResumeFrame() { - return createResumeFrame(DEFAULT, 1, 0, EMPTY_BUFFER, 1, 1); - } - - /** - * Returns a test instance of {@link ResumeOkFrame}. - * - * @return a test instance of {@link ResumeOkFrame} - */ - public static ResumeOkFrame createTestResumeOkFrame() { - return createResumeOkFrame(DEFAULT, 1); - } - - /** - * Returns a test instance of {@link SetupFrame}. - * - * @return a test instance of {@link SetupFrame} - */ - public static SetupFrame createTestSetupFrame() { - return createSetupFrame( - DEFAULT, true, 1, 1, Duration.ofMillis(1), Duration.ofMillis(1), null, "", "", null, null); - } - - /** - * Returns a test instance of {@link StreamIdFrame}. - * - * @return a test instance of {@link StreamIdFrame} - */ - public static StreamIdFrame createTestStreamIdFrame() { - return createStreamIdFrame(DEFAULT, 1, createTestCancelFrame()); - } - - private static final class TestFrame implements Frame { - - private final ByteBuf byteBuf; - - private final FrameType frameType; - - private TestFrame(FrameType frameType, ByteBuf byteBuf) { - this.frameType = frameType; - this.byteBuf = byteBuf; - } - - @Override - public void dispose() {} - - @Override - public FrameType getFrameType() { - return frameType; - } - - @Override - public ByteBuf getUnsafeFrame() { - return byteBuf.asReadOnly(); - } + /** @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 ccd6de168..d48700445 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java @@ -16,18 +16,25 @@ package io.rsocket.test; -import io.rsocket.AbstractRSocket; import io.rsocket.Payload; +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(DefaultPayload.create("hello world", "metadata")); + return Mono.just(DefaultPayload.create(data, metadata)); } @Override @@ -48,6 +55,6 @@ public Mono fireAndForget(Payload payload) { @Override public Flux requestChannel(Publisher payloads) { // TODO is defensive copy neccesary? - return Flux.from(payloads).map(DefaultPayload::create); + return Flux.from(payloads).map(Payload::retain); } } diff --git a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java index 29b97d4c6..fc059c7d1 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,27 +19,62 @@ 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.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) { @@ -53,12 +88,12 @@ default Payload createTestPayload(int metadataPresent) { metadata1 = ""; break; default: - metadata1 = "metadata"; + metadata1 = MOCK_METADATA; break; } String metadata = metadata1; - return DefaultPayload.create("test-data", metadata); + return DefaultPayload.create(MOCK_DATA, metadata); } @DisplayName("makes 10 fireAndForget requests") @@ -72,6 +107,17 @@ default void fireAndForget10() { .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(); } @@ -91,6 +137,17 @@ default void metadataPush10() { .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() { @@ -126,13 +183,27 @@ default void requestChannel200_000() { .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(this::createTestPayload); + Flux payloads = Flux.range(0, 20_000).map(metadataPresent -> createTestPayload(7)); getClient() .requestChannel(payloads) + .doOnNext(this::assertChannelPayload) .as(StepVerifier::create) .expectNextCount(20_000) .expectComplete() @@ -155,14 +226,18 @@ default void requestChannel2_000_000() { @DisplayName("makes 1 requestChannel request with 3 payloads") @Test default void requestChannel3() { - Flux payloads = Flux.range(0, 3).map(this::createTestPayload); + AtomicLong requested = new AtomicLong(); + Flux payloads = + Flux.range(0, 3).doOnRequest(requested::addAndGet).map(this::createTestPayload); getClient() .requestChannel(payloads) - .as(StepVerifier::create) + .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") @@ -170,10 +245,18 @@ default void requestChannel3() { 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()); } @@ -183,7 +266,7 @@ default void requestChannel512() { default void requestResponse1() { getClient() .requestResponse(createTestPayload(1)) - .map(Payload::getDataUtf8) + .doOnNext(this::assertPayload) .as(StepVerifier::create) .expectNextCount(1) .expectComplete() @@ -194,7 +277,8 @@ default void requestResponse1() { @Test default void requestResponse10() { Flux.range(1, 10) - .flatMap(i -> getClient().requestResponse(createTestPayload(i)).map(Payload::getDataUtf8)) + .flatMap( + i -> getClient().requestResponse(createTestPayload(i)).doOnNext(v -> assertPayload(v))) .as(StepVerifier::create) .expectNextCount(10) .expectComplete() @@ -212,6 +296,17 @@ default void requestResponse100() { .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() { @@ -228,6 +323,7 @@ default void requestResponse10_000() { default void requestStream10_000() { getClient() .requestStream(createTestPayload(3)) + .doOnNext(this::assertPayload) .as(StepVerifier::create) .expectNextCount(10_000) .expectComplete() @@ -239,6 +335,7 @@ default void requestStream10_000() { default void requestStream5() { getClient() .requestStream(createTestPayload(3)) + .doOnNext(this::assertPayload) .take(5) .as(StepVerifier::create) .expectNextCount(5) @@ -261,7 +358,23 @@ default void requestStreamDelayedRequestN() { .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; @@ -275,16 +388,12 @@ public TransportPair( T address = addressSupplier.get(); server = - RSocketFactory.receive() - .acceptor((setup, sendingSocket) -> Mono.just(new TestRSocket())) - .transport(serverTransportSupplier.apply(address)) - .start() + RSocketServer.create((setup, sendingSocket) -> Mono.just(new TestRSocket(data, metadata))) + .bind(serverTransportSupplier.apply(address)) .block(); client = - RSocketFactory.connect() - .transport(clientTransportSupplier.apply(address, server)) - .start() + RSocketConnector.connectWith(clientTransportSupplier.apply(address, server)) .doOnError(Throwable::printStackTrace) .block(); } @@ -297,5 +406,13 @@ public void dispose() { RSocket getClient() { return client; } + + public String expectedPayloadData() { + return data; + } + + public String expectedPayloadMetadata() { + return metadata; + } } } diff --git a/rsocket-test/src/main/java/io/rsocket/test/UriHandlerTest.java b/rsocket-test/src/main/java/io/rsocket/test/UriHandlerTest.java deleted file mode 100644 index ad45e106a..000000000 --- a/rsocket-test/src/main/java/io/rsocket/test/UriHandlerTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -import io.rsocket.uri.UriHandler; -import java.net.URI; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -public interface UriHandlerTest { - - @DisplayName("returns empty Optional client with invalid URI") - @Test - default void buildClientInvalidUri() { - assertThat(getUriHandler().buildClient(URI.create(getInvalidUri()))).isEmpty(); - } - - @DisplayName("buildClient throws NullPointerException with null uri") - @Test - default void buildClientNullUri() { - assertThatNullPointerException() - .isThrownBy(() -> getUriHandler().buildClient(null)) - .withMessage("uri must not be null"); - } - - @DisplayName("returns client with value URI") - @Test - default void buildClientValidUri() { - assertThat(getUriHandler().buildClient(URI.create(getValidUri()))).isNotEmpty(); - } - - @DisplayName("returns empty Optional server with invalid URI") - @Test - default void buildServerInvalidUri() { - assertThat(getUriHandler().buildServer(URI.create(getInvalidUri()))).isEmpty(); - } - - @DisplayName("buildServer throws NullPointerException with null uri") - @Test - default void buildServerNullUri() { - assertThatNullPointerException() - .isThrownBy(() -> getUriHandler().buildServer(null)) - .withMessage("uri must not be null"); - } - - @DisplayName("returns server with value URI") - @Test - default void buildServerValidUri() { - assertThat(getUriHandler().buildServer(URI.create(getValidUri()))).isNotEmpty(); - } - - String getInvalidUri(); - - UriHandler getUriHandler(); - - String getValidUri(); -} 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 644f8e6ab..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/AeronDuplexConnection.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 onClose; - - public AeronDuplexConnection(String name, AeronChannel channel) { - this.name = name; - this.channel = channel; - this.onClose = 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::printStackTrace); - } - - @Override - public void dispose() { - try { - channel.dispose(); - onClose.onComplete(); - } catch (Exception e) { - onClose.onError(e); - } - } - - @Override - public boolean isDisposed() { - return channel.isDisposed(); - } - - @Override - public Mono onClose() { - return onClose; - } - - @Override - public String toString() { - return "AeronDuplexConnection{" - + "name='" - + name - + '\'' - + ", channel=" - + channel - + ", onClose=" - + onClose - + '}'; - } -} 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 970887f98..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/client/AeronClientTransport.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 9434b322c..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/AeronWrapper.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 streamId) { - return getAeron().addSubscription(channel, streamId); - } - - default Publication addPublication(String channel, int streamId) { - return getAeron().addPublication(channel, streamId); - } - - 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 70ca1ebd4..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/Constants.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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 a6d3b7a6b..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/DefaultAeronWrapper.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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) { - unavailableImageHandlers.removeIf(handler -> handler.apply(image)); - } -} 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 df5f64718..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/EventLoop.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 signal for roughly how many items could be processed. - * @return whether items could be processed - */ - 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 24a02cb44..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/SingleThreadedEventLoop.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 5275f0c92..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronChannel.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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.Disposable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** */ -public class AeronChannel implements ReactiveStreamsRemote.Channel, Disposable { - 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 the publisher of buffers. - * @return Mono the completes when all publishers have been sent. - */ - 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 dispose() { - destination.close(); - source.close(); - } - - @Override - public boolean isDisposed() { - return destination.isClosed() && source.isClosed(); - } - - @Override - public String toString() { - return "AeronChannel{" + "name='" + name + '\'' + '}'; - } -} 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 3afe64a13..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelServer.java +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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.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 clientSessionId {}"); - - 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(); - - 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)); - onClose().block(d); - } - - @Override - public void awaitShutdown() { - onClose().block(); - } - - @Override - public void shutdown() { - dispose(); - } - - @Override - public void dispose() { - running = false; - managementSubscription.close(); - onClose.onComplete(); - } - - @Override - public boolean isDisposed() { - return onClose.isDisposed(); - } - - @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 7272ecbfc..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronClientChannelConnector.java +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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.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 Mono.from( - 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(aeronClientConfig.receiveSocketAddress); - 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(aeronClientConfig.receiveSocketAddress); - 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 - * @param receiveStreamId receiving stream id - * @param sendStreamId the sending stream id - * @param eventLoop event loop for this client - * @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() { - 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 78cf93c95..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronInSubscriber.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 7732c3160..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronOutPublisher.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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.CoreSubscriber; -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(CoreSubscriber 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 ca8926add..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/AeronSocketAddress.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 static final long serialVersionUID = -7691068719112973697L; - 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 f2a8666b9..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/ReactiveStreamsRemote.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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(); - } - - 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. - * - * @param duration the number of durationUnit to wait - * @param durationUnit the unit e.g. seconds - */ - 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 626c1f6d8..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/AckConnectDecoder.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* 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 05fef8487..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/AckConnectEncoder.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* 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 cd294a48b..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/ConnectDecoder.java +++ /dev/null @@ -1,549 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* 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 140a5a293..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/ConnectEncoder.java +++ /dev/null @@ -1,393 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 2c3e6f464..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/MessageHeaderDecoder.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* 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 8bd1b58f6..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/MessageHeaderEncoder.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* 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/VarDataEncodingDecoder.java b/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/VarDataEncodingDecoder.java deleted file mode 100644 index cea04b1e7..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/VarDataEncodingDecoder.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* 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 ab23e24a1..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/internal/reactivestreams/messages/VarDataEncodingEncoder.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* 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 1a0429c7d..000000000 --- a/rsocket-transport-aeron/src/main/java/io/rsocket/aeron/server/AeronServerTransport.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 84db6fd4a..000000000 --- a/rsocket-transport-aeron/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2015-2018 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -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 cbd64809f..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 4a7cdc6ae..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/AeronClientSetupRule.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 15db974c7..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/AeronPing.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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) { - // 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 28b9080c4..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/AeronPongServer.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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) - .dirDeleteOnStart(true); - MediaDriver.launch(ctx); - } - - public static void main(String... args) { - 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 c34b6d4df..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/ClientServerTest.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.aeron; - -import static org.junit.Assert.assertEquals; - -import io.rsocket.Payload; -import io.rsocket.test.ClientSetupRule; -import io.rsocket.util.DefaultPayload; -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(DefaultPayload.create("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(DefaultPayload.create("", "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(DefaultPayload.create("hello", "metadata")) - .map(Payload::getDataUtf8)) - .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(DefaultPayload.create("hello", "metadata")) - .map(Payload::getDataUtf8)) - .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(DefaultPayload.create("hello", "metadata")) - .map(Payload::getDataUtf8)) - .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(DefaultPayload.create("hello", "metadata")) - .map(Payload::getDataUtf8)) - .doOnError(Throwable::printStackTrace) - .count() - .block(); - - assertEquals(10_000, outputCount); - } - - @Test(timeout = 10000) - public void testRequestStream() { - Flux publisher = - setup.getRSocket().requestStream(DefaultPayload.create("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 d43768c28..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/MediaDriverHolder.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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) - .dirDeleteOnStart(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 af62c46c7..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelPing.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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) { - 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::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 aa0296c45..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelPongServer.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 = - aeronChannel -> { - Flux receive = aeronChannel.receive(); - // .doOnNext(b -> System.out.println("server got => " + b.getInt(0))); - - aeronChannel.send(receive).doOnError(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 2a5339af2..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronChannelTest.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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() - // latch.countDown(); - // System.out.println("received -> " + f.getInt(0)); - .flatMap(serverChannel::send, 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 1874281d5..000000000 --- a/rsocket-transport-aeron/src/test/java/io/rsocket/aeron/internal/reactivestreams/AeronClientServerChannelTest.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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 8c3226065..8c855f26c 100644 --- a/rsocket-transport-local/build.gradle +++ b/rsocket-transport-local/build.gradle @@ -17,15 +17,12 @@ plugins { id 'java-library' id 'maven-publish' - id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' + id 'signing' } dependencies { api project(':rsocket-core') - compileOnly 'com.google.code.findbugs:jsr305' - testImplementation project(':rsocket-test') testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' 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 a6cb7e6b5..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,15 @@ 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.ServerTransport; -import io.rsocket.transport.local.LocalServerTransport.ServerDuplexConnectionAcceptor; import java.util.Objects; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; -import reactor.core.publisher.UnicastProcessor; /** * An implementation of {@link ClientTransport} that connects to a {@link ServerTransport} in the @@ -34,39 +34,58 @@ 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 ServerTransport} instance to connect to + * @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) { Objects.requireNonNull(name, "name must not be null"); - return new LocalClientTransport(name); + 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); + ServerTransport.ConnectionAcceptor server = LocalServerTransport.findServer(name); if (server == null) { return Mono.error(new IllegalArgumentException("Could not find server: " + name)); } - UnicastProcessor in = UnicastProcessor.create(); - UnicastProcessor out = UnicastProcessor.create(); + UnboundedProcessor in = new UnboundedProcessor<>(); + UnboundedProcessor out = new UnboundedProcessor<>(); MonoProcessor closeNotifier = MonoProcessor.create(); - server.accept(new LocalDuplexConnection(out, in, closeNotifier)); + server.apply(new LocalDuplexConnection(allocator, out, in, closeNotifier)).subscribe(); - return Mono.just((DuplexConnection) new LocalDuplexConnection(in, out, closeNotifier)); + 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 84a542714..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 @@ -16,8 +16,9 @@ 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; @@ -28,21 +29,27 @@ /** An implementation of {@link DuplexConnection} that connects inside the same JVM. */ final class LocalDuplexConnection implements DuplexConnection { - private final Flux in; + private final ByteBufAllocator allocator; + private final Flux in; private final MonoProcessor onClose; - private final Subscriber out; + private final Subscriber out; /** * Creates a new instance. * - * @param in the inbound {@link Frame}s - * @param out the outbound {@link Frame}s + * @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(Flux in, Subscriber out, MonoProcessor onClose) { + 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"); @@ -65,14 +72,26 @@ public Mono onClose() { } @Override - public Flux receive() { + public Flux receive() { return in; } @Override - public Mono send(Publisher frames) { + public Mono send(Publisher frames) { Objects.requireNonNull(frames, "frames must not be null"); return Flux.from(frames).doOnNext(out::onNext).then(); } + + @Override + public Mono sendOne(ByteBuf frame) { + Objects.requireNonNull(frame, "frame must not be null"); + out.onNext(frame); + return Mono.empty(); + } + + @Override + 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 68e7d462f..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,12 @@ 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; @@ -35,7 +33,7 @@ */ public final class LocalServerTransport implements ServerTransport { - private static final ConcurrentMap registry = + private static final ConcurrentMap registry = new ConcurrentHashMap<>(); private final String name; @@ -77,94 +75,64 @@ public static void dispose(String name) { } /** - * Returns a new {@link LocalClientTransport} that is connected to this {@code - * LocalServerTransport}. - * - * @return a new {@link LocalClientTransport} that is connected to this {@code - * LocalServerTransport} - */ - 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) { - throw new IllegalStateException("name already registered: " + name); - } - - sink.success(serverDuplexConnectionAcceptor); - }); - } - - /** - * Retrieves an instance of {@link ServerDuplexConnectionAcceptor} based on the name of its {@code + * 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 ServerDuplexConnectionAcceptor findServer(String name) { + static @Nullable ConnectionAcceptor findServer(String name) { Objects.requireNonNull(name, "name must not be null"); return registry.get(name); } - /** - * Returns the name of this instance. - * - * @return the name of this instance - */ + /** Return the name associated with this local server instance. */ String getName() { return name; } /** - * A {@link Consumer} of {@link DuplexConnection} that is called when a server has been created. + * Return a new {@link LocalClientTransport} connected to this {@code LocalServerTransport} + * through its {@link #getName()}. */ - static class ServerDuplexConnectionAcceptor implements Consumer, Closeable { + public LocalClientTransport clientTransport() { + return LocalClientTransport.create(name); + } - private final ConnectionAcceptor acceptor; + @Override + public Mono start(ConnectionAcceptor acceptor) { + Objects.requireNonNull(acceptor, "acceptor must not be null"); + return Mono.create( + sink -> { + ServerCloseable closeable = new ServerCloseable(name, acceptor); + if (registry.putIfAbsent(name, acceptor) != null) { + throw new IllegalStateException("name already registered: " + name); + } + sink.success(closeable); + }); + } + + static class ServerCloseable implements Closeable { private final LocalSocketAddress address; + private final ConnectionAcceptor acceptor; + private final MonoProcessor onClose = MonoProcessor.create(); - /** - * Creates a new instance - * - * @param name the name of the server - * @param acceptor the {@link ConnectionAcceptor} to call when the server has been created - * @throws NullPointerException if {@code name} or {@code acceptor} is {@code null} - */ - ServerDuplexConnectionAcceptor(String name, ConnectionAcceptor acceptor) { + ServerCloseable(String name, ConnectionAcceptor acceptor) { Objects.requireNonNull(name, "name must not be null"); - this.address = new LocalSocketAddress(name); - this.acceptor = Objects.requireNonNull(acceptor, "acceptor must not be null"); - } - - @Override - public void accept(DuplexConnection duplexConnection) { - Objects.requireNonNull(duplexConnection, "duplexConnection must not be null"); - - acceptor.apply(duplexConnection).subscribe(); + this.acceptor = acceptor; } @Override public void dispose() { - if (!registry.remove(address.getName(), this)) { + if (!registry.remove(address.getName(), acceptor)) { throw new AssertionError(); } - onClose.onComplete(); } 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 89c816d7a..000000000 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalUriHandler.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.local; - -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import io.rsocket.uri.UriHandler; -import java.net.URI; -import java.util.Objects; -import java.util.Optional; - -/** - * An implementation of {@link UriHandler} that creates {@link LocalClientTransport}s and {@link - * LocalServerTransport}s. - */ -public final class LocalUriHandler implements UriHandler { - - private static final String SCHEME = "local"; - - @Override - public Optional buildClient(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - if (!SCHEME.equals(uri.getScheme())) { - return Optional.empty(); - } - - return Optional.of(LocalClientTransport.create(uri.getSchemeSpecificPart())); - } - - @Override - public Optional buildServer(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - if (!SCHEME.equals(uri.getScheme())) { - return Optional.empty(); - } - - return Optional.of(LocalServerTransport.create(uri.getSchemeSpecificPart())); - } -} 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 6ff8ffb50..000000000 --- a/rsocket-transport-local/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2015-2018 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -io.rsocket.transport.local.LocalUriHandler diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientTransportTest.java index 92478b0bd..ac4c13efe 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientTransportTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. 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 eb91318f6..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,9 @@ 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; @@ -27,16 +29,15 @@ public final class LocalPingPong { public static void main(String... args) { - RSocketFactory.receive() - .acceptor(new PingHandler()) - .transport(LocalServerTransport.create("test-local-server")) - .start() + RSocketServer.create(new PingHandler()) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(LocalServerTransport.create("test-local-server")) .block(); Mono client = - RSocketFactory.connect() - .transport(LocalClientTransport.create("test-local-server")) - .start(); + RSocketConnector.create() + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(LocalClientTransport.create("test-local-server")); PingClient pingClient = new PingClient(client); @@ -45,7 +46,7 @@ public static void main(String... args) { int count = 1_000_000_000; pingClient - .startPingPong(count, recorder) + .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 index 7fb350432..ed906f65b 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalServerTransportTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalServerTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportTest.java index a6656c4d7..7184dd645 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportTest.java @@ -16,27 +16,31 @@ package io.rsocket.transport.local; -import io.rsocket.test.TransportTest; -import java.time.Duration; -import java.util.concurrent.atomic.AtomicInteger; +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(); -final class LocalTransportTest implements TransportTest { + private final TransportPair transportPair = + new TransportPair<>( + () -> "test" + UNIQUE_NAME_GENERATOR.incrementAndGet(), + (address, server) -> LocalClientTransport.create(address), + LocalServerTransport::create); - private static final AtomicInteger UNIQUE_NAME_GENERATOR = new AtomicInteger(); + @Override + @Test + public void requestChannel512() { - private final TransportPair transportPair = - new TransportPair<>( - () -> "test" + UNIQUE_NAME_GENERATOR.incrementAndGet(), - (address, server) -> LocalClientTransport.create(address), - LocalServerTransport::create); + } - @Override - public Duration getTimeout() { - return Duration.ofSeconds(10); - } + @Override + public Duration getTimeout() { + return Duration.ofSeconds(10); + } - @Override - public TransportPair getTransportPair() { - return transportPair; - } + @Override + public TransportPair getTransportPair() { + return transportPair; + }*/ } diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriHandlerTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriHandlerTest.java deleted file mode 100644 index ed8e6cd1d..000000000 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriHandlerTest.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.local; - -import io.rsocket.test.UriHandlerTest; -import io.rsocket.uri.UriHandler; - -final class LocalUriHandlerTest implements UriHandlerTest { - - @Override - public String getInvalidUri() { - return "http://test"; - } - - @Override - public UriHandler getUriHandler() { - return new LocalUriHandler(); - } - - @Override - public String getValidUri() { - return "local:test"; - } -} 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 f6b5cda7e..000000000 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriTransportRegistryTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.local; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.rsocket.uri.UriTransportRegistry; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -final class LocalUriTransportRegistryTest { - - @DisplayName("local URI returns LocalClientTransport") - @Test - void clientForUri() { - assertThat(UriTransportRegistry.clientForUri("local:test1")) - .isInstanceOf(LocalClientTransport.class); - } - - @DisplayName("non-local URI does not return LocalClientTransport") - @Test - void clientForUriInvalid() { - assertThat(UriTransportRegistry.clientForUri("http://localhost")) - .isNotInstanceOf(LocalClientTransport.class); - } - - @DisplayName("local URI returns LocalServerTransport") - @Test - void serverForUri() { - assertThat(UriTransportRegistry.serverForUri("local:test1")) - .isInstanceOf(LocalServerTransport.class); - } - - @DisplayName("non-local URI does not return LocalServerTransport") - @Test - void serverForUriInvalid() { - assertThat(UriTransportRegistry.serverForUri("http://localhost")) - .isNotInstanceOf(LocalServerTransport.class); - } -} diff --git a/rsocket-transport-local/src/test/resources/logback-test.xml b/rsocket-transport-local/src/test/resources/logback-test.xml index 5cceb90a9..01a7fa4cd 100644 --- a/rsocket-transport-local/src/test/resources/logback-test.xml +++ b/rsocket-transport-local/src/test/resources/logback-test.xml @@ -25,7 +25,7 @@ - + diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 71d0d5088..201d56bbf 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -17,31 +17,31 @@ plugins { id 'java-library' id 'maven-publish' - id 'com.jfrog.artifactory' - id 'com.jfrog.bintray' + id 'signing' id "com.google.osdetector" version "1.4.0" } def os_suffix = "" if (osdetector.classifier in ["linux-x86_64"] || ["osx-x86_64"] || ["windows-x86_64"]) { - os_suffix = ":" + osdetector.classifier + os_suffix = "::" + osdetector.classifier } dependencies { api project(':rsocket-core') api 'io.projectreactor.netty:reactor-netty' - - compileOnly 'com.google.code.findbugs:jsr305' + 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' testRuntimeOnly 'ch.qos.logback:logback-classic' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' - testRuntimeOnly 'io.netty:netty-tcnative-boringssl-static:2.0.14.Final' + os_suffix + 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/RSocketLengthCodec.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/RSocketLengthCodec.java index f4faf9e2c..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 @@ -16,8 +16,8 @@ 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; @@ -31,7 +31,16 @@ 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); } /** diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java index 6ac2dbe0a..618708bf0 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java @@ -1,11 +1,11 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * 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,56 +16,92 @@ package io.rsocket.transport.netty; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; -import io.rsocket.Frame; +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; -import java.util.Objects; - /** An implementation of {@link DuplexConnection} that connects via TCP. */ -public final class TcpDuplexConnection implements DuplexConnection { +public final class TcpDuplexConnection extends BaseDuplexConnection { private final Connection connection; + private final boolean encodeLength; /** * Creates a new instance * - * @param connection the {@link Connection} to for managing the server + * @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 void dispose() { - connection.dispose(); + public ByteBufAllocator alloc() { + return connection.channel().alloc(); } @Override - public boolean isDisposed() { - return connection.isDisposed(); + protected void doOnClose() { + if (!connection.isDisposed()) { + connection.dispose(); + } } @Override - public Mono onClose() { - return connection.onDispose(); + public Flux receive() { + return connection.inbound().receive().map(this::decode); } @Override - public Flux receive() { - return connection.inbound().receive().map(buf -> Frame.from(buf.retain())); + 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(); } - @Override - public Mono send(Publisher frames) { - return Flux.from(frames).concatMap(this::sendOne).then(); + private ByteBuf encode(ByteBuf frame) { + if (encodeLength) { + return FrameLengthCodec.encode(alloc(), frame.readableBytes(), frame); + } else { + return frame; + } } - @Override - public Mono sendOne(Frame frame) { - return connection.outbound().sendObject(frame.content()).then(); + 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 952a1e398..000000000 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpUriHandler.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.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.Objects; -import java.util.Optional; -import reactor.netty.tcp.TcpServer; - -/** - * An implementation of {@link UriHandler} that creates {@link TcpClientTransport}s and {@link - * TcpServerTransport}s. - */ -public final class TcpUriHandler implements UriHandler { - - private static final String SCHEME = "tcp"; - - @Override - public Optional buildClient(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - if (!SCHEME.equals(uri.getScheme())) { - return Optional.empty(); - } - - return Optional.of(TcpClientTransport.create(uri.getHost(), uri.getPort())); - } - - @Override - public Optional buildServer(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - if (!SCHEME.equals(uri.getScheme())) { - return Optional.empty(); - } - - return Optional.of(TcpServerTransport.create( - TcpServer.create() - .host(uri.getHost()) - .port(uri.getPort()))); - } -} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/UriUtils.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/UriUtils.java deleted file mode 100644 index 5134ba8d6..000000000 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/UriUtils.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.netty; - -import java.net.URI; -import java.util.Objects; - -/** Utilities for dealing with with {@link URI}s */ -public final class UriUtils { - - private UriUtils() {} - - /** - * Returns the port of a URI. If the port is unset (i.e. {@code -1}) then returns the {@code - * defaultPort}. - * - * @param uri the URI to extract the port from - * @param defaultPort the default to use if the port is unset - * @return the port of a URI or {@code defaultPort} if unset - * @throws NullPointerException if {@code uri} is {@code null} - */ - public static int getPort(URI uri, int defaultPort) { - Objects.requireNonNull(uri, "uri must not be null"); - return uri.getPort() == -1 ? defaultPort : uri.getPort(); - } - - /** - * Returns whether the URI has a secure schema. Secure is defined as being either {@code wss} or - * {@code https}. - * - * @param uri the URI to examine - * @return whether the URI has a secure schema - * @throws NullPointerException if {@code uri} is {@code null} - */ - public static boolean isSecure(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - return uri.getScheme().equals("wss") || uri.getScheme().equals("https"); - } -} 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 efb47c2a5..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 @@ -5,7 +5,7 @@ * you may not use this 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,15 +15,11 @@ */ package io.rsocket.transport.netty; -import static io.netty.buffer.Unpooled.wrappedBuffer; -import static io.rsocket.frame.FrameHeaderFlyweight.FRAME_LENGTH_SIZE; - 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; @@ -33,11 +29,11 @@ /** * 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 final class WebsocketDuplexConnection implements DuplexConnection { +public final class WebsocketDuplexConnection extends BaseDuplexConnection { private final Connection connection; @@ -48,44 +44,44 @@ public final class WebsocketDuplexConnection implements DuplexConnection { */ public WebsocketDuplexConnection(Connection connection) { this.connection = Objects.requireNonNull(connection, "connection must not be null"); - } - - @Override - public void dispose() { - connection.dispose(); - } - @Override - public boolean isDisposed() { - return connection.isDisposed(); + connection + .channel() + .closeFuture() + .addListener( + future -> { + if (!isDisposed()) dispose(); + }); } @Override - public Mono onClose() { - return connection.onDispose(); + public ByteBufAllocator alloc() { + return connection.channel().alloc(); } @Override - public Flux receive() { - return connection.inbound().receive() - .map( - buf -> { - CompositeByteBuf composite = connection.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); - }); + protected void doOnClose() { + if (!connection.isDisposed()) { + connection.dispose(); + } } @Override - public Mono send(Publisher frames) { - return Flux.from(frames).concatMap(this::sendOne).then(); + public Flux receive() { + return connection.inbound().receive().map(ByteBuf::retain); } @Override - public Mono sendOne(Frame frame) { - return connection.outbound().sendObject(new BinaryWebSocketFrame(frame.content().skipBytes(FRAME_LENGTH_SIZE))) + 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 6438c4e28..000000000 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketUriHandler.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.netty; - -import static io.rsocket.transport.netty.UriUtils.getPort; -import static io.rsocket.transport.netty.UriUtils.isSecure; - -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.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -/** - * An implementation of {@link UriHandler} that creates {@link WebsocketClientTransport}s and {@link - * WebsocketServerTransport}s. - */ -public final class WebsocketUriHandler implements UriHandler { - - private static final List SCHEME = Arrays.asList("ws", "wss", "http", "https"); - - @Override - public Optional buildClient(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - if (SCHEME.stream().noneMatch(scheme -> scheme.equals(uri.getScheme()))) { - return Optional.empty(); - } - - return Optional.of(WebsocketClientTransport.create(uri)); - } - - @Override - public Optional buildServer(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - if (SCHEME.stream().noneMatch(scheme -> scheme.equals(uri.getScheme()))) { - return Optional.empty(); - } - - int port = isSecure(uri) ? getPort(uri, 443) : getPort(uri, 80); - - return Optional.of(WebsocketServerTransport.create(uri.getHost(), port)); - } -} 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 aca238d31..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ 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.ServerTransport; @@ -32,9 +34,11 @@ public final class TcpClientTransport implements ClientTransport { private final TcpClient client; + private final int maxFrameLength; - private TcpClientTransport(TcpClient client) { + private TcpClientTransport(TcpClient client, int maxFrameLength) { this.client = client; + this.maxFrameLength = maxFrameLength; } /** @@ -73,7 +77,7 @@ public static TcpClientTransport create(String bindAddress, int port) { public static TcpClientTransport create(InetSocketAddress address) { Objects.requireNonNull(address, "address must not be null"); - TcpClient tcpClient = TcpClient.create().addressSupplier(() -> address); + TcpClient tcpClient = TcpClient.create().remoteAddress(() -> address); return create(tcpClient); } @@ -85,16 +89,33 @@ public static TcpClientTransport create(InetSocketAddress address) { * @throws NullPointerException if {@code client} is {@code null} */ public static TcpClientTransport create(TcpClient 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); + return new TcpClientTransport(client, maxFrameLength); + } + + @Override + public int maxFrameLength() { + return maxFrameLength; } @Override public Mono connect() { return client - .doOnConnected(c -> c.addHandlerLast(new RSocketLengthCodec())) - .connect() - .map(TcpDuplexConnection::new); + .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 8f4dac7eb..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,41 +16,50 @@ package io.rsocket.transport.netty.client; -import static io.rsocket.transport.netty.UriUtils.getPort; -import static io.rsocket.transport.netty.UriUtils.isSecure; +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.TransportHeaderAware; import io.rsocket.transport.netty.WebsocketDuplexConnection; import java.net.InetSocketAddress; import java.net.URI; -import java.util.Collections; +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.netty.Connection; 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} via a - * Websocket. + * An implementation of {@link ClientTransport} that connects to a {@link ServerTransport} over + * WebSocket. */ -public final class WebsocketClientTransport implements ClientTransport, TransportHeaderAware { +@SuppressWarnings("deprecation") +public final class WebsocketClientTransport + implements ClientTransport, io.rsocket.transport.TransportHeaderAware { + + private static final String DEFAULT_PATH = "/"; private final HttpClient client; - private String path; + private final String path; + + private HttpHeaders headers = new DefaultHttpHeaders(); - private Supplier> transportHeaders = Collections::emptyMap; + 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; } /** @@ -60,8 +69,7 @@ private WebsocketClientTransport(HttpClient client, String path) { * @return a new instance */ public static WebsocketClientTransport create(int port) { - TcpClient client = TcpClient.create().port(port); - return create(client); + return create(TcpClient.create().port(port)); } /** @@ -73,10 +81,7 @@ public static WebsocketClientTransport create(int port) { * @throws NullPointerException if {@code bindAddress} is {@code null} */ public static WebsocketClientTransport create(String bindAddress, int port) { - Objects.requireNonNull(bindAddress, "bindAddress must not be null"); - - TcpClient client = TcpClient.create().host(bindAddress).port(port); - return create(client); + return create(TcpClient.create().host(bindAddress).port(port)); } /** @@ -88,36 +93,35 @@ public static WebsocketClientTransport create(String bindAddress, int port) { */ public static WebsocketClientTransport create(InetSocketAddress address) { Objects.requireNonNull(address, "address must not be null"); - - TcpClient client = TcpClient.create().addressSupplier(() -> address); - return create(client); + return create(TcpClient.create().remoteAddress(() -> address)); } /** * Creates a new instance * - * @param uri the URI to connect to + * @param client the {@link TcpClient} to use * @return a new instance - * @throws NullPointerException if {@code uri} is {@code null} + * @throws NullPointerException if {@code client} or {@code path} is {@code null} */ - public static WebsocketClientTransport create(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - TcpClient client = createClient(uri); - return create(HttpClient.from(client), uri.getPath()); + public static WebsocketClientTransport create(TcpClient client) { + return new WebsocketClientTransport(HttpClient.from(client), DEFAULT_PATH); } /** * Creates a new instance * - * @param client the {@link TcpClient} to use + * @param uri the URI to connect to * @return a new instance - * @throws NullPointerException if {@code client} or {@code path} is {@code null} + * @throws NullPointerException if {@code uri} is {@code null} */ - public static WebsocketClientTransport create(TcpClient client) { - Objects.requireNonNull(client, "client must not be null"); - - return create(HttpClient.from(client), "/"); + public static WebsocketClientTransport create(URI uri) { + 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()); } /** @@ -129,38 +133,56 @@ public static WebsocketClientTransport create(TcpClient client) { * @throws NullPointerException if {@code client} or {@code path} is {@code null} */ public static WebsocketClientTransport create(HttpClient client, String path) { - Objects.requireNonNull(client, "client must not be null"); - Objects.requireNonNull(path, "path must not be null"); - return new WebsocketClientTransport(client, path); } - @Override - public Mono connect() { - return client - .headers(headers -> transportHeaders.get().forEach(headers::set)) - .websocket() - .uri(path) - .connect() - .map(WebsocketDuplexConnection::new); + /** + * 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; + } + + /** + * 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; } @Override public void setTransportHeaders(Supplier> transportHeaders) { - this.transportHeaders = - Objects.requireNonNull(transportHeaders, "transportHeaders must not be null"); + if (transportHeaders != null) { + transportHeaders.get().forEach((name, value) -> headers.add(name, value)); + } } - private static TcpClient createClient(URI uri) { - if (isSecure(uri)) { - return TcpClient.create() - .secure() - .host(uri.getHost()) - .port(getPort(uri, 443)); - } else { - return TcpClient.create() - .host(uri.getHost()) - .port(getPort(uri, 80)); - } + @Override + public int maxFrameLength() { + return specBuilder.build().maxFramePayloadLength(); + } + + @Override + public Mono connect() { + 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/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 index 639100b06..c4a257f76 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/CloseableChannel.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/CloseableChannel.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,18 +17,30 @@ 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. + * 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 { - private DisposableChannel channel; + /** 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 @@ -47,7 +59,15 @@ public final class CloseableChannel implements Closeable { * @see DisposableChannel#address() */ public InetSocketAddress address() { - return channel.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 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 dcf1476a7..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,14 @@ 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.RSocketLengthCodec; import io.rsocket.transport.netty.TcpDuplexConnection; - import java.net.InetSocketAddress; import java.util.Objects; - import reactor.core.publisher.Mono; import reactor.netty.tcp.TcpServer; @@ -33,9 +33,11 @@ public final class TcpServerTransport implements ServerTransport { private final TcpServer server; + private final int maxFrameLength; - private TcpServerTransport(TcpServer server) { + private TcpServerTransport(TcpServer server, int maxFrameLength) { this.server = server; + this.maxFrameLength = maxFrameLength; } /** @@ -59,7 +61,6 @@ public static TcpServerTransport create(int port) { */ public static TcpServerTransport create(String bindAddress, int port) { Objects.requireNonNull(bindAddress, "bindAddress must not be null"); - TcpServer server = TcpServer.create().host(bindAddress).port(port); return create(server); } @@ -73,7 +74,6 @@ public static TcpServerTransport create(String bindAddress, int port) { */ public static TcpServerTransport create(InetSocketAddress address) { Objects.requireNonNull(address, "address must not be null"); - return create(address.getHostName(), address.getPort()); } @@ -85,24 +85,39 @@ public static TcpServerTransport create(InetSocketAddress address) { * @throws NullPointerException if {@code server} is {@code null} */ public static TcpServerTransport create(TcpServer 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); + } - return new TcpServerTransport(server); + @Override + public int maxFrameLength() { + return maxFrameLength; } @Override public Mono start(ConnectionAcceptor acceptor) { Objects.requireNonNull(acceptor, "acceptor must not be null"); - return server - .doOnConnection(c -> { - c.addHandlerLast(new RSocketLengthCodec()); - TcpDuplexConnection connection = new TcpDuplexConnection(c); - acceptor - .apply(connection) - .then(Mono.never()) - .subscribe(c.disposeSubscriber()); - }) + .doOnConnection( + c -> { + c.addHandlerLast(new RSocketLengthCodec(maxFrameLength)); + acceptor + .apply(new TcpDuplexConnection(c)) + .then(Mono.never()) + .subscribe(c.disposeSubscriber()); + }) .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 index 27bafd3de..38344c472 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,8 @@ * An implementation of {@link ServerTransport} that connects via Websocket and listens on specified * routes. */ -public final class WebsocketRouteTransport implements ServerTransport { +public final class WebsocketRouteTransport + extends BaseWebsocketServerTransport { private final String path; @@ -51,8 +52,7 @@ public final class WebsocketRouteTransport implements ServerTransport */ public WebsocketRouteTransport( HttpServer server, Consumer routesBuilder, String path) { - - this.server = Objects.requireNonNull(server, "server must not be null"); + 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"); } @@ -60,12 +60,11 @@ public WebsocketRouteTransport( @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)); + routes.ws(path, newHandler(acceptor), specBuilder.build()); }) .bind() .map(CloseableChannel::new); @@ -78,14 +77,9 @@ public Mono start(ConnectionAcceptor acceptor) { * @return a new Websocket handler * @throws NullPointerException if {@code acceptor} is {@code null} */ - static BiFunction> newHandler( + public static BiFunction> newHandler( ConnectionAcceptor acceptor) { - - Objects.requireNonNull(acceptor, "acceptor must not be null"); - - return (in, out) -> { - WebsocketDuplexConnection connection = new WebsocketDuplexConnection((Connection)in); - return acceptor.apply(connection).then(out.neverComplete()); - }; + 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 b6ef5eaea..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,30 +16,35 @@ 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.TransportHeaderAware; +import io.rsocket.transport.netty.WebsocketDuplexConnection; import java.net.InetSocketAddress; -import java.util.Collections; +import java.util.Arrays; import java.util.Map; import java.util.Objects; import java.util.function.Supplier; import reactor.core.publisher.Mono; +import reactor.netty.Connection; import reactor.netty.http.server.HttpServer; /** * An implementation of {@link ServerTransport} that connects to a {@link ClientTransport} via a * Websocket. */ +@SuppressWarnings("deprecation") public final class WebsocketServerTransport - implements ServerTransport, TransportHeaderAware { + extends BaseWebsocketServerTransport + implements io.rsocket.transport.TransportHeaderAware { private final HttpServer server; - private Supplier> transportHeaders = Collections::emptyMap; + private HttpHeaders headers = new DefaultHttpHeaders(); private WebsocketServerTransport(HttpServer server) { - this.server = server; + this.server = serverConfigurer.apply(Objects.requireNonNull(server, "server must not be null")); } /** @@ -63,7 +68,6 @@ public static WebsocketServerTransport create(int port) { */ 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); } @@ -77,7 +81,6 @@ public static WebsocketServerTransport create(String bindAddress, int port) { */ public static WebsocketServerTransport create(InetSocketAddress address) { Objects.requireNonNull(address, "address must not be null"); - return create(address.getHostName(), address.getPort()); } @@ -88,27 +91,46 @@ public static WebsocketServerTransport create(InetSocketAddress address) { * @return a new instance * @throws NullPointerException if {@code server} is {@code null} */ - public static WebsocketServerTransport create(HttpServer server) { + 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) { - this.transportHeaders = - Objects.requireNonNull(transportHeaders, "transportHeaders must not be null"); + if (transportHeaders != null) { + transportHeaders.get().forEach((name, value) -> headers.add(name, value)); + } } @Override public Mono start(ConnectionAcceptor acceptor) { Objects.requireNonNull(acceptor, "acceptor must not be null"); - return server .handle( (request, response) -> { - transportHeaders.get().forEach(response::addHeader); - return response.sendWebsocket(WebsocketRouteTransport.newHandler(acceptor)); + 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/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 ec7ddcb80..000000000 --- a/rsocket-transport-netty/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright 2015-2018 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -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/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/SetupRejectionTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/SetupRejectionTest.java index b524af7a1..6fd3de791 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/SetupRejectionTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/SetupRejectionTest.java @@ -2,8 +2,9 @@ import io.rsocket.ConnectionSetupPayload; 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.exceptions.RejectedSetupException; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; @@ -18,10 +19,7 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import reactor.core.publisher.EmitterProcessor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -29,10 +27,12 @@ public class SetupRejectionTest { + /* + TODO Fix this test @DisplayName( "Rejecting setup by server causes requester RSocket disposal and RejectedSetupException") @ParameterizedTest - @MethodSource(value = "transports") + @MethodSource(value = "transports")*/ void rejectSetupTcp( Function> serverTransport, Function clientTransport) { @@ -42,20 +42,16 @@ void rejectSetupTcp( Mono serverRequester = acceptor.requesterRSocket(); CloseableChannel channel = - RSocketFactory.receive() - .acceptor(acceptor) - .transport(serverTransport.apply(new InetSocketAddress(0))) - .start() - .block(); + RSocketServer.create(acceptor) + .bind(serverTransport.apply(new InetSocketAddress("localhost", 0))) + .block(Duration.ofSeconds(5)); ErrorConsumer errorConsumer = new ErrorConsumer(); RSocket clientRequester = - RSocketFactory.connect() - .errorConsumer(errorConsumer) - .transport(clientTransport.apply(channel.address())) - .start() - .block(); + RSocketConnector.connectWith(clientTransport.apply(channel.address())) + .doOnError(errorConsumer) + .block(Duration.ofSeconds(5)); StepVerifier.create(errorConsumer.errors().next()) .expectNextMatches( @@ -64,7 +60,8 @@ void rejectSetupTcp( .verify(Duration.ofSeconds(5)); StepVerifier.create(clientRequester.onClose()).expectComplete().verify(Duration.ofSeconds(5)); - StepVerifier.create(serverRequester.flatMap(RSocket::onClose)) + + StepVerifier.create(serverRequester.flatMap(socket -> socket.onClose())) .expectComplete() .verify(Duration.ofSeconds(5)); 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 d21e809c3..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 @@ -16,33 +16,82 @@ package io.rsocket.transport.netty; -import io.rsocket.Frame; 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")); - public static void main(String... args) { - Mono client = - RSocketFactory.connect() - .frameDecoder(Frame::retain) - .transport(TcpClientTransport.create(7878)) - .start(); + @BeforeEach + void setUp() { + System.out.println("Starting ping-pong test (TCP transport)"); + System.out.println("port: " + port); + } - PingClient pingClient = new PingClient(client); + @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)); - int count = 1_000_000_000; + pingClient + .requestStreamPingPong(INTERACTIONS_COUNT, recorder) + .doOnTerminate(() -> System.out.println("Sent " + INTERACTIONS_COUNT + " messages.")) + .blockLast(); + } + + @Test + void requestStreamResumableTest() { + PingClient pingClient = newResumablePingClient(); + Recorder recorder = pingClient.startTracker(Duration.ofSeconds(1)); 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 e068b7dd2..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 @@ -16,19 +16,29 @@ package io.rsocket.transport.netty; -import io.rsocket.Frame; -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) { - RSocketFactory.receive() - .frameDecoder(Frame::retain) - .acceptor(new PingHandler()) - .transport(TcpServerTransport.create(7878)) - .start() + System.out.println("Starting TCP ping-pong server"); + System.out.println("port: " + port); + System.out.println("resume enabled: " + isResume); + + 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/TcpUriHandlerTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpUriHandlerTest.java deleted file mode 100644 index 25b443dd6..000000000 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpUriHandlerTest.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.netty; - -import io.rsocket.test.UriHandlerTest; -import io.rsocket.uri.UriHandler; - -final class TcpUriHandlerTest implements UriHandlerTest { - - @Override - public String getInvalidUri() { - return "http://test"; - } - - @Override - public UriHandler getUriHandler() { - return new TcpUriHandler(); - } - - @Override - public String getValidUri() { - return "tcp://test:9898"; - } -} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpUriTransportRegistryTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpUriTransportRegistryTest.java deleted file mode 100644 index a71cc27f9..000000000 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpUriTransportRegistryTest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.netty; - -import static org.assertj.core.api.Assertions.assertThat; - -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.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -final class TcpUriTransportRegistryTest { - - @DisplayName("non-tcp URI does not return TcpClientTransport") - @Test - void clientForUriInvalid() { - assertThat(UriTransportRegistry.clientForUri("amqp://localhost")) - .isNotInstanceOf(TcpClientTransport.class) - .isNotInstanceOf(WebsocketClientTransport.class); - } - - @DisplayName("tcp URI returns TcpClientTransport") - @Test - void clientForUriTcp() { - assertThat(UriTransportRegistry.clientForUri("tcp://test:9898")) - .isInstanceOf(TcpClientTransport.class); - } - - @DisplayName("non-tcp URI does not return TcpServerTransport") - @Test - void serverForUriInvalid() { - assertThat(UriTransportRegistry.serverForUri("amqp://localhost")) - .isNotInstanceOf(TcpServerTransport.class) - .isNotInstanceOf(WebsocketServerTransport.class); - } - - @DisplayName("tcp URI returns TcpServerTransport") - @Test - void serverForUriTcp() { - assertThat(UriTransportRegistry.serverForUri("tcp://test:9898")) - .isInstanceOf(TcpServerTransport.class); - } -} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/UriUtilsTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/UriUtilsTest.java deleted file mode 100644 index 7e5bf688d..000000000 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/UriUtilsTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.netty; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -import java.net.URI; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -final class UriUtilsTest { - - @DisplayName("returns the port") - @Test - void getPort() { - assertThat(UriUtils.getPort(URI.create("http://localhost:42"), Integer.MAX_VALUE)) - .isEqualTo(42); - } - - @DisplayName("getPort throws NullPointerException with null uri") - @Test - void getPortNullUri() { - assertThatNullPointerException() - .isThrownBy(() -> UriUtils.getPort(null, 80)) - .withMessage("uri must not be null"); - } - - @DisplayName("returns the default port") - @Test - void getPortUnset() { - assertThat(UriUtils.getPort(URI.create("http://localhost"), Integer.MAX_VALUE)) - .isEqualTo(Integer.MAX_VALUE); - } - - @DisplayName("returns the URI's secureness") - @Test - void isSecure() { - assertThat(UriUtils.isSecure(URI.create("http://localhost"))).isFalse(); - assertThat(UriUtils.isSecure(URI.create("ws://localhost"))).isFalse(); - - assertThat(UriUtils.isSecure(URI.create("https://localhost"))).isTrue(); - assertThat(UriUtils.isSecure(URI.create("wss://localhost"))).isTrue(); - } - - @DisplayName("isSecure throws NullPointerException with null uri") - @Test - void isSecureNullUri() { - assertThatNullPointerException() - .isThrownBy(() -> UriUtils.isSecure(null)) - .withMessage("uri must not be null"); - } -} 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/WebsocketPing.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java index 9b03d1fe2..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,8 @@ 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; @@ -28,7 +29,9 @@ public final class WebsocketPing { 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); @@ -37,7 +40,7 @@ public static void main(String... args) { int count = 1_000_000_000; pingClient - .startPingPong(count, recorder) + .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 c94a8c539..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,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,17 @@ 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) { - RSocketFactory.receive() - .acceptor(new PingHandler()) - .transport(WebsocketServerTransport.create(7878)) - .start() + 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 index 8b54d7189..ec33060b2 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketSecureTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketSecureTransportTest.java @@ -25,7 +25,6 @@ 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; @@ -39,21 +38,27 @@ final class WebsocketSecureTransportTest implements TransportTest { (address, server) -> WebsocketClientTransport.create( HttpClient.create() - .addressSupplier(server::address) - .secure(ssl -> ssl.sslContext( - SslContextBuilder.forClient() - .trustManager(InsecureTrustManagerFactory.INSTANCE))), + .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() - .addressSupplier(() -> address) - .secure(ssl -> ssl.sslContext( - SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())))); + 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); diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketUriHandlerTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketUriHandlerTest.java deleted file mode 100644 index 72a700b0e..000000000 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketUriHandlerTest.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.netty; - -import io.rsocket.test.UriHandlerTest; -import io.rsocket.uri.UriHandler; - -final class WebsocketUriHandlerTest implements UriHandlerTest { - - @Override - public String getInvalidUri() { - return "amqp://test"; - } - - @Override - public UriHandler getUriHandler() { - return new WebsocketUriHandler(); - } - - @Override - public String getValidUri() { - return "ws://test:9898"; - } -} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketUriTransportRegistryTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketUriTransportRegistryTest.java deleted file mode 100644 index 5688f14ed..000000000 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketUriTransportRegistryTest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket.transport.netty; - -import static org.assertj.core.api.Assertions.assertThat; - -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.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -final class WebsocketUriTransportRegistryTest { - - @DisplayName("non-ws URI does not return WebsocketClientTransport") - @Test - void clientForUriInvalid() { - assertThat(UriTransportRegistry.clientForUri("amqp://localhost")) - .isNotInstanceOf(TcpClientTransport.class) - .isNotInstanceOf(WebsocketClientTransport.class); - } - - @DisplayName("ws URI returns WebsocketClientTransport") - @Test - void clientForUriWebsocket() { - assertThat(UriTransportRegistry.clientForUri("ws://test:9898")) - .isInstanceOf(WebsocketClientTransport.class); - } - - @DisplayName("non-ws URI does not return WebsocketServerTransport") - @Test - void serverForUriInvalid() { - assertThat(UriTransportRegistry.serverForUri("amqp://localhost")) - .isNotInstanceOf(TcpServerTransport.class) - .isNotInstanceOf(WebsocketServerTransport.class); - } - - @DisplayName("ws URI returns WebsocketServerTransport") - @Test - void serverForUriWebsocket() { - assertThat(UriTransportRegistry.serverForUri("ws://test:9898")) - .isInstanceOf(WebsocketServerTransport.class); - } -} 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 index 388001fb6..ac4c6044b 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,7 +69,7 @@ void createInetSocketAddress() { @Test void createNullBindAddress() { assertThatNullPointerException() - .isThrownBy(() -> TcpClientTransport.create(null, 8000)) + .isThrownBy(() -> TcpClientTransport.create((String) null, 8000)) .withMessage("bindAddress must not be null"); } 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 index 202c5b3f3..944d20313 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,10 +25,13 @@ 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") @@ -55,13 +58,25 @@ void connectNoServer() { @DisplayName("creates client with BindAddress") @Test void createBindAddress() { - assertThat(WebsocketClientTransport.create("test-bind-address", 8000)).isNotNull(); + assertThat(WebsocketClientTransport.create("test-bind-address", 8000)) + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/"); } @DisplayName("creates client with HttpClient") @Test void createHttpClient() { - assertThat(WebsocketClientTransport.create(HttpClient.create(), "/")).isNotNull(); + 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") @@ -70,7 +85,8 @@ void createInetSocketAddress() { assertThat( WebsocketClientTransport.create( InetSocketAddress.createUnresolved("test-bind-address", 8000))) - .isNotNull(); + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/"); } @DisplayName("create throws NullPointerException with null bindAddress") @@ -78,7 +94,7 @@ void createInetSocketAddress() { void createNullBindAddress() { assertThatNullPointerException() .isThrownBy(() -> WebsocketClientTransport.create(null, 8000)) - .withMessage("bindAddress must not be null"); + .withMessage("host"); } @DisplayName("create throws NullPointerException with null client") @@ -86,7 +102,7 @@ void createNullBindAddress() { void createNullHttpClient() { assertThatNullPointerException() .isThrownBy(() -> WebsocketClientTransport.create(null, "/test-path")) - .withMessage("client must not be null"); + .withMessage("HttpClient must not be null"); } @DisplayName("create throws NullPointerException with null address") @@ -122,20 +138,22 @@ void createPort() { @DisplayName("creates client with URI") @Test void createUri() { - assertThat(WebsocketClientTransport.create(URI.create("ws://test-host/"))).isNotNull(); + assertThat(WebsocketClientTransport.create(URI.create("ws://test-host"))) + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/"); } - @DisplayName("sets transport headers") + @DisplayName("creates client with URI path") @Test - void setTransportHeader() { - WebsocketClientTransport.create(8000).setTransportHeaders(Collections::emptyMap); + void createUriPath() { + assertThat(WebsocketClientTransport.create(URI.create("ws://test-host/test"))) + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/test"); } - @DisplayName("setTransportHeaders throws NullPointerException with null headers") + @DisplayName("sets transport headers") @Test - void setTransportHeadersNullHeaders() { - assertThatNullPointerException() - .isThrownBy(() -> WebsocketClientTransport.create(8000).setTransportHeaders(null)) - .withMessage("transportHeaders must not be null"); + 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 index 0e03317c7..308118955 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/CloseableChannelTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/CloseableChannelTest.java @@ -46,11 +46,7 @@ void address() { @DisplayName("creates instance") @Test void constructor() { - channel - .map(CloseableChannel::new) - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); + channel.map(CloseableChannel::new).as(StepVerifier::create).expectNextCount(1).verifyComplete(); } @DisplayName("constructor throws NullPointerException with null context") 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 index 15a216b96..0e14d8f1d 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ void createInetSocketAddress() { @Test void createNullBindAddress() { assertThatNullPointerException() - .isThrownBy(() -> TcpServerTransport.create(null, 8000)) + .isThrownBy(() -> TcpServerTransport.create((String) null, 8000)) .withMessage("bindAddress must not be null"); } @@ -70,7 +70,7 @@ void createNullTcpClient() { @DisplayName("creates server with port") @Test void createPort() { - assertThat(TcpServerTransport.create(8000)).isNotNull(); + assertThat(TcpServerTransport.create("localhost", 8000)).isNotNull(); } @DisplayName("creates client with TcpServer") @@ -97,7 +97,7 @@ void start() { @Test void startNullAcceptor() { assertThatNullPointerException() - .isThrownBy(() -> TcpServerTransport.create(8000).start(null)) + .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 index 66822890a..2670b4a4b 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ 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.DisplayName; @@ -57,20 +56,6 @@ void constructorNullServer() { .withMessage("server must not be null"); } - @DisplayName("creates a new handler") - @Test - void newHandler() { - assertThat(WebsocketRouteTransport.newHandler(duplexConnection -> null)).isNotNull(); - } - - @DisplayName("newHandler throws NullPointerException with null acceptor") - @Test - void newHandlerNullAcceptor() { - assertThatNullPointerException() - .isThrownBy(() -> WebsocketRouteTransport.newHandler(null)) - .withMessage("acceptor must not be null"); - } - @DisplayName("starts server") @Test void start() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java index d1a6b374e..7f7567dc8 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,46 @@ 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() { @@ -86,14 +113,6 @@ void setTransportHeader() { WebsocketServerTransport.create(8000).setTransportHeaders(Collections::emptyMap); } - @DisplayName("setTransportHeaders throws NullPointerException with null headers") - @Test - void setTransportHeadersNullHeaders() { - assertThatNullPointerException() - .isThrownBy(() -> WebsocketServerTransport.create(8000).setTransportHeaders(null)) - .withMessage("transportHeaders must not be null"); - } - @DisplayName("starts server") @Test void start() { diff --git a/rsocket-transport-netty/src/test/resources/logback-test.xml b/rsocket-transport-netty/src/test/resources/logback-test.xml index 49b11d6fb..f9dec2bbe 100644 --- a/rsocket-transport-netty/src/test/resources/logback-test.xml +++ b/rsocket-transport-netty/src/test/resources/logback-test.xml @@ -24,6 +24,8 @@ + + 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 f32f1e087..25c3feee5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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' include 'rsocket-core' -include 'rsocket-examples' include 'rsocket-load-balancer' include 'rsocket-micrometer' include 'rsocket-test' -include 'rsocket-transport-aeron' include 'rsocket-transport-local' include 'rsocket-transport-netty' +include 'rsocket-bom' + +include 'rsocket-examples' +include 'benchmarks' + + + +gradleEnterprise { + buildScan { + termsOfServiceUrl = 'https://gradle.com/terms-of-service' + termsOfServiceAgree = 'yes' + } +} +