diff --git a/.github/workflows/gradle-main.yml b/.github/workflows/gradle-main.yml index 469ccb103..33bca8e72 100644 --- a/.github/workflows/gradle-main.yml +++ b/.github/workflows/gradle-main.yml @@ -142,10 +142,14 @@ jobs: run: chmod +x gradlew - name: Publish Packages to Artifactory if: ${{ matrix.jdk == '1.8' }} - run: ./gradlew -PversionSuffix="-SNAPSHOT" -PbuildNumber="${buildNumber}" publishMavenPublicationToGitHubPackagesRepository --no-daemon --stacktrace + run: ./gradlew -PversionSuffix="-SNAPSHOT" -PbuildNumber="${buildNumber}" publishMavenPublicationToSonatypeRepository --no-daemon --stacktrace env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} buildNumber: ${{ github.run_number }} + ORG_GRADLE_PROJECT_signingKey: ${{secrets.signingKey}} + ORG_GRADLE_PROJECT_signingPassword: ${{secrets.signingPassword}} + ORG_GRADLE_PROJECT_sonatypeUsername: ${{secrets.sonatypeUsername}} + ORG_GRADLE_PROJECT_sonatypePassword: ${{secrets.sonatypePassword}} - name: Aggregate test reports with ciMate if: always() continue-on-error: true diff --git a/build.gradle b/build.gradle index 9f4129932..2971a7767 100644 --- a/build.gradle +++ b/build.gradle @@ -16,11 +16,11 @@ plugins { id 'com.github.sherter.google-java-format' version '0.9' apply false - id 'me.champeau.jmh' version '0.6.7' apply false - id 'io.spring.dependency-management' version '1.0.13.RELEASE' apply false + id 'me.champeau.jmh' version '0.7.1' apply false + id 'io.spring.dependency-management' version '1.1.0' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false - id 'io.github.reyerizo.gradle.jcstress' version '0.8.13' apply false - id 'com.github.vlsi.gradle-extensions' version '1.76' apply false + id 'io.github.reyerizo.gradle.jcstress' version '0.8.15' apply false + id 'com.github.vlsi.gradle-extensions' version '1.89' apply false } boolean isCiServer = ["CI", "CONTINUOUS_INTEGRATION", "TRAVIS", "CIRCLECI", "bamboo_planKey", "GITHUB_ACTION"].with { @@ -33,21 +33,21 @@ subprojects { apply plugin: 'com.github.sherter.google-java-format' apply plugin: 'com.github.vlsi.gradle-extensions' - ext['reactor-bom.version'] = '2020.0.23' - ext['logback.version'] = '1.2.10' - ext['netty-bom.version'] = '4.1.81.Final' - ext['netty-boringssl.version'] = '2.0.54.Final' + ext['reactor-bom.version'] = '2022.0.7-SNAPSHOT' + ext['logback.version'] = '1.2.13' + ext['netty-bom.version'] = '4.1.117.Final' + ext['netty-boringssl.version'] = '2.0.69.Final' ext['hdrhistogram.version'] = '2.1.12' - ext['mockito.version'] = '4.4.0' + ext['mockito.version'] = '4.11.0' ext['slf4j.version'] = '1.7.36' - ext['jmh.version'] = '1.35' - ext['junit.version'] = '5.8.1' - ext['micrometer.version'] = '1.10.0-SNAPSHOT' - ext['micrometer-tracing.version'] = '1.0.0-SNAPSHOT' - ext['assertj.version'] = '3.22.0' + ext['jmh.version'] = '1.36' + ext['junit.version'] = '5.9.3' + ext['micrometer.version'] = '1.11.12' + ext['micrometer-tracing.version'] = '1.1.13' + ext['assertj.version'] = '3.24.2' ext['netflix.limits.version'] = '0.3.6' ext['bouncycastle-bcpkix.version'] = '1.70' - ext['awaitility.version'] = '4.1.1' + ext['awaitility.version'] = '4.2.0' group = "io.rsocket" @@ -70,6 +70,8 @@ subprojects { mavenBom "io.projectreactor:reactor-bom:${ext['reactor-bom.version']}" mavenBom "io.netty:netty-bom:${ext['netty-bom.version']}" mavenBom "org.junit:junit-bom:${ext['junit.version']}" + mavenBom "io.micrometer:micrometer-bom:${ext['micrometer.version']}" + mavenBom "io.micrometer:micrometer-tracing-bom:${ext['micrometer-tracing.version']}" } dependencies { @@ -77,11 +79,6 @@ subprojects { dependency "ch.qos.logback:logback-classic:${ext['logback.version']}" dependency "io.netty:netty-tcnative-boringssl-static:${ext['netty-boringssl.version']}" dependency "org.bouncycastle:bcpkix-jdk15on:${ext['bouncycastle-bcpkix.version']}" - dependency "io.micrometer:micrometer-core:${ext['micrometer.version']}" - dependency "io.micrometer:micrometer-observation:${ext['micrometer.version']}" - dependency "io.micrometer:micrometer-test:${ext['micrometer.version']}" - dependency "io.micrometer:micrometer-tracing:${ext['micrometer-tracing.version']}" - dependency "io.micrometer:micrometer-tracing-integration-test:${ext['micrometer-tracing.version']}" dependency "org.assertj:assertj-core:${ext['assertj.version']}" dependency "org.hdrhistogram:HdrHistogram:${ext['hdrhistogram.version']}" dependency "org.slf4j:slf4j-api:${ext['slf4j.version']}" @@ -109,6 +106,7 @@ subprojects { includeGroup "io.micrometer" includeGroup "io.projectreactor" includeGroup "io.projectreactor.netty" + includeGroup "io.micrometer" } } @@ -179,8 +177,6 @@ subprojects { } } - forkEvery = 1 - if (isCiServer) { def stdout = new LinkedList() beforeTest { TestDescriptor td -> diff --git a/gradle.properties b/gradle.properties index 0421e1496..d138852c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,4 +12,4 @@ # limitations under the License. # version=1.2.0 -perfBaselineVersion=1.1.2 +perfBaselineVersion=1.1.4 diff --git a/gradle/publications.gradle b/gradle/publications.gradle index 97704e701..9e8dd6d88 100644 --- a/gradle/publications.gradle +++ b/gradle/publications.gradle @@ -21,16 +21,6 @@ subprojects { } } developers { - developer { - id = 'rdegnan' - name = 'Ryland Degnan' - email = 'ryland@netifi.com' - } - developer { - id = 'yschimke' - name = 'Yuri Schimke' - email = 'yuri@schimke.ee' - } developer { id = 'OlegDokuka' name = 'Oleh Dokuka' diff --git a/gradle/sonotype.gradle b/gradle/sonotype.gradle index 1effd76b0..f339079b0 100644 --- a/gradle/sonotype.gradle +++ b/gradle/sonotype.gradle @@ -20,7 +20,9 @@ subprojects { repositories { maven { name = "sonatype" - url = "https://oss.sonatype.org/service/local/staging/deploy/maven2" + url = project.version.contains("-SNAPSHOT") + ? "https://oss.sonatype.org/content/repositories/snapshots/" + : "https://oss.sonatype.org/service/local/staging/deploy/maven2" credentials { username project.findProperty("sonatypeUsername") password project.findProperty("sonatypePassword") diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c02..249e5832f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e750102e0..774fae876 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c8..a69d9cb6c 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,101 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f93..53a6b238d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index cd8595216..da5b69b14 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -34,20 +34,21 @@ dependencies { testImplementation 'org.assertj:assertj-core' testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-params' - testImplementation 'org.mockito:mockito-core' + testImplementation 'org.mockito:mockito-junit-jupiter' testImplementation 'org.awaitility:awaitility' testRuntimeOnly 'ch.qos.logback:logback-classic' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' jcstressImplementation(project(":rsocket-test")) + jcstressImplementation 'org.slf4j:slf4j-api' jcstressImplementation "ch.qos.logback:logback-classic" jcstressImplementation 'io.projectreactor:reactor-test' } jcstress { - mode = 'quick' //quick, default, tough - jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.15" + mode = 'sanity' //sanity, quick, default, tough + jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.16" } jar { diff --git a/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java index bdbdc7a3b..a2d9fcf4d 100644 --- a/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java +++ b/rsocket-core/src/jcstress/java/io/rsocket/internal/UnboundedProcessorStressTest.java @@ -3,6 +3,9 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.UnpooledByteBufAllocator; import io.rsocket.core.StressSubscriber; +import io.rsocket.utils.FastLogger; +import java.util.Arrays; +import java.util.ConcurrentModificationException; import org.openjdk.jcstress.annotations.Actor; import org.openjdk.jcstress.annotations.Arbiter; import org.openjdk.jcstress.annotations.Expect; @@ -14,6 +17,7 @@ import org.openjdk.jcstress.infra.results.L_Result; import reactor.core.Fuseable; import reactor.core.publisher.Hooks; +import reactor.util.Logger; public abstract class UnboundedProcessorStressTest { @@ -21,7 +25,9 @@ public abstract class UnboundedProcessorStressTest { Hooks.onErrorDropped(t -> {}); } - final UnboundedProcessor unboundedProcessor = new UnboundedProcessor(); + final Logger logger = new FastLogger(getClass().getName()); + + final UnboundedProcessor unboundedProcessor = new UnboundedProcessor(logger); @JCStressTest @Outcome( @@ -145,6 +151,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -270,6 +278,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -375,6 +385,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -476,6 +488,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -578,6 +592,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -701,6 +717,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -781,6 +799,8 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -837,9 +857,15 @@ public void arbiter(LLL_Result r) { + stressSubscriber.onErrorCalls * 2 + stressSubscriber.droppedErrors.size() * 3; + if (stressSubscriber.concurrentOnNext || stressSubscriber.concurrentOnComplete) { + throw new ConcurrentModificationException("boo"); + } + stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -892,6 +918,72 @@ public void arbiter(LLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r3 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); + } + } + + @JCStressTest + @Outcome( + id = { + "0, 1, 0, 5", + "1, 1, 0, 5", + "2, 1, 0, 5", + "3, 1, 0, 5", + "4, 1, 0, 5", + "5, 1, 0, 5", + }, + expect = Expect.ACCEPTABLE, + desc = "onComplete()") + @State + public static class Smoke33StressTest extends UnboundedProcessorStressTest { + + final StressSubscriber stressSubscriber = + new StressSubscriber<>(Long.MAX_VALUE, Fuseable.NONE); + final ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(1); + final ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(2); + final ByteBuf byteBuf3 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(3); + final ByteBuf byteBuf4 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(4); + final ByteBuf byteBuf5 = UnpooledByteBufAllocator.DEFAULT.buffer().writeByte(5); + + { + unboundedProcessor.subscribe(stressSubscriber); + } + + @Actor + public void next1() { + unboundedProcessor.tryEmitNormal(byteBuf1); + unboundedProcessor.tryEmitPrioritized(byteBuf2); + } + + @Actor + public void next2() { + unboundedProcessor.tryEmitPrioritized(byteBuf3); + unboundedProcessor.tryEmitNormal(byteBuf4); + } + + @Actor + public void complete() { + unboundedProcessor.tryEmitFinal(byteBuf5); + } + + @Arbiter + public void arbiter(LLLL_Result r) { + r.r1 = stressSubscriber.onNextCalls; + r.r2 = + stressSubscriber.onCompleteCalls + + stressSubscriber.onErrorCalls * 2 + + stressSubscriber.droppedErrors.size() * 3; + + r.r4 = stressSubscriber.values.get(stressSubscriber.values.size() - 1).readByte(); + stressSubscriber.values.forEach(ByteBuf::release); + + r.r3 = + byteBuf1.refCnt() + + byteBuf2.refCnt() + + byteBuf3.refCnt() + + byteBuf4.refCnt() + + byteBuf5.refCnt(); } } @@ -1043,6 +1135,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1174,6 +1268,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1326,6 +1422,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1458,6 +1556,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1523,6 +1623,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1588,6 +1690,8 @@ public void arbiter(LLLL_Result r) { stressSubscriber.values.forEach(ByteBuf::release); r.r4 = byteBuf1.refCnt() + byteBuf2.refCnt() + byteBuf3.refCnt() + byteBuf4.refCnt(); + + checkOutcomes(this, r.toString(), logger); } } @@ -1614,6 +1718,16 @@ public void subscribe2() { @Arbiter public void arbiter(L_Result r) { r.r1 = stressSubscriber1.onErrorCalls + stressSubscriber2.onErrorCalls; + + checkOutcomes(this, r.toString(), logger); + } + } + + static void checkOutcomes(Object instance, String result, Logger logger) { + if (Arrays.stream(instance.getClass().getDeclaredAnnotationsByType(Outcome.class)) + .flatMap(o -> Arrays.stream(o.id())) + .noneMatch(s -> s.equalsIgnoreCase(result))) { + throw new RuntimeException(result + " " + logger); } } } diff --git a/rsocket-core/src/jcstress/java/io/rsocket/resume/InMemoryResumableFramesStoreStressTest.java b/rsocket-core/src/jcstress/java/io/rsocket/resume/InMemoryResumableFramesStoreStressTest.java new file mode 100644 index 000000000..f0b209552 --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/resume/InMemoryResumableFramesStoreStressTest.java @@ -0,0 +1,118 @@ +package io.rsocket.resume; + +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.rsocket.exceptions.ConnectionErrorException; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.internal.UnboundedProcessor; +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.LL_Result; +import reactor.core.Disposable; + +public class InMemoryResumableFramesStoreStressTest { + boolean storeClosed; + + InMemoryResumableFramesStore store = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 128); + boolean processorClosed; + UnboundedProcessor processor = new UnboundedProcessor(() -> processorClosed = true); + + void subscribe() { + store.saveFrames(processor).subscribe(); + store.onClose().subscribe(null, t -> storeClosed = true, () -> storeClosed = true); + } + + @JCStressTest + @Outcome( + id = {"true, true"}, + expect = ACCEPTABLE) + @State + public static class TwoSubscribesRaceStressTest extends InMemoryResumableFramesStoreStressTest { + + Disposable d1; + + final ByteBuf b1 = + PayloadFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 1, + false, + true, + false, + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello1"), + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello2")); + final ByteBuf b2 = + PayloadFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 3, + false, + true, + false, + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello3"), + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello4")); + final ByteBuf b3 = + PayloadFrameCodec.encode( + ByteBufAllocator.DEFAULT, + 5, + false, + true, + false, + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello5"), + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "hello6")); + + final ByteBuf c1 = + ErrorFrameCodec.encode(ByteBufAllocator.DEFAULT, 0, new ConnectionErrorException("closed")); + + { + subscribe(); + d1 = store.doOnDiscard(ByteBuf.class, ByteBuf::release).subscribe(ByteBuf::release, t -> {}); + } + + @Actor + public void producer1() { + processor.tryEmitNormal(b1); + processor.tryEmitNormal(b2); + processor.tryEmitNormal(b3); + } + + @Actor + public void producer2() { + processor.tryEmitFinal(c1); + } + + @Actor + public void producer3() { + d1.dispose(); + store + .doOnDiscard(ByteBuf.class, ByteBuf::release) + .subscribe(ByteBuf::release, t -> {}) + .dispose(); + store + .doOnDiscard(ByteBuf.class, ByteBuf::release) + .subscribe(ByteBuf::release, t -> {}) + .dispose(); + store.doOnDiscard(ByteBuf.class, ByteBuf::release).subscribe(ByteBuf::release, t -> {}); + } + + @Actor + public void producer4() { + store.releaseFrames(0); + store.releaseFrames(0); + store.releaseFrames(0); + } + + @Arbiter + public void arbiter(LL_Result r) { + r.r1 = storeClosed; + r.r2 = processorClosed; + } + } +} diff --git a/rsocket-core/src/jcstress/java/io/rsocket/utils/FastLogger.java b/rsocket-core/src/jcstress/java/io/rsocket/utils/FastLogger.java new file mode 100644 index 000000000..c301d87cf --- /dev/null +++ b/rsocket-core/src/jcstress/java/io/rsocket/utils/FastLogger.java @@ -0,0 +1,137 @@ +package io.rsocket.utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import reactor.util.Logger; + +/** + * Implementation of {@link Logger} which is based on the {@link ThreadLocal} based queue which + * collects all the events on the per-thread basis.
Such logger is designed to have all events + * stored during the stress-test run and then sorted and printed out once all the Threads completed + * execution (inside the {@link org.openjdk.jcstress.annotations.Arbiter} annotated method.
+ * Note, this implementation only supports trace-level logs and ignores all others, it is intended + * to be used by {@link reactor.core.publisher.StateLogger}. + */ +public class FastLogger implements Logger { + + final Map> queues = new ConcurrentHashMap<>(); + + final ThreadLocal> logsQueueLocal = + ThreadLocal.withInitial( + () -> { + final ArrayList logs = new ArrayList<>(100); + queues.put(Thread.currentThread(), logs); + return logs; + }); + + private final String name; + + public FastLogger(String name) { + this.name = name; + } + + @Override + public String toString() { + return queues + .values() + .stream() + .flatMap(List::stream) + .sorted( + Comparator.comparingLong( + s -> { + Pattern pattern = Pattern.compile("\\[(.*?)]"); + Matcher matcher = pattern.matcher(s); + matcher.find(); + return Long.parseLong(matcher.group(1)); + })) + .collect(Collectors.joining("\n")); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public boolean isTraceEnabled() { + return true; + } + + @Override + public void trace(String msg) { + logsQueueLocal.get().add(String.format("[%s] %s", System.nanoTime(), msg)); + } + + @Override + public void trace(String format, Object... arguments) { + trace(String.format(format, arguments)); + } + + @Override + public void trace(String msg, Throwable t) { + trace(String.format("%s, %s", msg, Arrays.toString(t.getStackTrace()))); + } + + @Override + public boolean isDebugEnabled() { + return false; + } + + @Override + public void debug(String msg) {} + + @Override + public void debug(String format, Object... arguments) {} + + @Override + public void debug(String msg, Throwable t) {} + + @Override + public boolean isInfoEnabled() { + return false; + } + + @Override + public void info(String msg) {} + + @Override + public void info(String format, Object... arguments) {} + + @Override + public void info(String msg, Throwable t) {} + + @Override + public boolean isWarnEnabled() { + return false; + } + + @Override + public void warn(String msg) {} + + @Override + public void warn(String format, Object... arguments) {} + + @Override + public void warn(String msg, Throwable t) {} + + @Override + public boolean isErrorEnabled() { + return false; + } + + @Override + public void error(String msg) {} + + @Override + public void error(String format, Object... arguments) {} + + @Override + public void error(String msg, Throwable t) {} +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java index d6cb46d98..e19d31924 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ClientServerInputMultiplexer.java @@ -67,8 +67,8 @@ public ClientServerInputMultiplexer( this.source = source; this.isClient = isClient; - this.serverReceiver = new InternalDuplexConnection(this, source); - this.clientReceiver = new InternalDuplexConnection(this, source); + this.serverReceiver = new InternalDuplexConnection(Type.SERVER, this, source); + this.clientReceiver = new InternalDuplexConnection(Type.CLIENT, this, source); this.serverConnection = registry.initConnection(Type.SERVER, serverReceiver); this.clientConnection = registry.initConnection(Type.CLIENT, clientReceiver); } @@ -195,8 +195,33 @@ int incrementAndGetCheckingState() { } } + @Override + public String toString() { + return "ClientServerInputMultiplexer{" + + "serverReceiver=" + + serverReceiver + + ", clientReceiver=" + + clientReceiver + + ", serverConnection=" + + serverConnection + + ", clientConnection=" + + clientConnection + + ", source=" + + source + + ", isClient=" + + isClient + + ", s=" + + s + + ", t=" + + t + + ", state=" + + state + + '}'; + } + private static class InternalDuplexConnection extends Flux implements Subscription, DuplexConnection { + private final Type type; private final ClientServerInputMultiplexer clientServerInputMultiplexer; private final DuplexConnection source; @@ -207,7 +232,10 @@ private static class InternalDuplexConnection extends Flux CoreSubscriber actual; public InternalDuplexConnection( - ClientServerInputMultiplexer clientServerInputMultiplexer, DuplexConnection source) { + Type type, + ClientServerInputMultiplexer clientServerInputMultiplexer, + DuplexConnection source) { + this.type = type; this.clientServerInputMultiplexer = clientServerInputMultiplexer; this.source = source; } @@ -304,5 +332,17 @@ public Mono onClose() { public double availability() { return source.availability(); } + + @Override + public String toString() { + return "InternalDuplexConnection{" + + "type=" + + type + + ", source=" + + source + + ", state=" + + state + + '}'; + } } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/ClientSetup.java b/rsocket-core/src/main/java/io/rsocket/core/ClientSetup.java index 725201fe7..3477b8d6d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ClientSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ClientSetup.java @@ -4,6 +4,7 @@ import io.netty.buffer.Unpooled; import io.rsocket.DuplexConnection; import java.nio.channels.ClosedChannelException; +import reactor.core.Disposable; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; @@ -25,8 +26,24 @@ class ResumableClientSetup extends ClientSetup { @Override Mono> init(DuplexConnection connection) { - return Mono.>create( - sink -> sink.onRequest(__ -> new SetupHandlingDuplexConnection(connection, sink))) - .or(connection.onClose().then(Mono.error(ClosedChannelException::new))); + return Mono.create( + sink -> { + sink.onRequest( + __ -> { + new SetupHandlingDuplexConnection(connection, sink); + }); + + Disposable subscribe = + connection + .onClose() + .doFinally(__ -> sink.error(new ClosedChannelException())) + .subscribe(); + sink.onCancel( + () -> { + subscribe.dispose(); + connection.dispose(); + connection.receive().subscribe(); + }); + }); } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java index 5119814cd..82a02268d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultRSocketClient.java @@ -46,13 +46,16 @@ */ class DefaultRSocketClient extends ResolvingOperator implements CoreSubscriber, CorePublisher, RSocketClient { - static final Consumer DISCARD_ELEMENTS_CONSUMER = - referenceCounted -> { - if (referenceCounted.refCnt() > 0) { - try { - referenceCounted.release(); - } catch (IllegalReferenceCountException e) { - // ignored + static final Consumer DISCARD_ELEMENTS_CONSUMER = + data -> { + if (data instanceof ReferenceCounted) { + ReferenceCounted referenceCounted = ((ReferenceCounted) data); + if (referenceCounted.refCnt() > 0) { + try { + referenceCounted.release(); + } catch (IllegalReferenceCountException e) { + // ignored + } } } }; @@ -450,8 +453,8 @@ public void accept(RSocket rSocket, Throwable t) { @Override public void request(long n) { - this.main.request(n); super.request(n); + this.main.request(n); } public void cancel() { diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index 432c0f0f5..de494c4e3 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -47,6 +47,7 @@ import java.util.function.Supplier; import reactor.core.Disposable; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; import reactor.util.annotation.Nullable; import reactor.util.function.Tuples; import reactor.util.retry.Retry; @@ -633,8 +634,7 @@ public Mono connect(Supplier transportSupplier) { wrappedConnection = resumableDuplexConnection; } else { keepAliveHandler = - new KeepAliveHandler.DefaultKeepAliveHandler( - clientServerConnection); + new KeepAliveHandler.DefaultKeepAliveHandler(); wrappedConnection = clientServerConnection; } @@ -655,6 +655,11 @@ public Mono connect(Supplier transportSupplier) { requesterLeaseTracker = null; } + final Sinks.Empty requesterOnAllClosedSink = + Sinks.unsafe().empty(); + final Sinks.Empty responderOnAllClosedSink = + Sinks.unsafe().empty(); + RSocket rSocketRequester = new RSocketRequester( multiplexer.asClientConnection(), @@ -667,7 +672,11 @@ public Mono connect(Supplier transportSupplier) { (int) keepAliveMaxLifeTime.toMillis(), keepAliveHandler, interceptors::initRequesterRequestInterceptor, - requesterLeaseTracker); + requesterLeaseTracker, + requesterOnAllClosedSink, + Mono.whenDelayError( + responderOnAllClosedSink.asMono(), + requesterOnAllClosedSink.asMono())); RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); @@ -715,7 +724,8 @@ public Mono connect(Supplier transportSupplier) { (RequestInterceptor) leases.sender) : interceptors - ::initResponderRequestInterceptor); + ::initResponderRequestInterceptor, + responderOnAllClosedSink); return wrappedRSocketRequester; }) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index bf298706a..b8a9c00ff 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -66,8 +66,10 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { RSocketRequester.class, Throwable.class, "terminationError"); @Nullable private final RequesterLeaseTracker requesterLeaseTracker; + + private final Sinks.Empty onThisSideClosedSink; + private final Mono onAllClosed; private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; - private final Sinks.Empty onClose; RSocketRequester( DuplexConnection connection, @@ -80,7 +82,9 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { int keepAliveAckTimeout, @Nullable KeepAliveHandler keepAliveHandler, Function requestInterceptorFunction, - @Nullable RequesterLeaseTracker requesterLeaseTracker) { + @Nullable RequesterLeaseTracker requesterLeaseTracker, + Sinks.Empty onThisSideClosedSink, + Mono onAllClosed) { super( mtu, maxFrameLength, @@ -91,10 +95,11 @@ class RSocketRequester extends RequesterResponderSupport implements RSocket { requestInterceptorFunction); this.requesterLeaseTracker = requesterLeaseTracker; - this.onClose = Sinks.empty(); + this.onThisSideClosedSink = onThisSideClosedSink; + this.onAllClosed = onAllClosed; // DO NOT Change the order here. The Send processor must be subscribed to before receiving - connection.onClose().subscribe(null, this::tryTerminateOnConnectionError, this::tryShutdown); + connection.onClose().subscribe(null, this::tryShutdown, this::tryShutdown); connection.receive().subscribe(this::handleIncomingFrames, e -> {}); @@ -188,7 +193,11 @@ public double availability() { @Override public void dispose() { - tryShutdown(); + if (terminationError != null) { + return; + } + + getDuplexConnection().sendErrorAndClose(new ConnectionErrorException("Disposed")); } @Override @@ -198,7 +207,7 @@ public boolean isDisposed() { @Override public Mono onClose() { - return onClose.asMono(); + return onAllClosed; } private void handleIncomingFrames(ByteBuf frame) { @@ -303,10 +312,34 @@ private void tryTerminateOnKeepAlive(KeepAliveSupport.KeepAlive keepAlive) { () -> new ConnectionErrorException( String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()))); + getDuplexConnection().dispose(); } - private void tryTerminateOnConnectionError(Throwable e) { - tryTerminate(() -> e); + private void tryShutdown(Throwable e) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("trying to close requester " + getDuplexConnection()); + } + if (terminationError == null) { + if (TERMINATION_ERROR.compareAndSet(this, null, e)) { + terminate(CLOSED_CHANNEL_EXCEPTION); + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); + } + } + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.info( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); + } + } } private void tryTerminateOnZeroError(ByteBuf errorFrame) { @@ -314,27 +347,67 @@ private void tryTerminateOnZeroError(ByteBuf errorFrame) { } private void tryTerminate(Supplier errorSupplier) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("trying to close requester " + getDuplexConnection()); + } if (terminationError == null) { Throwable e = errorSupplier.get(); if (TERMINATION_ERROR.compareAndSet(this, null, e)) { terminate(e); + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); + } + } + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); } } } private void tryShutdown() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("trying to close requester " + getDuplexConnection()); + } if (terminationError == null) { if (TERMINATION_ERROR.compareAndSet(this, null, CLOSED_CHANNEL_EXCEPTION)) { terminate(CLOSED_CHANNEL_EXCEPTION); + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); + } + } + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "trying to close requester failed because of " + + terminationError + + " " + + getDuplexConnection()); } } } private void terminate(Throwable e) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("closing requester " + getDuplexConnection() + " due to " + e); + } if (keepAliveFramesAcceptor != null) { keepAliveFramesAcceptor.dispose(); } - getDuplexConnection().dispose(); final RequestInterceptor requestInterceptor = getRequestInterceptor(); if (requestInterceptor != null) { requestInterceptor.dispose(); @@ -361,9 +434,12 @@ private void terminate(Throwable e) { } if (e == CLOSED_CHANNEL_EXCEPTION) { - onClose.tryEmitEmpty(); + onThisSideClosedSink.tryEmitEmpty(); } else { - onClose.tryEmitError(e); + onThisSideClosedSink.tryEmitError(e); + } + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("requester closed " + getDuplexConnection()); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index ce4fe70a3..50c5ba54c 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -44,6 +44,7 @@ import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; import reactor.util.annotation.Nullable; /** Responder side of RSocket. Receives {@link ByteBuf}s from a peer's {@link RSocketRequester} */ @@ -54,6 +55,7 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { private static final Exception CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException(); private final RSocket requestHandler; + private final Sinks.Empty onThisSideClosedSink; @Nullable private final ResponderLeaseTracker leaseHandler; @@ -70,7 +72,8 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { int mtu, int maxFrameLength, int maxInboundPayloadSize, - Function requestInterceptorFunction) { + Function requestInterceptorFunction, + Sinks.Empty onThisSideClosedSink) { super( mtu, maxFrameLength, @@ -83,19 +86,27 @@ class RSocketResponder extends RequesterResponderSupport implements RSocket { this.requestHandler = requestHandler; this.leaseHandler = leaseHandler; - - connection.receive().subscribe(this::handleFrame, e -> {}); + this.onThisSideClosedSink = onThisSideClosedSink; connection .onClose() .subscribe(null, this::tryTerminateOnConnectionError, this::tryTerminateOnConnectionClose); + + connection.receive().subscribe(this::handleFrame, e -> {}); } private void tryTerminateOnConnectionError(Throwable e) { + if (LOGGER.isDebugEnabled()) { + + LOGGER.debug("Try terminate connection on responder side"); + } tryTerminate(() -> e); } private void tryTerminateOnConnectionClose() { + if (LOGGER.isDebugEnabled()) { + LOGGER.info("Try terminate connection on responder side"); + } tryTerminate(() -> CLOSED_CHANNEL_EXCEPTION); } @@ -169,6 +180,9 @@ public Mono onClose() { } final void doOnDispose() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("closing responder " + getDuplexConnection()); + } cleanUpSendingSubscriptions(); getDuplexConnection().dispose(); @@ -183,6 +197,10 @@ final void doOnDispose() { } requestHandler.dispose(); + onThisSideClosedSink.tryEmitEmpty(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("responder closed " + getDuplexConnection()); + } } private void cleanUpSendingSubscriptions() { diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index 3208bb4fd..e969c39d2 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; /** * The main class for starting an RSocket server. @@ -437,6 +438,9 @@ private Mono acceptSetup( requesterLeaseTracker = null; } + final Sinks.Empty requesterOnAllClosedSink = Sinks.unsafe().empty(); + final Sinks.Empty responderOnAllClosedSink = Sinks.unsafe().empty(); + RSocket rSocketRequester = new RSocketRequester( multiplexer.asServerConnection(), @@ -449,15 +453,24 @@ private Mono acceptSetup( setupPayload.keepAliveMaxLifetime(), keepAliveHandler, interceptors::initRequesterRequestInterceptor, - requesterLeaseTracker); + requesterLeaseTracker, + requesterOnAllClosedSink, + Mono.whenDelayError( + responderOnAllClosedSink.asMono(), requesterOnAllClosedSink.asMono())); RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); return interceptors .initSocketAcceptor(acceptor) .accept(setupPayload, wrappedRSocketRequester) - .doOnError( - err -> serverSetup.sendError(wrappedDuplexConnection, rejectedSetupError(err))) + .onErrorResume( + err -> + Mono.fromRunnable( + () -> + serverSetup.sendError( + wrappedDuplexConnection, rejectedSetupError(err))) + .then(wrappedDuplexConnection.onClose()) + .then(Mono.error(err))) .doOnNext( rSocketHandler -> { RSocket wrappedRSocketHandler = interceptors.initResponder(rSocketHandler); @@ -481,7 +494,8 @@ private Mono acceptSetup( ? rSocket -> interceptors.initResponderRequestInterceptor( rSocket, (RequestInterceptor) leases.sender) - : interceptors::initResponderRequestInterceptor); + : interceptors::initResponderRequestInterceptor, + responderOnAllClosedSink); }) .doFinally(signalType -> setupPayload.release()) .then(); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java index 55ec43feb..6182ca506 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamRequesterFlux.java @@ -238,11 +238,11 @@ void sendFirstPayload(Payload payload, long initialRequestN) { return; } - sm.remove(streamId, this); - final ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, streamId); connection.sendFrame(streamId, cancelFrame); + sm.remove(streamId, this); + if (requestInterceptor != null) { requestInterceptor.onCancel(streamId, FrameType.REQUEST_STREAM); } @@ -276,12 +276,13 @@ public final void cancel() { if (isFirstFrameSent(previousState)) { final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); ReassemblyUtils.synchronizedRelease(this, previousState); this.connection.sendFrame(streamId, CancelFrameCodec.encode(this.allocator, streamId)); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onCancel(streamId, FrameType.REQUEST_STREAM); @@ -309,13 +310,14 @@ public final void handlePayload(Payload p) { } final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); final IllegalStateException cause = Exceptions.failWithOverflow( "The number of messages received exceeds the number requested"); this.connection.sendFrame(streamId, CancelFrameCodec.encode(this.allocator, streamId)); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, cause); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamResponderSubscriber.java b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamResponderSubscriber.java index 774fae9e5..48903ae38 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RequestStreamResponderSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestStreamResponderSubscriber.java @@ -144,6 +144,8 @@ public void onNext(Payload p) { final ByteBuf errorFrame = ErrorFrameCodec.encode(allocator, streamId, e); sender.sendFrame(streamId, errorFrame); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, e); @@ -162,6 +164,8 @@ public void onNext(Payload p) { new CanceledException("Failed to validate payload. Cause" + e.getMessage())); sender.sendFrame(streamId, errorFrame); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, e); @@ -176,6 +180,8 @@ public void onNext(Payload p) { return; } + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, t); @@ -195,8 +201,6 @@ boolean tryTerminateOnError() { return false; } - this.requesterResponderSupport.remove(this.streamId, this); - currentSubscription.cancel(); return true; @@ -222,11 +226,12 @@ public void onError(Throwable t) { } final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); final ByteBuf errorFrame = ErrorFrameCodec.encode(this.allocator, streamId, t); this.connection.sendFrame(streamId, errorFrame); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, t); @@ -246,11 +251,12 @@ public void onComplete() { } final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); final ByteBuf completeFrame = PayloadFrameCodec.encodeComplete(this.allocator, streamId); this.connection.sendFrame(streamId, completeFrame); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, null); @@ -321,7 +327,6 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas S.lazySet(this, Operators.cancelledSubscription()); final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); this.frames = null; frames.release(); @@ -334,6 +339,8 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas new CanceledException("Failed to reassemble payload. Cause: " + e.getMessage())); this.connection.sendFrame(streamId, errorFrame); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, e); @@ -354,7 +361,6 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas this.done = true; final int streamId = this.streamId; - this.requesterResponderSupport.remove(streamId, this); ReferenceCountUtil.safeRelease(frames); @@ -366,6 +372,8 @@ public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLas new CanceledException("Failed to reassemble payload. Cause: " + t.getMessage())); this.connection.sendFrame(streamId, errorFrame); + this.requesterResponderSupport.remove(streamId, this); + final RequestInterceptor requestInterceptor = this.requestInterceptor; if (requestInterceptor != null) { requestInterceptor.onTerminate(streamId, FrameType.REQUEST_STREAM, t); diff --git a/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java b/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java index 53d222605..568dada2e 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java +++ b/rsocket-core/src/main/java/io/rsocket/core/SendUtils.java @@ -40,11 +40,13 @@ final class SendUtils { private static final Consumer DROPPED_ELEMENTS_CONSUMER = data -> { - try { - ReferenceCounted referenceCounted = (ReferenceCounted) data; - referenceCounted.release(); - } catch (Throwable e) { - // ignored + if (data instanceof ReferenceCounted) { + try { + ReferenceCounted referenceCounted = (ReferenceCounted) data; + referenceCounted.release(); + } catch (Throwable e) { + // ignored + } } }; diff --git a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java index 2d367bd73..5aae22e89 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java @@ -60,6 +60,7 @@ void dispose() {} void sendError(DuplexConnection duplexConnection, RSocketErrorException exception) { duplexConnection.sendErrorAndClose(exception); + duplexConnection.receive().subscribe(); } static class DefaultServerSetup extends ServerSetup { @@ -78,7 +79,7 @@ public Mono acceptRSocketSetup( sendError(duplexConnection, new UnsupportedSetupException("resume not supported")); return duplexConnection.onClose(); } else { - return then.apply(new DefaultKeepAliveHandler(duplexConnection), duplexConnection); + return then.apply(new DefaultKeepAliveHandler(), duplexConnection); } } @@ -140,7 +141,7 @@ public Mono acceptRSocketSetup( resumableDuplexConnection, serverRSocketSession, serverRSocketSession), resumableDuplexConnection); } else { - return then.apply(new DefaultKeepAliveHandler(duplexConnection), duplexConnection); + return then.apply(new DefaultKeepAliveHandler(), duplexConnection); } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java index 2da572de3..3beedf97f 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java @@ -168,4 +168,9 @@ public void sendErrorAndClose(RSocketErrorException e) { public ByteBufAllocator alloc() { return source.alloc(); } + + @Override + public String toString() { + return "SetupHandlingDuplexConnection{" + "source=" + source + ", done=" + done + '}'; + } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java index 66d18c8a7..d581731a3 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.rsocket.frame; import io.netty.buffer.ByteBuf; @@ -99,8 +114,9 @@ private static ByteBuf getData(ByteBuf frame, FrameType frameType) { case REQUEST_CHANNEL: data = RequestChannelFrameCodec.data(frame); break; - // Payload and synthetic types + // Payload, KeepAlive and synthetic types case PAYLOAD: + case KEEPALIVE: case NEXT: case NEXT_COMPLETE: case COMPLETE: diff --git a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java index 09026356f..0296b0a07 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java @@ -30,9 +30,9 @@ public BaseDuplexConnection() {} @Override public void sendFrame(int streamId, ByteBuf frame) { if (streamId == 0) { - sender.onNextPrioritized(frame); + sender.tryEmitPrioritized(frame); } else { - sender.onNext(frame); + sender.tryEmitNormal(frame); } } diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java index 520ff318a..c96a7aed2 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -32,6 +32,7 @@ import reactor.core.Scannable; import reactor.core.publisher.Flux; import reactor.core.publisher.Operators; +import reactor.util.Logger; import reactor.util.annotation.Nullable; import reactor.util.concurrent.Queues; import reactor.util.context.Context; @@ -51,6 +52,7 @@ public final class UnboundedProcessor extends Flux final Queue queue; final Queue priorityQueue; final Runnable onFinalizedHook; + @Nullable final Logger logger; boolean cancelled; boolean done; @@ -91,16 +93,27 @@ public final class UnboundedProcessor extends Flux static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(UnboundedProcessor.class, "requested"); + ByteBuf last; + boolean outputFused; public UnboundedProcessor() { this(() -> {}); } + UnboundedProcessor(Logger logger) { + this(() -> {}, logger); + } + public UnboundedProcessor(Runnable onFinalizedHook) { + this(onFinalizedHook, null); + } + + UnboundedProcessor(Runnable onFinalizedHook, @Nullable Logger logger) { this.onFinalizedHook = onFinalizedHook; this.queue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); this.priorityQueue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); + this.logger = logger; } @Override @@ -121,78 +134,125 @@ public Object scanUnsafe(Attr key) { return null; } - public void onNextPrioritized(ByteBuf t) { + public boolean tryEmitPrioritized(ByteBuf t) { if (this.done || this.cancelled) { release(t); - return; + return false; } if (!this.priorityQueue.offer(t)) { onError(Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext())); release(t); - return; + return false; } final long previousState = markValueAdded(this); if (isFinalized(previousState)) { this.clearSafely(); - return; + return false; } if (isSubscriberReady(previousState)) { if (this.outputFused) { // fast path for fusion this.actual.onNext(null); - return; + return true; } if (isWorkInProgress(previousState)) { - return; + return true; } if (hasRequest(previousState)) { - drainRegular(previousState); + drainRegular((previousState | FLAG_HAS_VALUE) + 1); } } + return true; } - @Override - public void onNext(ByteBuf t) { + public boolean tryEmitNormal(ByteBuf t) { if (this.done || this.cancelled) { release(t); - return; + return false; } if (!this.queue.offer(t)) { onError(Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext())); release(t); - return; + return false; } final long previousState = markValueAdded(this); if (isFinalized(previousState)) { this.clearSafely(); - return; + return false; } if (isSubscriberReady(previousState)) { if (this.outputFused) { // fast path for fusion this.actual.onNext(null); - return; + return true; } if (isWorkInProgress(previousState)) { - return; + return true; } if (hasRequest(previousState)) { - drainRegular(previousState); + drainRegular((previousState | FLAG_HAS_VALUE) + 1); + } + } + + return true; + } + + public boolean tryEmitFinal(ByteBuf t) { + if (this.done || this.cancelled) { + release(t); + return false; + } + + this.last = t; + this.done = true; + + final long previousState = markValueAddedAndTerminated(this); + if (isFinalized(previousState)) { + this.clearSafely(); + return false; + } + + if (isSubscriberReady(previousState)) { + if (this.outputFused) { + // fast path for fusion + this.actual.onNext(null); + this.actual.onComplete(); + return true; + } + + if (isWorkInProgress(previousState)) { + return true; } + + drainRegular((previousState | FLAG_TERMINATED | FLAG_HAS_VALUE) + 1); } + + return true; + } + + @Deprecated + public void onNextPrioritized(ByteBuf t) { + tryEmitPrioritized(t); + } + + @Override + @Deprecated + public void onNext(ByteBuf t) { + tryEmitNormal(t); } @Override + @Deprecated public void onError(Throwable t) { if (this.done || this.cancelled) { Operators.onErrorDropped(t, currentContext()); @@ -228,13 +288,12 @@ public void onError(Throwable t) { return; } - if (hasRequest(previousState)) { - drainRegular(previousState); - } + drainRegular((previousState | FLAG_TERMINATED) + 1); } } @Override + @Deprecated public void onComplete() { if (this.done || this.cancelled) { return; @@ -266,18 +325,15 @@ public void onComplete() { return; } - if (hasRequest(previousState)) { - drainRegular(previousState); - } + drainRegular((previousState | FLAG_TERMINATED) + 1); } } - void drainRegular(long previousState) { + void drainRegular(long expectedState) { final CoreSubscriber a = this.actual; final Queue q = this.queue; final Queue pq = this.priorityQueue; - long expectedState = previousState + 1; for (; ; ) { long r = this.requested; @@ -299,7 +355,7 @@ void drainRegular(long previousState) { empty = t == null; } - if (checkTerminated(done, empty, a)) { + if (checkTerminated(done, empty, true, a)) { if (!empty) { release(t); } @@ -322,7 +378,7 @@ void drainRegular(long previousState) { done = this.done; empty = q.isEmpty() && pq.isEmpty(); - if (checkTerminated(done, empty, a)) { + if (checkTerminated(done, empty, false, a)) { return; } } @@ -349,7 +405,8 @@ void drainRegular(long previousState) { } } - boolean checkTerminated(boolean done, boolean empty, CoreSubscriber a) { + boolean checkTerminated( + boolean done, boolean empty, boolean hasDemand, CoreSubscriber a) { final long state = this.state; if (isCancelled(state)) { clearAndFinalize(this); @@ -363,6 +420,18 @@ boolean checkTerminated(boolean done, boolean empty, CoreSubscriber actual) { previousState = markSubscriberReady(this); + if (isSubscriberReady(previousState)) { + return; + } + if (this.outputFused) { if (isCancelled(previousState)) { return; @@ -466,7 +539,7 @@ public void subscribe(CoreSubscriber actual) { } if (hasRequest(previousState)) { - drainRegular(previousState); + drainRegular((previousState | FLAG_SUBSCRIBER_READY) + 1); } } @@ -492,7 +565,7 @@ public void request(long n) { } if (isSubscriberReady(previousState) && hasValue(previousState)) { - drainRegular(previousState); + drainRegular((previousState | FLAG_HAS_REQUEST) + 1); } } } @@ -515,6 +588,7 @@ public void cancel() { } @Override + @Deprecated public void dispose() { this.cancelled = true; @@ -553,7 +627,19 @@ public ByteBuf poll() { if (t != null) { return t; } - return this.queue.poll(); + + t = this.queue.poll(); + if (t != null) { + return t; + } + + t = this.last; + if (t != null) { + this.last = null; + return t; + } + + return null; } @Override @@ -598,6 +684,12 @@ void clearUnsafely() { final Queue queue = this.queue; final Queue priorityQueue = this.priorityQueue; + final ByteBuf last = this.last; + + if (last != null) { + release(last); + } + ByteBuf byteBuf; while ((byteBuf = queue.poll()) != null) { release(byteBuf); @@ -651,7 +743,9 @@ static long markSubscribedOnce(UnboundedProcessor instance) { return state; } - if (STATE.compareAndSet(instance, state, state | FLAG_SUBSCRIBED_ONCE)) { + final long nextState = state | FLAG_SUBSCRIBED_ONCE; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mso", state, nextState); return state; } } @@ -667,7 +761,10 @@ static long markSubscriberReady(UnboundedProcessor instance) { for (; ; ) { long state = instance.state; - if (isFinalized(state) || isCancelled(state) || isDisposed(state)) { + if (isFinalized(state) + || isCancelled(state) + || isDisposed(state) + || isSubscriberReady(state)) { return state; } @@ -678,7 +775,9 @@ static long markSubscriberReady(UnboundedProcessor instance) { } } - if (STATE.compareAndSet(instance, state, nextState | FLAG_SUBSCRIBER_READY)) { + nextState = nextState | FLAG_SUBSCRIBER_READY; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " msr", state, nextState); return state; } } @@ -700,11 +799,13 @@ static long markRequestAdded(UnboundedProcessor instance) { } long nextState = state; - if (isSubscriberReady(state) && hasValue(state)) { + if (isWorkInProgress(state) || (isSubscriberReady(state) && hasValue(state))) { nextState = addWork(state); } - if (STATE.compareAndSet(instance, state, nextState | FLAG_HAS_REQUEST)) { + nextState = nextState | FLAG_HAS_REQUEST; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mra", state, nextState); return state; } } @@ -739,7 +840,39 @@ static long markValueAdded(UnboundedProcessor instance) { } } - if (STATE.compareAndSet(instance, state, nextState | FLAG_HAS_VALUE)) { + nextState = nextState | FLAG_HAS_VALUE; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mva", state, nextState); + return state; + } + } + } + + /** + * Sets {@link #FLAG_HAS_VALUE} flag if it was not set before and if flags {@link + * #FLAG_FINALIZED}, {@link #FLAG_CANCELLED}, {@link #FLAG_DISPOSED} are unset. Also, this method + * increments number of work in progress (WIP) if {@link #FLAG_HAS_REQUEST} is set + * + * @return previous state + */ + static long markValueAddedAndTerminated(UnboundedProcessor instance) { + for (; ; ) { + final long state = instance.state; + + if (isFinalized(state)) { + return state; + } + + long nextState = state; + if (isWorkInProgress(state)) { + nextState = addWork(state); + } else if (isSubscriberReady(state) && !instance.outputFused) { + nextState = addWork(state); + } + + nextState = nextState | FLAG_HAS_VALUE | FLAG_TERMINATED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, "mva&t", state, nextState); return state; } } @@ -761,16 +894,20 @@ static long markTerminatedOrFinalized(UnboundedProcessor instance) { } long nextState = state; - if (isSubscriberReady(state) && !instance.outputFused) { + if (isWorkInProgress(state)) { + nextState = addWork(state); + } else if (isSubscriberReady(state) && !instance.outputFused) { if (!hasValue(state)) { // fast path for no values and no work in progress nextState = FLAG_FINALIZED; - } else if (hasRequest(state)) { + } else { nextState = addWork(state); } } - if (STATE.compareAndSet(instance, state, nextState | FLAG_TERMINATED)) { + nextState = nextState | FLAG_TERMINATED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mt|f", state, nextState); if (isFinalized(nextState)) { instance.onFinalizedHook.run(); } @@ -793,8 +930,9 @@ static long markCancelled(UnboundedProcessor instance) { return state; } - final long nextState = addWork(state); - if (STATE.compareAndSet(instance, state, nextState | FLAG_CANCELLED)) { + final long nextState = addWork(state) | FLAG_CANCELLED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mc", state, nextState); return state; } } @@ -815,8 +953,9 @@ static long markDisposed(UnboundedProcessor instance) { return state; } - final long nextState = addWork(state); - if (STATE.compareAndSet(instance, state, nextState | FLAG_DISPOSED)) { + final long nextState = addWork(state) | FLAG_DISPOSED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " md", state, nextState); return state; } } @@ -839,12 +978,10 @@ static long addWork(long state) { */ static long markWorkDone( UnboundedProcessor instance, long expectedState, boolean hasRequest, boolean hasValue) { - final long expectedMissed = expectedState & MAX_WIP_VALUE; for (; ; ) { final long state = instance.state; - final long missed = state & MAX_WIP_VALUE; - if (missed != expectedMissed) { + if (state != expectedState) { return state; } @@ -852,11 +989,12 @@ static long markWorkDone( return state; } - final long nextState = state - expectedMissed; - if (STATE.compareAndSet( - instance, - state, - nextState ^ (hasRequest ? 0 : FLAG_HAS_REQUEST) ^ (hasValue ? 0 : FLAG_HAS_VALUE))) { + final long nextState = + (state - (expectedState & MAX_WIP_VALUE)) + ^ (hasRequest ? 0 : FLAG_HAS_REQUEST) + ^ (hasValue ? 0 : FLAG_HAS_VALUE); + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " mwd", state, nextState); return nextState; } } @@ -885,8 +1023,9 @@ static void clearAndFinalize(UnboundedProcessor instance) { instance.clearUnsafely(); } - if (STATE.compareAndSet( - instance, state, (state & ~MAX_WIP_VALUE & ~FLAG_HAS_VALUE) | FLAG_FINALIZED)) { + long nextState = (state & ~MAX_WIP_VALUE & ~FLAG_HAS_VALUE) | FLAG_FINALIZED; + if (STATE.compareAndSet(instance, state, nextState)) { + log(instance, " c&f", state, nextState); instance.onFinalizedHook.run(); break; } @@ -928,4 +1067,101 @@ static boolean isSubscriberReady(long state) { static boolean isSubscribedOnce(long state) { return (state & FLAG_SUBSCRIBED_ONCE) == FLAG_SUBSCRIBED_ONCE; } + + static void log( + UnboundedProcessor instance, String action, long initialState, long committedState) { + log(instance, action, initialState, committedState, false); + } + + static void log( + UnboundedProcessor instance, + String action, + long initialState, + long committedState, + boolean logStackTrace) { + Logger logger = instance.logger; + if (logger == null || !logger.isTraceEnabled()) { + return; + } + + if (logStackTrace) { + logger.trace( + String.format( + "[%s][%s][%s][%s-%s]", + instance, + action, + action, + Thread.currentThread().getId(), + formatState(initialState, 64), + formatState(committedState, 64)), + new RuntimeException()); + } else { + logger.trace( + String.format( + "[%s][%s][%s][%s-%s]", + instance, + action, + Thread.currentThread().getId(), + formatState(initialState, 64), + formatState(committedState, 64))); + } + } + + static void log( + UnboundedProcessor instance, String action, int initialState, int committedState) { + log(instance, action, initialState, committedState, false); + } + + static void log( + UnboundedProcessor instance, + String action, + int initialState, + int committedState, + boolean logStackTrace) { + Logger logger = instance.logger; + if (logger == null || !logger.isTraceEnabled()) { + return; + } + + if (logStackTrace) { + logger.trace( + String.format( + "[%s][%s][%s][%s-%s]", + instance, + action, + action, + Thread.currentThread().getId(), + formatState(initialState, 32), + formatState(committedState, 32)), + new RuntimeException()); + } else { + logger.trace( + String.format( + "[%s][%s][%s][%s-%s]", + instance, + action, + Thread.currentThread().getId(), + formatState(initialState, 32), + formatState(committedState, 32))); + } + } + + static String formatState(long state, int size) { + final String defaultFormat = Long.toBinaryString(state); + final StringBuilder formatted = new StringBuilder(); + final int toPrepend = size - defaultFormat.length(); + for (int i = 0; i < size; i++) { + if (i != 0 && i % 4 == 0) { + formatted.append("_"); + } + if (i < toPrepend) { + formatted.append("0"); + } else { + formatted.append(defaultFormat.charAt(i - toPrepend)); + } + } + + formatted.insert(0, "0b"); + return formatted.toString(); + } } diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java index b92c25f46..4fd7a772d 100644 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java @@ -1,7 +1,6 @@ package io.rsocket.keepalive; import io.netty.buffer.ByteBuf; -import io.rsocket.Closeable; import io.rsocket.keepalive.KeepAliveSupport.KeepAlive; import io.rsocket.resume.RSocketSession; import io.rsocket.resume.ResumableDuplexConnection; @@ -16,18 +15,11 @@ KeepAliveFramesAcceptor start( Consumer onTimeout); class DefaultKeepAliveHandler implements KeepAliveHandler { - private final Closeable duplexConnection; - - public DefaultKeepAliveHandler(Closeable duplexConnection) { - this.duplexConnection = duplexConnection; - } - @Override public KeepAliveFramesAcceptor start( KeepAliveSupport keepAliveSupport, Consumer onSendKeepAliveFrame, Consumer onTimeout) { - duplexConnection.onClose().doFinally(s -> keepAliveSupport.stop()).subscribe(); return keepAliveSupport .onSendKeepAliveFrame(onSendKeepAliveFrame) .onTimeout(onTimeout) diff --git a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java index 21ea3d836..d59cbb86e 100644 --- a/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/loadbalance/LoadbalanceRSocketClient.java @@ -27,8 +27,8 @@ import reactor.util.annotation.Nullable; /** - * An implementation of {@link RSocketClient backed by a pool of {@code RSocket} instances and using a {@link - * LoadbalanceStrategy} to select the {@code RSocket} to use for a given request. + * An implementation of {@link RSocketClient} backed by a pool of {@code RSocket} instances and + * using a {@link LoadbalanceStrategy} to select the {@code RSocket} to use for a given request. * * @since 1.1 */ @@ -73,7 +73,7 @@ public Flux requestStream(Mono payloadMono) { @Override public Flux requestChannel(Publisher payloads) { - return rSocketPool.select().requestChannel(payloads); + return source().flatMapMany(rSocket -> rSocket.requestChannel(payloads)); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java index 2f2f29001..ca4f5dcb4 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -29,7 +29,6 @@ import io.rsocket.frame.ResumeOkFrameCodec; import io.rsocket.keepalive.KeepAliveSupport; import java.time.Duration; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Function; import org.reactivestreams.Subscription; @@ -79,31 +78,41 @@ public ClientRSocketSession( this.resumeToken = resumeToken; this.session = resumeToken.toString(CharsetUtil.UTF_8); this.connectionFactory = - connectionFactory.flatMap( - dc -> { - final long impliedPosition = resumableFramesStore.frameImpliedPosition(); - final long position = resumableFramesStore.framePosition(); - dc.sendFrame( - 0, - ResumeFrameCodec.encode( - dc.alloc(), - resumeToken.retain(), - // server uses this to release its cache - impliedPosition, // observed on the client side - // server uses this to check whether there is no mismatch - position // sent from the client sent - )); - - if (logger.isDebugEnabled()) { - logger.debug( - "Side[client]|Session[{}]. ResumeFrame[impliedPosition[{}], position[{}]] has been sent.", - session, - impliedPosition, - position); - } - - return connectionTransformer.apply(dc); - }); + connectionFactory + .doOnDiscard( + DuplexConnection.class, + c -> { + final ConnectionErrorException connectionErrorException = + new ConnectionErrorException("resumption_server=[Session Expired]"); + c.sendErrorAndClose(connectionErrorException); + c.receive().subscribe(); + }) + .flatMap( + dc -> { + final long impliedPosition = resumableFramesStore.frameImpliedPosition(); + final long position = resumableFramesStore.framePosition(); + dc.sendFrame( + 0, + ResumeFrameCodec.encode( + dc.alloc(), + resumeToken.retain(), + // server uses this to release its cache + impliedPosition, // observed on the client side + // server uses this to check whether there is no mismatch + position // sent from the client sent + )); + + if (logger.isDebugEnabled()) { + logger.debug( + "Side[client]|Session[{}]. ResumeFrame[impliedPosition[{}], position[{}]] has been sent.", + session, + impliedPosition, + position); + } + + return connectionTransformer.apply(dc); + }) + .doOnDiscard(Tuple2.class, this::tryReestablishSession); this.resumableFramesStore = resumableFramesStore; this.allocator = resumableDuplexConnection.alloc(); this.resumeSessionDuration = resumeSessionDuration; @@ -160,11 +169,20 @@ public void onImpliedPosition(long remoteImpliedPos) { @Override public void dispose() { - Operators.terminate(S, this); + if (logger.isDebugEnabled()) { + logger.debug("Side[client]|Session[{}]. Disposing", session); + } + + boolean result = Operators.terminate(S, this); + + if (logger.isDebugEnabled()) { + logger.debug("Side[client]|Session[{}]. Sessions[isDisposed={}]", session, result); + } reconnectDisposable.dispose(); resumableConnection.dispose(); - resumableFramesStore.dispose(); + // frame store is disposed by resumable connection + // resumableFramesStore.dispose(); if (resumeToken.refCnt() > 0) { resumeToken.release(); @@ -177,6 +195,9 @@ public boolean isDisposed() { } void tryReestablishSession(Tuple2 tuple2) { + if (logger.isDebugEnabled()) { + logger.debug("Active subscription is canceled {}", s == Operators.cancelledSubscription()); + } ByteBuf shouldBeResumeOKFrame = tuple2.getT1(); DuplexConnection nextDuplexConnection = tuple2.getT2(); @@ -189,9 +210,9 @@ void tryReestablishSession(Tuple2 tuple2) { } final ConnectionErrorException connectionErrorException = new ConnectionErrorException("RESUME_OK frame must be received before any others"); - resumableConnection.dispose(connectionErrorException); + resumableConnection.dispose(nextDuplexConnection, connectionErrorException); nextDuplexConnection.sendErrorAndClose(connectionErrorException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); throw connectionErrorException; // throw to retry connection again } @@ -227,10 +248,10 @@ void tryReestablishSession(Tuple2 tuple2) { } final ConnectionErrorException t = new ConnectionErrorException(e.getMessage(), e); - resumableConnection.dispose(t); + resumableConnection.dispose(nextDuplexConnection, t); nextDuplexConnection.sendErrorAndClose(t); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); return; } @@ -244,7 +265,7 @@ void tryReestablishSession(Tuple2 tuple2) { final ConnectionErrorException connectionErrorException = new ConnectionErrorException("resumption_server=[Session Expired]"); nextDuplexConnection.sendErrorAndClose(connectionErrorException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); return; } @@ -263,7 +284,7 @@ void tryReestablishSession(Tuple2 tuple2) { final ConnectionErrorException connectionErrorException = new ConnectionErrorException("resumption_server_pos=[Session Expired]"); nextDuplexConnection.sendErrorAndClose(connectionErrorException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); // no need to do anything since connection resumable connection is liklly to // be disposed } @@ -278,10 +299,10 @@ void tryReestablishSession(Tuple2 tuple2) { final ConnectionErrorException connectionErrorException = new ConnectionErrorException("resumption_server_pos=[" + remoteImpliedPos + "]"); - resumableConnection.dispose(connectionErrorException); + resumableConnection.dispose(nextDuplexConnection, connectionErrorException); nextDuplexConnection.sendErrorAndClose(connectionErrorException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); } } else if (frameType == FrameType.ERROR) { final RuntimeException exception = Exceptions.from(0, shouldBeResumeOKFrame); @@ -292,13 +313,14 @@ void tryReestablishSession(Tuple2 tuple2) { exception); } if (exception instanceof RejectedResumeException) { - resumableConnection.dispose(exception); + resumableConnection.dispose(nextDuplexConnection, exception); nextDuplexConnection.dispose(); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); return; } nextDuplexConnection.dispose(); + nextDuplexConnection.receive().subscribe(); throw exception; // assume retryable exception } else { if (logger.isDebugEnabled()) { @@ -309,10 +331,10 @@ void tryReestablishSession(Tuple2 tuple2) { final ConnectionErrorException connectionErrorException = new ConnectionErrorException("RESUME_OK frame must be received before any others"); - resumableConnection.dispose(connectionErrorException); + resumableConnection.dispose(nextDuplexConnection, connectionErrorException); nextDuplexConnection.sendErrorAndClose(connectionErrorException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); // no need to do anything since remote server rejected our connection completely } @@ -349,11 +371,7 @@ public void onError(Throwable t) { Operators.onErrorDropped(t, currentContext()); } - if (t instanceof TimeoutException) { - resumableConnection.dispose(); - } else { - resumableConnection.dispose(t); - } + resumableConnection.dispose(); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java index 87d82048d..e23bc154b 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java @@ -118,7 +118,7 @@ public class InMemoryResumableFramesStore extends Flux * the {@link InMemoryResumableFramesStore#drain(long)} method. */ static final long MAX_WORK_IN_PROGRESS = - 0b0000_0000_0000_0000_0000_0000_0000_0000_1111_1111_1111_1111_1111_1111_1111_1111L; + 0b0000_0000_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111L; public InMemoryResumableFramesStore(String side, ByteBuf session, int cacheSizeBytes) { this.side = side; @@ -168,9 +168,9 @@ void drain(long expectedState) { if (isConnected(expectedState)) { if (isTerminated(expectedState)) { - handleTerminal(this.terminal); + handleTerminated(qs, this.terminal); } else if (isDisposed()) { - handleTerminal(new CancellationException("Disposed")); + handleDisposed(); } else if (hasFrames(expectedState)) { handlePendingFrames(qs); } @@ -374,7 +374,7 @@ public void dispose() { return; } - drain(previousState | DISPOSED_FLAG); + drain((previousState + 1) | DISPOSED_FLAG); } void clearCache() { @@ -402,7 +402,17 @@ void handleFrame(ByteBuf frame) { handleConnectionFrame(frame); } - void handleTerminal(@Nullable Throwable t) { + void handleTerminated(Fuseable.QueueSubscription qs, @Nullable Throwable t) { + for (; ; ) { + final ByteBuf frame = qs.poll(); + final boolean empty = frame == null; + + if (empty) { + break; + } + + handleFrame(frame); + } if (t != null) { this.actual.onError(t); } else { @@ -410,6 +420,10 @@ void handleTerminal(@Nullable Throwable t) { } } + void handleDisposed() { + this.actual.onError(new CancellationException("Disposed")); + } + void handleConnectionFrame(ByteBuf frame) { this.actual.onNext(frame); } @@ -543,12 +557,13 @@ public void onNext(ByteBuf byteBuf) { return; } - if (isWorkInProgress(previousState) - || (!isConnected(previousState) && !hasPendingConnection(previousState))) { + if (isWorkInProgress(previousState)) { return; } - parent.drain(previousState + 1); + if (isConnected(previousState) || hasPendingConnection(previousState)) { + parent.drain((previousState + 1) | HAS_FRAME_FLAG); + } } @Override @@ -573,7 +588,7 @@ public void onError(Throwable t) { return; } - parent.drain(previousState | TERMINATED_FLAG); + parent.drain((previousState + 1) | TERMINATED_FLAG); } @Override @@ -595,7 +610,7 @@ public void onComplete() { return; } - parent.drain(previousState | TERMINATED_FLAG); + parent.drain((previousState + 1) | TERMINATED_FLAG); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java index f061857ff..c8811b9b3 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -21,8 +21,8 @@ import io.netty.util.CharsetUtil; import io.rsocket.DuplexConnection; import io.rsocket.RSocketErrorException; -import io.rsocket.exceptions.ConnectionCloseException; import io.rsocket.exceptions.ConnectionErrorException; +import io.rsocket.frame.ErrorFrameCodec; import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.internal.UnboundedProcessor; import java.net.SocketAddress; @@ -50,8 +50,8 @@ public class ResumableDuplexConnection extends Flux final ResumableFramesStore resumableFramesStore; final UnboundedProcessor savableFramesSender; - final Disposable framesSaverDisposable; - final Sinks.Empty onClose; + final Sinks.Empty onQueueClose; + final Sinks.Empty onLastConnectionClose; final SocketAddress remoteAddress; final Sinks.Many onConnectionClosedSink; @@ -79,11 +79,13 @@ public ResumableDuplexConnection( this.session = session.toString(CharsetUtil.UTF_8); this.onConnectionClosedSink = Sinks.unsafe().many().unicast().onBackpressureBuffer(); this.resumableFramesStore = resumableFramesStore; - this.savableFramesSender = new UnboundedProcessor(); - this.framesSaverDisposable = resumableFramesStore.saveFrames(savableFramesSender).subscribe(); - this.onClose = Sinks.empty(); + this.onQueueClose = Sinks.unsafe().empty(); + this.onLastConnectionClose = Sinks.unsafe().empty(); + this.savableFramesSender = new UnboundedProcessor(onQueueClose::tryEmitEmpty); this.remoteAddress = initialConnection.remoteAddress(); + resumableFramesStore.saveFrames(savableFramesSender).subscribe(); + ACTIVE_CONNECTION.lazySet(this, initialConnection); } @@ -92,7 +94,10 @@ public boolean connect(DuplexConnection nextConnection) { if (activeConnection != DisposedConnection.INSTANCE && ACTIVE_CONNECTION.compareAndSet(this, activeConnection, nextConnection)) { - activeConnection.dispose(); + if (!activeConnection.isDisposed()) { + activeConnection.sendErrorAndClose( + new ConnectionErrorException("Connection unexpectedly replaced")); + } initConnection(nextConnection); @@ -120,10 +125,16 @@ void initConnection(DuplexConnection nextConnection) { .resumeStream() .subscribe( f -> nextConnection.sendFrame(FrameHeaderCodec.streamId(f), f), - t -> sendErrorAndClose(new ConnectionErrorException(t.getMessage())), - () -> - sendErrorAndClose( - new ConnectionCloseException("Connection Closed Unexpectedly"))); + t -> { + dispose(nextConnection, t); + nextConnection.sendErrorAndClose(new ConnectionErrorException(t.getMessage(), t)); + }, + () -> { + final ConnectionErrorException e = + new ConnectionErrorException("Connection Closed Unexpectedly"); + dispose(nextConnection, e); + nextConnection.sendErrorAndClose(e); + }); nextConnection.receive().subscribe(frameReceivingSubscriber); nextConnection .onClose() @@ -153,7 +164,7 @@ void initConnection(DuplexConnection nextConnection) { public void disconnect() { final DuplexConnection activeConnection = this.activeConnection; - if (activeConnection != DisposedConnection.INSTANCE) { + if (activeConnection != DisposedConnection.INSTANCE && !activeConnection.isDisposed()) { activeConnection.dispose(); } } @@ -161,9 +172,9 @@ public void disconnect() { @Override public void sendFrame(int streamId, ByteBuf frame) { if (streamId == 0) { - savableFramesSender.onNextPrioritized(frame); + savableFramesSender.tryEmitPrioritized(frame); } else { - savableFramesSender.onNext(frame); + savableFramesSender.tryEmitNormal(frame); } } @@ -184,32 +195,25 @@ public void sendErrorAndClose(RSocketErrorException rSocketErrorException) { return; } - activeConnection.sendErrorAndClose(rSocketErrorException); + savableFramesSender.tryEmitFinal( + ErrorFrameCodec.encode(activeConnection.alloc(), 0, rSocketErrorException)); + activeConnection .onClose() .subscribe( null, t -> { - framesSaverDisposable.dispose(); - activeReceivingSubscriber.dispose(); - savableFramesSender.onComplete(); - savableFramesSender.cancel(); onConnectionClosedSink.tryEmitComplete(); - - onClose.tryEmitError(t); + onLastConnectionClose.tryEmitEmpty(); }, () -> { - framesSaverDisposable.dispose(); - activeReceivingSubscriber.dispose(); - savableFramesSender.onComplete(); - savableFramesSender.cancel(); onConnectionClosedSink.tryEmitComplete(); final Throwable cause = rSocketErrorException.getCause(); if (cause == null) { - onClose.tryEmitEmpty(); + onLastConnectionClose.tryEmitEmpty(); } else { - onClose.tryEmitError(cause); + onLastConnectionClose.tryEmitError(cause); } }); } @@ -226,50 +230,66 @@ public ByteBufAllocator alloc() { @Override public Mono onClose() { - return onClose.asMono(); + return Mono.whenDelayError( + onQueueClose.asMono(), resumableFramesStore.onClose(), onLastConnectionClose.asMono()); } @Override public void dispose() { - dispose(null); - } - - void dispose(@Nullable Throwable e) { final DuplexConnection activeConnection = ACTIVE_CONNECTION.getAndSet(this, DisposedConnection.INSTANCE); if (activeConnection == DisposedConnection.INSTANCE) { return; } - - if (activeConnection != null) { - activeConnection.dispose(); - } - - if (logger.isDebugEnabled()) { - logger.debug( - "Side[{}]|Session[{}]|DuplexConnection[{}]. Disposing...", - side, - session, - connectionIndex); - } - - framesSaverDisposable.dispose(); - activeReceivingSubscriber.dispose(); savableFramesSender.onComplete(); - savableFramesSender.cancel(); - onConnectionClosedSink.tryEmitComplete(); + activeConnection + .onClose() + .subscribe( + null, + t -> { + onConnectionClosedSink.tryEmitComplete(); + onLastConnectionClose.tryEmitEmpty(); + }, + () -> { + onConnectionClosedSink.tryEmitComplete(); + onLastConnectionClose.tryEmitEmpty(); + }); + } - if (e != null) { - onClose.tryEmitError(e); - } else { - onClose.tryEmitEmpty(); + void dispose(DuplexConnection nextConnection, @Nullable Throwable e) { + final DuplexConnection activeConnection = + ACTIVE_CONNECTION.getAndSet(this, DisposedConnection.INSTANCE); + if (activeConnection == DisposedConnection.INSTANCE) { + return; } + savableFramesSender.onComplete(); + nextConnection + .onClose() + .subscribe( + null, + t -> { + if (e != null) { + onLastConnectionClose.tryEmitError(e); + } else { + onLastConnectionClose.tryEmitEmpty(); + } + onConnectionClosedSink.tryEmitComplete(); + }, + () -> { + if (e != null) { + onLastConnectionClose.tryEmitError(e); + } else { + onLastConnectionClose.tryEmitEmpty(); + } + onConnectionClosedSink.tryEmitComplete(); + }); } @Override @SuppressWarnings("ConstantConditions") public boolean isDisposed() { - return onClose.scan(Scannable.Attr.TERMINATED) || onClose.scan(Scannable.Attr.CANCELLED); + return onQueueClose.scan(Scannable.Attr.TERMINATED) + || onQueueClose.scan(Scannable.Attr.CANCELLED); } @Override @@ -280,6 +300,7 @@ public SocketAddress remoteAddress() { @Override public void request(long n) { if (state == 1 && STATE.compareAndSet(this, 1, 2)) { + // happens for the very first time with the initial connection initConnection(this.activeConnection); } } @@ -301,6 +322,26 @@ static boolean isResumableFrame(ByteBuf frame) { return FrameHeaderCodec.streamId(frame) != 0; } + @Override + public String toString() { + return "ResumableDuplexConnection{" + + "side='" + + side + + '\'' + + ", session='" + + session + + '\'' + + ", remoteAddress=" + + remoteAddress + + ", state=" + + state + + ", activeConnection=" + + activeConnection + + ", connectionIndex=" + + connectionIndex + + '}'; + } + private static final class DisposedConnection implements DuplexConnection { static final DisposedConnection INSTANCE = new DisposedConnection(); diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java index 83c5bf8c1..ad1b38375 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -138,7 +138,7 @@ void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplex final RejectedResumeException rejectedResumeException = new RejectedResumeException("resume_internal_error: Session Expired"); nextDuplexConnection.sendErrorAndClose(rejectedResumeException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); return; } @@ -180,7 +180,7 @@ void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplex final RejectedResumeException rejectedResumeException = new RejectedResumeException(t.getMessage(), t); nextDuplexConnection.sendErrorAndClose(rejectedResumeException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); return; } @@ -200,7 +200,7 @@ void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplex final RejectedResumeException rejectedResumeException = new RejectedResumeException("resume_internal_error: Session Expired"); nextDuplexConnection.sendErrorAndClose(rejectedResumeException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); // resumableConnection is likely to be disposed at this stage. Thus we have // nothing to do @@ -224,7 +224,7 @@ void doResume(long remotePos, long remoteImpliedPos, DuplexConnection nextDuplex "resumption_pos=[ remote: { pos: %d, impliedPos: %d }, local: { pos: %d, impliedPos: %d }]", remotePos, remoteImpliedPos, position, impliedPosition)); nextDuplexConnection.sendErrorAndClose(rejectedResumeException); - nextDuplexConnection.receive().subscribe().dispose(); + nextDuplexConnection.receive().subscribe(); } } @@ -287,9 +287,11 @@ public void setKeepAliveSupport(KeepAliveSupport keepAliveSupport) { @Override public void dispose() { + if (logger.isDebugEnabled()) { + logger.debug("Side[server]|Session[{}]. Disposing session", session); + } Operators.terminate(S, this); resumableConnection.dispose(); - resumableFramesStore.dispose(); } @Override diff --git a/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java b/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java index 04c9e4bff..1db708ab5 100644 --- a/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java +++ b/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java @@ -5,20 +5,25 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; +import io.netty.util.IllegalReferenceCountException; import io.netty.util.ResourceLeakDetector; import java.lang.reflect.Field; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import org.assertj.core.api.Assertions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Additional Utils which allows to decorate a ByteBufAllocator and track/assertOnLeaks all created * ByteBuffs */ public class LeaksTrackingByteBufAllocator implements ByteBufAllocator { + static final Logger LOGGER = LoggerFactory.getLogger(LeaksTrackingByteBufAllocator.class); /** * Allows to instrument any given the instance of ByteBufAllocator @@ -83,6 +88,7 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { return this; } + LOGGER.debug(tag + " await buffers to be released"); for (int i = 0; i < 100; i++) { System.gc(); parkNanos(1000); @@ -91,22 +97,31 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { } } - Assertions.assertThat(unreleased) - .allMatch( - bb -> { - final boolean checkResult = bb.refCnt() == 0; - - if (!checkResult) { - try { - System.out.println(tag + " " + resolveTrackingInfo(bb)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - return checkResult; - }, - tag); + Set collected = new HashSet<>(); + for (ByteBuf buf : unreleased) { + if (buf.refCnt() != 0) { + try { + collected.add(buf); + } catch (IllegalReferenceCountException ignored) { + // fine to ignore if throws because of refCnt + } + } + } + + Assertions.assertThat( + collected + .stream() + .filter(bb -> bb.refCnt() != 0) + .peek( + bb -> { + try { + LOGGER.debug(tag + " " + resolveTrackingInfo(bb)); + } catch (Exception e) { + e.printStackTrace(); + } + })) + .describedAs("[" + tag + "] all buffers expected to be released but got ") + .isEmpty(); } finally { tracker.clear(); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java index a3e5a62ff..310e15b3e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java +++ b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java @@ -44,12 +44,12 @@ public void init() { } protected void doInit() { - if (socket != null) { - socket.dispose(); - } if (connection != null) { connection.dispose(); } + if (socket != null) { + socket.dispose(); + } connection = new TestDuplexConnection(allocator); socket = newRSocket(); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java b/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java index c9ecb6eb6..195df9434 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ClientServerInputMultiplexerTest.java @@ -56,12 +56,20 @@ public void clientSplits() { clientMultiplexer .asClientConnection() .receive() - .doOnNext(f -> clientFrames.incrementAndGet()) + .doOnNext( + f -> { + clientFrames.incrementAndGet(); + f.release(); + }) .subscribe(); clientMultiplexer .asServerConnection() .receive() - .doOnNext(f -> serverFrames.incrementAndGet()) + .doOnNext( + f -> { + serverFrames.incrementAndGet(); + f.release(); + }) .subscribe(); source.addToReceivedBuffer(errorFrame(1).retain()); @@ -101,12 +109,20 @@ public void serverSplits() { serverMultiplexer .asClientConnection() .receive() - .doOnNext(f -> clientFrames.incrementAndGet()) + .doOnNext( + f -> { + clientFrames.incrementAndGet(); + f.release(); + }) .subscribe(); serverMultiplexer .asServerConnection() .receive() - .doOnNext(f -> serverFrames.incrementAndGet()) + .doOnNext( + f -> { + serverFrames.incrementAndGet(); + f.release(); + }) .subscribe(); source.addToReceivedBuffer(errorFrame(1).retain()); diff --git a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java index 3c95fd65c..84576e6ce 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/DefaultRSocketClientTests.java @@ -19,6 +19,7 @@ import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; +import io.rsocket.FrameAssert; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.RaceTestConstants; @@ -38,6 +39,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import org.assertj.core.api.Assertions; @@ -48,6 +50,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; import org.reactivestreams.Publisher; import reactor.core.Disposable; import reactor.core.publisher.Flux; @@ -78,6 +81,26 @@ public void setUp() throws Throwable { public void tearDown() { Hooks.resetOnErrorDropped(); Hooks.resetOnNextDropped(); + rule.allocator.assertHasNoLeaks(); + } + + @Test + @SuppressWarnings("unchecked") + void discardElementsConsumerShouldAcceptOtherTypesThanReferenceCounted() { + Consumer discardElementsConsumer = DefaultRSocketClient.DISCARD_ELEMENTS_CONSUMER; + discardElementsConsumer.accept(new Object()); + } + + @Test + void droppedElementsConsumerReleaseReference() { + ReferenceCounted referenceCounted = Mockito.mock(ReferenceCounted.class); + Mockito.when(referenceCounted.release()).thenReturn(true); + Mockito.when(referenceCounted.refCnt()).thenReturn(1); + + Consumer discardElementsConsumer = DefaultRSocketClient.DISCARD_ELEMENTS_CONSUMER; + discardElementsConsumer.accept(referenceCounted); + + Mockito.verify(referenceCounted).release(); } static Stream interactions() { @@ -434,6 +457,8 @@ public void shouldBeAbleToResolveOriginalSource() { assertSubscriber1.assertTerminated().assertValueCount(1); Assertions.assertThat(assertSubscriber1.values()).isEqualTo(assertSubscriber.values()); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -457,6 +482,13 @@ public void shouldDisposeOriginalSource() { .assertErrorMessage("Disposed"); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -494,6 +526,13 @@ public Mono onClose() { onCloseSubscriber.assertTerminated().assertComplete(); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -515,6 +554,13 @@ public void shouldResolveOnStartSource() { assertSubscriber1.assertTerminated().assertComplete(); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -536,6 +582,13 @@ public void shouldNotStartIfAlreadyDisposed() { assertSubscriber1.assertTerminated().assertComplete(); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -553,6 +606,11 @@ public void shouldBeRestartedIfSourceWasClosed() { rule.socket.dispose(); + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + terminateSubscriber.assertNotTerminated(); Assertions.assertThat(rule.client.isDisposed()).isFalse(); @@ -576,6 +634,13 @@ public void shouldBeRestartedIfSourceWasClosed() { Assertions.assertThat(rule.client.connect()).isFalse(); Assertions.assertThat(rule.socket.isDisposed()).isTrue(); + + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + + rule.allocator.assertHasNoLeaks(); } @Test @@ -603,6 +668,13 @@ public void shouldDisposeOriginalSourceIfRacing() { .assertTerminated() .assertError(CancellationException.class) .assertErrorMessage("Disposed"); + + ByteBuf buf; + while ((buf = rule.connection.pollFrame()) != null) { + FrameAssert.assertThat(buf).hasStreamIdZero().hasData("Disposed").hasNoLeaks(); + } + + rule.allocator.assertHasNoLeaks(); } } @@ -632,8 +704,14 @@ public void shouldStartOriginalSourceOnceIfRacing() { AssertSubscriber assertSubscriber1 = AssertSubscriber.create(); rule.client.onClose().subscribe(assertSubscriber1); + FrameAssert.assertThat(rule.connection.awaitFrame()) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); assertSubscriber1.assertTerminated().assertComplete(); + + rule.allocator.assertHasNoLeaks(); } } @@ -643,6 +721,8 @@ public static class ClientSocketRule extends AbstractSocketRule { protected Runnable delayer; protected Sinks.One producer; + protected Sinks.Empty thisClosedSink; + @Override protected void doInit() { super.doInit(); @@ -660,6 +740,7 @@ protected void doInit() { @Override protected RSocket newRSocket() { + this.thisClosedSink = Sinks.empty(); return new RSocketRequester( connection, PayloadDecoder.ZERO_COPY, @@ -671,7 +752,9 @@ protected RSocket newRSocket() { Integer.MAX_VALUE, null, __ -> null, - null); + null, + thisClosedSink, + thisClosedSink.asMono()); } } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java index 5bd5f9999..5be59235c 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java @@ -1,376 +1,420 @@ -/// * -// * Copyright 2015-2019 the original author or authors. -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ -// -// package io.rsocket.core; -// -// import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; -// import static io.rsocket.keepalive.KeepAliveHandler.DefaultKeepAliveHandler; -// import static io.rsocket.keepalive.KeepAliveHandler.ResumableKeepAliveHandler; -// -// import io.netty.buffer.ByteBuf; -// import io.netty.buffer.ByteBufAllocator; -// import io.netty.buffer.Unpooled; -// import io.rsocket.RSocket; -// import io.rsocket.buffer.LeaksTrackingByteBufAllocator; -// import io.rsocket.exceptions.ConnectionErrorException; -// import io.rsocket.frame.FrameHeaderCodec; -// import io.rsocket.frame.FrameType; -// import io.rsocket.frame.KeepAliveFrameCodec; -// import io.rsocket.core.RequesterLeaseHandler; -// import io.rsocket.resume.InMemoryResumableFramesStore; -//// import io.rsocket.resume.ResumableDuplexConnection; -// import io.rsocket.test.util.TestDuplexConnection; -// import io.rsocket.util.DefaultPayload; -// import java.time.Duration; -// import org.assertj.core.api.Assertions; -// import org.junit.jupiter.api.AfterEach; -// import org.junit.jupiter.api.BeforeEach; -// import org.junit.jupiter.api.Test; -// import reactor.core.Disposable; -// import reactor.core.publisher.Flux; -// import reactor.core.publisher.Mono; -// import reactor.test.StepVerifier; -// import reactor.test.scheduler.VirtualTimeScheduler; -// -// public class KeepAliveTest { -// private static final int KEEP_ALIVE_INTERVAL = 100; -// private static final int KEEP_ALIVE_TIMEOUT = 1000; -// private static final int RESUMABLE_KEEP_ALIVE_TIMEOUT = 200; -// -// VirtualTimeScheduler virtualTimeScheduler; -// -// @BeforeEach -// public void setUp() { -// virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); -// } -// -// @AfterEach -// public void tearDown() { -// VirtualTimeScheduler.reset(); -// } -// -// static RSocketState requester(int tickPeriod, int timeout) { -// LeaksTrackingByteBufAllocator allocator = -// LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); -// TestDuplexConnection connection = new TestDuplexConnection(allocator); -// RSocketRequester rSocket = -// new RSocketRequester( -// connection, -// DefaultPayload::create, -// StreamIdSupplier.clientSupplier(), -// 0, -// FRAME_LENGTH_MASK, -// Integer.MAX_VALUE, -// tickPeriod, -// timeout, -// new DefaultKeepAliveHandler(connection), -// RequesterLeaseHandler.None); -// return new RSocketState(rSocket, allocator, connection); -// } -// -// static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { -// LeaksTrackingByteBufAllocator allocator = -// LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); -// TestDuplexConnection connection = new TestDuplexConnection(allocator); -//// ResumableDuplexConnection resumableConnection = -//// new ResumableDuplexConnection( -//// "test", -//// connection, -//// new InMemoryResumableFramesStore("test", 10_000), -//// Duration.ofSeconds(10), -//// false); -// -// RSocketRequester rSocket = -// new RSocketRequester( -// resumableConnection, -// DefaultPayload::create, -// StreamIdSupplier.clientSupplier(), -// 0, -// FRAME_LENGTH_MASK, -// Integer.MAX_VALUE, -// tickPeriod, -// timeout, -// new ResumableKeepAliveHandler(resumableConnection), -// RequesterLeaseHandler.None); -// return new ResumableRSocketState(rSocket, connection, resumableConnection, allocator); -// } -// -// @Test -// void rSocketNotDisposedOnPresentKeepAlives() { -// RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// -// TestDuplexConnection connection = requesterState.connection(); -// -// Disposable disposable = -// Flux.interval(Duration.ofMillis(KEEP_ALIVE_INTERVAL)) -// .subscribe( -// n -> -// connection.addToReceivedBuffer( -// KeepAliveFrameCodec.encode( -// requesterState.allocator, true, 0, Unpooled.EMPTY_BUFFER))); -// -// virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_TIMEOUT * 2)); -// -// RSocket rSocket = requesterState.rSocket(); -// -// Assertions.assertThat(rSocket.isDisposed()).isFalse(); -// -// disposable.dispose(); -// -// requesterState.connection.dispose(); -// requesterState.rSocket.dispose(); -// -// Assertions.assertThat(requesterState.connection.getSent()).allMatch(ByteBuf::release); -// -// requesterState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void noKeepAlivesSentAfterRSocketDispose() { -// RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// -// requesterState.rSocket().dispose(); -// -// Duration duration = Duration.ofMillis(500); -// -// StepVerifier.create(Flux.from(requesterState.connection().getSentAsPublisher()).take(duration)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) -// .expectComplete() -// .verify(Duration.ofSeconds(1)); -// -// requesterState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void rSocketDisposedOnMissingKeepAlives() { -// RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// -// RSocket rSocket = requesterState.rSocket(); -// -// virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_TIMEOUT * 2)); -// -// Assertions.assertThat(rSocket.isDisposed()).isTrue(); -// rSocket -// .onClose() -// .as(StepVerifier::create) -// .expectError(ConnectionErrorException.class) -// .verify(Duration.ofMillis(100)); -// -// Assertions.assertThat(requesterState.connection.getSent()).allMatch(ByteBuf::release); -// -// requesterState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void clientRequesterSendsKeepAlives() { -// RSocketState RSocketState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// TestDuplexConnection connection = RSocketState.connection(); -// -// StepVerifier.create(Flux.from(connection.getSentAsPublisher()).take(3)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) -// .expectNextMatches(this::keepAliveFrameWithRespondFlag) -// .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) -// .expectNextMatches(this::keepAliveFrameWithRespondFlag) -// .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) -// .expectNextMatches(this::keepAliveFrameWithRespondFlag) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// RSocketState.rSocket.dispose(); -// RSocketState.connection.dispose(); -// -// RSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void requesterRespondsToKeepAlives() { -// RSocketState rSocketState = requester(100_000, 100_000); -// TestDuplexConnection connection = rSocketState.connection(); -// Duration duration = Duration.ofMillis(100); -// Mono.delay(duration) -// .subscribe( -// l -> -// connection.addToReceivedBuffer( -// KeepAliveFrameCodec.encode( -// rSocketState.allocator, true, 0, Unpooled.EMPTY_BUFFER))); -// -// StepVerifier.create(Flux.from(connection.getSentAsPublisher()).take(1)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) -// .expectNextMatches(this::keepAliveFrameWithoutRespondFlag) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// rSocketState.rSocket.dispose(); -// rSocketState.connection.dispose(); -// -// rSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void resumableRequesterNoKeepAlivesAfterDisconnect() { -// ResumableRSocketState rSocketState = -// resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// TestDuplexConnection testConnection = rSocketState.connection(); -// ResumableDuplexConnection resumableDuplexConnection = -// rSocketState.resumableDuplexConnection(); -// -// resumableDuplexConnection.disconnect(); -// -// Duration duration = Duration.ofMillis(500); -// StepVerifier.create(Flux.from(testConnection.getSentAsPublisher()).take(duration)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// rSocketState.rSocket.dispose(); -// rSocketState.connection.dispose(); -// -// rSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void resumableRequesterKeepAlivesAfterReconnect() { -// ResumableRSocketState rSocketState = -// resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// ResumableDuplexConnection resumableDuplexConnection = -// rSocketState.resumableDuplexConnection(); -// resumableDuplexConnection.disconnect(); -// TestDuplexConnection newTestConnection = new TestDuplexConnection(rSocketState.alloc()); -// resumableDuplexConnection.reconnect(newTestConnection); -// resumableDuplexConnection.resume(0, 0, ignored -> Mono.empty()); -// -// StepVerifier.create(Flux.from(newTestConnection.getSentAsPublisher()).take(1)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL))) -// .expectNextMatches(frame -> keepAliveFrame(frame) && frame.release()) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// rSocketState.rSocket.dispose(); -// rSocketState.connection.dispose(); -// -// rSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void resumableRequesterNoKeepAlivesAfterDispose() { -// ResumableRSocketState rSocketState = -// resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); -// rSocketState.rSocket().dispose(); -// Duration duration = Duration.ofMillis(500); -// StepVerifier.create(Flux.from(rSocketState.connection().getSentAsPublisher()).take(duration)) -// .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) -// .expectComplete() -// .verify(Duration.ofSeconds(5)); -// -// rSocketState.rSocket.dispose(); -// rSocketState.connection.dispose(); -// -// rSocketState.allocator.assertHasNoLeaks(); -// } -// -// @Test -// void resumableRSocketsNotDisposedOnMissingKeepAlives() throws InterruptedException { -// ResumableRSocketState resumableRequesterState = -// resumableRequester(KEEP_ALIVE_INTERVAL, RESUMABLE_KEEP_ALIVE_TIMEOUT); -// RSocket rSocket = resumableRequesterState.rSocket(); -// TestDuplexConnection connection = resumableRequesterState.connection(); -// -// virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(500)); -// -// Assertions.assertThat(rSocket.isDisposed()).isFalse(); -// Assertions.assertThat(connection.isDisposed()).isTrue(); -// -// -// Assertions.assertThat(resumableRequesterState.connection.getSent()).allMatch(ByteBuf::release); -// -// resumableRequesterState.connection.dispose(); -// resumableRequesterState.rSocket.dispose(); -// -// resumableRequesterState.allocator.assertHasNoLeaks(); -// } -// -// private boolean keepAliveFrame(ByteBuf frame) { -// return FrameHeaderCodec.frameType(frame) == FrameType.KEEPALIVE; -// } -// -// private boolean keepAliveFrameWithRespondFlag(ByteBuf frame) { -// return keepAliveFrame(frame) && KeepAliveFrameCodec.respondFlag(frame) && frame.release(); -// } -// -// private boolean keepAliveFrameWithoutRespondFlag(ByteBuf frame) { -// return keepAliveFrame(frame) && !KeepAliveFrameCodec.respondFlag(frame) && frame.release(); -// } -// -// static class RSocketState { -// private final RSocket rSocket; -// private final TestDuplexConnection connection; -// private final LeaksTrackingByteBufAllocator allocator; -// -// public RSocketState( -// RSocket rSocket, LeaksTrackingByteBufAllocator allocator, TestDuplexConnection connection) -// { -// this.rSocket = rSocket; -// this.connection = connection; -// this.allocator = allocator; -// } -// -// public TestDuplexConnection connection() { -// return connection; -// } -// -// public RSocket rSocket() { -// return rSocket; -// } -// -// public LeaksTrackingByteBufAllocator alloc() { -// return allocator; -// } -// } -// -// static class ResumableRSocketState { -// private final RSocket rSocket; -// private final TestDuplexConnection connection; -// private final ResumableDuplexConnection resumableDuplexConnection; -// private final LeaksTrackingByteBufAllocator allocator; -// -// public ResumableRSocketState( -// RSocket rSocket, -// TestDuplexConnection connection, -// ResumableDuplexConnection resumableDuplexConnection, -// LeaksTrackingByteBufAllocator allocator) { -// this.rSocket = rSocket; -// this.connection = connection; -// this.resumableDuplexConnection = resumableDuplexConnection; -// this.allocator = allocator; -// } -// -// public TestDuplexConnection connection() { -// return connection; -// } -// -// public ResumableDuplexConnection resumableDuplexConnection() { -// return resumableDuplexConnection; -// } -// -// public RSocket rSocket() { -// return rSocket; -// } -// -// public LeaksTrackingByteBufAllocator alloc() { -// return allocator; -// } -// } -// } +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.core; + +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; +import static io.rsocket.keepalive.KeepAliveHandler.DefaultKeepAliveHandler; +import static io.rsocket.keepalive.KeepAliveHandler.ResumableKeepAliveHandler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.FrameAssert; +import io.rsocket.RSocket; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.exceptions.ConnectionErrorException; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.KeepAliveFrameCodec; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.resume.InMemoryResumableFramesStore; +import io.rsocket.resume.RSocketSession; +import io.rsocket.resume.ResumableDuplexConnection; +import io.rsocket.resume.ResumeStateHolder; +import io.rsocket.test.util.TestDuplexConnection; +import java.time.Duration; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; +import reactor.test.StepVerifier; +import reactor.test.scheduler.VirtualTimeScheduler; + +public class KeepAliveTest { + private static final int KEEP_ALIVE_INTERVAL = 100; + private static final int KEEP_ALIVE_TIMEOUT = 1000; + private static final int RESUMABLE_KEEP_ALIVE_TIMEOUT = 200; + + VirtualTimeScheduler virtualTimeScheduler; + + @BeforeEach + public void setUp() { + virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); + } + + @AfterEach + public void tearDown() { + VirtualTimeScheduler.reset(); + } + + static RSocketState requester(int tickPeriod, int timeout) { + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + TestDuplexConnection connection = new TestDuplexConnection(allocator); + Sinks.Empty empty = Sinks.empty(); + RSocketRequester rSocket = + new RSocketRequester( + connection, + PayloadDecoder.ZERO_COPY, + StreamIdSupplier.clientSupplier(), + 0, + FRAME_LENGTH_MASK, + Integer.MAX_VALUE, + tickPeriod, + timeout, + new DefaultKeepAliveHandler(), + r -> null, + null, + empty, + empty.asMono()); + return new RSocketState(rSocket, allocator, connection, empty); + } + + static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + TestDuplexConnection connection = new TestDuplexConnection(allocator); + ResumableDuplexConnection resumableConnection = + new ResumableDuplexConnection( + "test", + Unpooled.EMPTY_BUFFER, + connection, + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 10_000)); + Sinks.Empty onClose = Sinks.empty(); + + RSocketRequester rSocket = + new RSocketRequester( + resumableConnection, + PayloadDecoder.ZERO_COPY, + StreamIdSupplier.clientSupplier(), + 0, + FRAME_LENGTH_MASK, + Integer.MAX_VALUE, + tickPeriod, + timeout, + new ResumableKeepAliveHandler( + resumableConnection, + Mockito.mock(RSocketSession.class), + Mockito.mock(ResumeStateHolder.class)), + __ -> null, + null, + onClose, + onClose.asMono()); + return new ResumableRSocketState(rSocket, connection, resumableConnection, onClose, allocator); + } + + @Test + void rSocketNotDisposedOnPresentKeepAlives() { + RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + + TestDuplexConnection connection = requesterState.connection(); + + Disposable disposable = + Flux.interval(Duration.ofMillis(KEEP_ALIVE_INTERVAL)) + .subscribe( + n -> + connection.addToReceivedBuffer( + KeepAliveFrameCodec.encode( + requesterState.allocator, true, 0, Unpooled.EMPTY_BUFFER))); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_TIMEOUT * 2)); + + RSocket rSocket = requesterState.rSocket(); + + Assertions.assertThat(rSocket.isDisposed()).isFalse(); + + disposable.dispose(); + + requesterState.connection.dispose(); + requesterState.rSocket.dispose(); + + Assertions.assertThat(requesterState.connection.getSent()).allMatch(ByteBuf::release); + + requesterState.allocator.assertHasNoLeaks(); + } + + @Test + void noKeepAlivesSentAfterRSocketDispose() { + RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + + requesterState.rSocket().dispose(); + + Duration duration = Duration.ofMillis(500); + + virtualTimeScheduler.advanceTimeBy(duration); + + FrameAssert.assertThat(requesterState.connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasData("Disposed") + .hasNoLeaks(); + FrameAssert.assertThat(requesterState.connection.pollFrame()).isNull(); + requesterState.allocator.assertHasNoLeaks(); + } + + @Test + void rSocketDisposedOnMissingKeepAlives() { + RSocketState requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + + RSocket rSocket = requesterState.rSocket(); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_TIMEOUT * 2)); + + Assertions.assertThat(rSocket.isDisposed()).isTrue(); + rSocket + .onClose() + .as(StepVerifier::create) + .expectError(ConnectionErrorException.class) + .verify(Duration.ofMillis(100)); + + Assertions.assertThat(requesterState.connection.getSent()).allMatch(ByteBuf::release); + + requesterState.allocator.assertHasNoLeaks(); + } + + @Test + void clientRequesterSendsKeepAlives() { + RSocketState RSocketState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + TestDuplexConnection connection = RSocketState.connection(); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL)); + this.keepAliveFrameWithRespondFlag(connection.pollFrame()); + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL)); + this.keepAliveFrameWithRespondFlag(connection.pollFrame()); + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL)); + this.keepAliveFrameWithRespondFlag(connection.pollFrame()); + + RSocketState.rSocket.dispose(); + FrameAssert.assertThat(connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasData("Disposed") + .hasNoLeaks(); + RSocketState.connection.dispose(); + + RSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void requesterRespondsToKeepAlives() { + RSocketState rSocketState = requester(100_000, 100_000); + TestDuplexConnection connection = rSocketState.connection(); + Duration duration = Duration.ofMillis(100); + Mono.delay(duration) + .subscribe( + l -> + connection.addToReceivedBuffer( + KeepAliveFrameCodec.encode( + rSocketState.allocator, true, 0, Unpooled.EMPTY_BUFFER))); + + virtualTimeScheduler.advanceTimeBy(duration); + FrameAssert.assertThat(connection.awaitFrame()) + .typeOf(FrameType.KEEPALIVE) + .matches(this::keepAliveFrameWithoutRespondFlag); + + rSocketState.rSocket.dispose(); + FrameAssert.assertThat(rSocketState.connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + rSocketState.connection.dispose(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRequesterNoKeepAlivesAfterDisconnect() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + TestDuplexConnection testConnection = rSocketState.connection(); + ResumableDuplexConnection resumableDuplexConnection = rSocketState.resumableDuplexConnection(); + + resumableDuplexConnection.disconnect(); + + Duration duration = Duration.ofMillis(KEEP_ALIVE_INTERVAL * 5); + virtualTimeScheduler.advanceTimeBy(duration); + Assertions.assertThat(testConnection.pollFrame()).isNull(); + + rSocketState.rSocket.dispose(); + rSocketState.connection.dispose(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRequesterKeepAlivesAfterReconnect() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + ResumableDuplexConnection resumableDuplexConnection = rSocketState.resumableDuplexConnection(); + resumableDuplexConnection.disconnect(); + TestDuplexConnection newTestConnection = new TestDuplexConnection(rSocketState.alloc()); + resumableDuplexConnection.connect(newTestConnection); + // resumableDuplexConnection.(0, 0, ignored -> Mono.empty()); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(KEEP_ALIVE_INTERVAL)); + + FrameAssert.assertThat(newTestConnection.awaitFrame()) + .typeOf(FrameType.KEEPALIVE) + .hasStreamIdZero() + .hasNoLeaks(); + + rSocketState.rSocket.dispose(); + FrameAssert.assertThat(newTestConnection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + FrameAssert.assertThat(newTestConnection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Connection Closed Unexpectedly") // API limitations + .hasNoLeaks(); + newTestConnection.dispose(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRequesterNoKeepAlivesAfterDispose() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + rSocketState.rSocket().dispose(); + Duration duration = Duration.ofMillis(500); + StepVerifier.create(Flux.from(rSocketState.connection().getSentAsPublisher()).take(duration)) + .then(() -> virtualTimeScheduler.advanceTimeBy(duration)) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + rSocketState.rSocket.dispose(); + FrameAssert.assertThat(rSocketState.connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Disposed") + .hasNoLeaks(); + rSocketState.connection.dispose(); + FrameAssert.assertThat(rSocketState.connection.pollFrame()) + .typeOf(FrameType.ERROR) + .hasStreamIdZero() + .hasData("Connection Closed Unexpectedly") + .hasNoLeaks(); + + rSocketState.allocator.assertHasNoLeaks(); + } + + @Test + void resumableRSocketsNotDisposedOnMissingKeepAlives() throws InterruptedException { + ResumableRSocketState resumableRequesterState = + resumableRequester(KEEP_ALIVE_INTERVAL, RESUMABLE_KEEP_ALIVE_TIMEOUT); + RSocket rSocket = resumableRequesterState.rSocket(); + TestDuplexConnection connection = resumableRequesterState.connection(); + + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(500)); + + Assertions.assertThat(rSocket.isDisposed()).isFalse(); + Assertions.assertThat(connection.isDisposed()).isTrue(); + + Assertions.assertThat(resumableRequesterState.connection.getSent()).allMatch(ByteBuf::release); + + resumableRequesterState.connection.dispose(); + resumableRequesterState.rSocket.dispose(); + + resumableRequesterState.allocator.assertHasNoLeaks(); + } + + private boolean keepAliveFrame(ByteBuf frame) { + return FrameHeaderCodec.frameType(frame) == FrameType.KEEPALIVE; + } + + private boolean keepAliveFrameWithRespondFlag(ByteBuf frame) { + return keepAliveFrame(frame) && KeepAliveFrameCodec.respondFlag(frame) && frame.release(); + } + + private boolean keepAliveFrameWithoutRespondFlag(ByteBuf frame) { + return keepAliveFrame(frame) && !KeepAliveFrameCodec.respondFlag(frame) && frame.release(); + } + + static class RSocketState { + private final RSocket rSocket; + private final TestDuplexConnection connection; + private final LeaksTrackingByteBufAllocator allocator; + private final Sinks.Empty onClose; + + public RSocketState( + RSocket rSocket, + LeaksTrackingByteBufAllocator allocator, + TestDuplexConnection connection, + Sinks.Empty onClose) { + this.rSocket = rSocket; + this.connection = connection; + this.allocator = allocator; + this.onClose = onClose; + } + + public TestDuplexConnection connection() { + return connection; + } + + public RSocket rSocket() { + return rSocket; + } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } + } + + static class ResumableRSocketState { + private final RSocket rSocket; + private final TestDuplexConnection connection; + private final ResumableDuplexConnection resumableDuplexConnection; + private final LeaksTrackingByteBufAllocator allocator; + private final Sinks.Empty onClose; + + public ResumableRSocketState( + RSocket rSocket, + TestDuplexConnection connection, + ResumableDuplexConnection resumableDuplexConnection, + Sinks.Empty onClose, + LeaksTrackingByteBufAllocator allocator) { + this.rSocket = rSocket; + this.connection = connection; + this.resumableDuplexConnection = resumableDuplexConnection; + this.onClose = onClose; + this.allocator = allocator; + } + + public TestDuplexConnection connection() { + return connection; + } + + public ResumableDuplexConnection resumableDuplexConnection() { + return resumableDuplexConnection; + } + + public RSocket rSocket() { + return rSocket; + } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java index 40487bec1..7cf12a81e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketConnectorTest.java @@ -100,6 +100,8 @@ public void unexpectedFramesBeforeResumeOKFrame(String frameType) { .hasData("RESUME_OK frame must be received before any others") .hasStreamIdZero() .hasNoLeaks(); + + transport.alloc().assertHasNoLeaks(); } @Test @@ -204,6 +206,8 @@ public void ensuresThatMonoFromRSocketConnectorCanBeUsedForMultipleSubscriptions return byteBuf.release(); }); assertThat(setupPayload.refCnt()).isZero(); + + testClientTransport.alloc().assertHasNoLeaks(); } @Test @@ -263,6 +267,8 @@ public void ensuresThatSetupPayloadProvidedAsMonoIsReleased() { payload.refCnt() == 1 && payload.data().refCnt() == 0 && payload.metadata().refCnt() == 0); + + testClientTransport.alloc().assertHasNoLeaks(); } @Test diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java index a9c9ed9a5..a461833d3 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -64,6 +64,7 @@ import java.util.function.BiFunction; import java.util.stream.Stream; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -91,6 +92,8 @@ class RSocketLeaseTest { private Sinks.Many leaseSender = Sinks.many().multicast().onBackpressureBuffer(); private RequesterLeaseTracker requesterLeaseTracker; + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; @BeforeEach void setUp() { @@ -100,6 +103,8 @@ void setUp() { connection = new TestDuplexConnection(byteBufAllocator); requesterLeaseTracker = new RequesterLeaseTracker(TAG, 0); responderLeaseTracker = new ResponderLeaseTracker(TAG, connection, () -> leaseSender.asFlux()); + this.thisClosedSink = Sinks.empty(); + this.otherClosedSink = Sinks.empty(); ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(connection, new InitializingInterceptorRegistry(), true); @@ -115,7 +120,9 @@ void setUp() { 0, null, __ -> null, - requesterLeaseTracker); + requesterLeaseTracker, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); mockRSocketHandler = mock(RSocket.class); when(mockRSocketHandler.metadataPush(any())) @@ -182,7 +189,13 @@ protected void hookOnError(Throwable throwable) { 0, FRAME_LENGTH_MASK, Integer.MAX_VALUE, - __ -> null); + __ -> null, + otherClosedSink); + } + + @AfterEach + void tearDownAndCheckForLeaks() { + byteBufAllocator.assertHasNoLeaks(); } @Test @@ -210,18 +223,26 @@ public void serverRSocketFactoryRejectsUnsupportedLease() { Assertions.assertThat(FrameHeaderCodec.frameType(error)).isEqualTo(ERROR); Assertions.assertThat(Exceptions.from(0, error).getMessage()) .isEqualTo("lease is not supported"); + error.release(); + connection.dispose(); + transport.alloc().assertHasNoLeaks(); } @Test public void clientRSocketFactorySetsLeaseFlag() { TestClientTransport clientTransport = new TestClientTransport(); - RSocketConnector.create().lease().connect(clientTransport).block(); - - Collection sent = clientTransport.testConnection().getSent(); - Assertions.assertThat(sent).hasSize(1); - ByteBuf setup = sent.iterator().next(); - Assertions.assertThat(FrameHeaderCodec.frameType(setup)).isEqualTo(SETUP); - Assertions.assertThat(SetupFrameCodec.honorLease(setup)).isTrue(); + try { + RSocketConnector.create().lease().connect(clientTransport).block(); + Collection sent = clientTransport.testConnection().getSent(); + Assertions.assertThat(sent).hasSize(1); + ByteBuf setup = sent.iterator().next(); + Assertions.assertThat(FrameHeaderCodec.frameType(setup)).isEqualTo(SETUP); + Assertions.assertThat(SetupFrameCodec.honorLease(setup)).isTrue(); + setup.release(); + } finally { + clientTransport.testConnection().dispose(); + clientTransport.alloc().assertHasNoLeaks(); + } } @ParameterizedTest @@ -375,10 +396,16 @@ void requesterExpiredLeaseRequestsAreRejected( @Test void requesterAvailabilityRespectsTransport() { - requesterLeaseTracker.handleLeaseFrame(leaseFrame(5_000, 1, Unpooled.EMPTY_BUFFER)); - double unavailable = 0.0; - connection.setAvailability(unavailable); - Assertions.assertThat(rSocketRequester.availability()).isCloseTo(unavailable, offset(1e-2)); + ByteBuf frame = leaseFrame(5_000, 1, Unpooled.EMPTY_BUFFER); + try { + + requesterLeaseTracker.handleLeaseFrame(frame); + double unavailable = 0.0; + connection.setAvailability(unavailable); + Assertions.assertThat(rSocketRequester.availability()).isCloseTo(unavailable, offset(1e-2)); + } finally { + frame.release(); + } } @ParameterizedTest @@ -630,10 +657,14 @@ void sendLease() { .findFirst() .orElseThrow(() -> new IllegalStateException("Lease frame not sent")); - Assertions.assertThat(LeaseFrameCodec.ttl(leaseFrame)).isEqualTo(ttl); - Assertions.assertThat(LeaseFrameCodec.numRequests(leaseFrame)).isEqualTo(numberOfRequests); - Assertions.assertThat(LeaseFrameCodec.metadata(leaseFrame).toString(utf8)) - .isEqualTo(metadataContent); + try { + Assertions.assertThat(LeaseFrameCodec.ttl(leaseFrame)).isEqualTo(ttl); + Assertions.assertThat(LeaseFrameCodec.numRequests(leaseFrame)).isEqualTo(numberOfRequests); + Assertions.assertThat(LeaseFrameCodec.metadata(leaseFrame).toString(utf8)) + .isEqualTo(metadataContent); + } finally { + leaseFrame.release(); + } } // @Test diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java index 8c662d67d..966fd65f2 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java @@ -17,8 +17,11 @@ import static org.assertj.core.api.Assertions.assertThat; +import io.rsocket.FrameAssert; import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; import io.rsocket.test.util.TestClientTransport; +import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.transport.ClientTransport; import java.io.UncheckedIOException; import java.time.Duration; @@ -49,27 +52,44 @@ public void shouldBeASharedReconnectableInstanceOfRSocketMono() throws Interrupt RSocket rSocket1 = rSocketMono.block(); RSocket rSocket2 = rSocketMono.block(); + FrameAssert.assertThat(testClientTransport[0].testConnection().awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + assertThat(rSocket1).isEqualTo(rSocket2); testClientTransport[0].testConnection().dispose(); + rSocket1.onClose().block(Duration.ofSeconds(1)); + testClientTransport[0].alloc().assertHasNoLeaks(); testClientTransport[0] = new TestClientTransport(); RSocket rSocket3 = rSocketMono.block(); RSocket rSocket4 = rSocketMono.block(); + FrameAssert.assertThat(testClientTransport[0].testConnection().awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + assertThat(rSocket3).isEqualTo(rSocket4).isNotEqualTo(rSocket2); + + testClientTransport[0].testConnection().dispose(); + rSocket3.onClose().block(Duration.ofSeconds(1)); + testClientTransport[0].alloc().assertHasNoLeaks(); } @Test - @SuppressWarnings({"rawtype", "unchecked"}) + @SuppressWarnings({"rawtype"}) public void shouldBeRetrieableConnectionSharedReconnectableInstanceOfRSocketMono() { ClientTransport transport = Mockito.mock(ClientTransport.class); + TestClientTransport transport1 = new TestClientTransport(); Mockito.when(transport.connect()) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) - .thenReturn(new TestClientTransport().connect()); + .thenReturn(transport1.connect()); Mono rSocketMono = RSocketConnector.create() .reconnect( @@ -87,19 +107,29 @@ public void shouldBeRetrieableConnectionSharedReconnectableInstanceOfRSocketMono UncheckedIOException.class, UncheckedIOException.class, UncheckedIOException.class); + + FrameAssert.assertThat(transport1.testConnection().awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + + transport1.testConnection().dispose(); + rSocket1.onClose().block(Duration.ofSeconds(1)); + transport1.alloc().assertHasNoLeaks(); } @Test - @SuppressWarnings({"rawtype", "unchecked"}) + @SuppressWarnings({"rawtype"}) public void shouldBeExaustedRetrieableConnectionSharedReconnectableInstanceOfRSocketMono() { ClientTransport transport = Mockito.mock(ClientTransport.class); + TestClientTransport transport1 = new TestClientTransport(); Mockito.when(transport.connect()) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) - .thenReturn(new TestClientTransport().connect()); + .thenReturn(transport1.connect()); Mono rSocketMono = RSocketConnector.create() .reconnect( @@ -121,17 +151,38 @@ public void shouldBeExaustedRetrieableConnectionSharedReconnectableInstanceOfRSo UncheckedIOException.class, UncheckedIOException.class, UncheckedIOException.class); + + transport1.alloc().assertHasNoLeaks(); } @Test public void shouldBeNotBeASharedReconnectableInstanceOfRSocketMono() { - - Mono rSocketMono = RSocketConnector.connectWith(new TestClientTransport()); + TestClientTransport transport = new TestClientTransport(); + Mono rSocketMono = RSocketConnector.connectWith(transport); RSocket rSocket1 = rSocketMono.block(); + TestDuplexConnection connection1 = transport.testConnection(); + + FrameAssert.assertThat(connection1.awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + RSocket rSocket2 = rSocketMono.block(); + TestDuplexConnection connection2 = transport.testConnection(); assertThat(rSocket1).isNotEqualTo(rSocket2); + + FrameAssert.assertThat(connection2.awaitFrame()) + .typeOf(FrameType.SETUP) + .hasStreamIdZero() + .hasNoLeaks(); + + connection1.dispose(); + connection2.dispose(); + rSocket1.onClose().block(Duration.ofSeconds(1)); + rSocket2.onClose().block(Duration.ofSeconds(1)); + transport.alloc().assertHasNoLeaks(); } @SafeVarargs diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java index 25d91b25b..01eb998c7 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -21,6 +21,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.CharsetUtil; +import io.rsocket.FrameAssert; import io.rsocket.RSocket; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.FrameHeaderCodec; @@ -37,6 +38,7 @@ import java.util.function.Function; import java.util.stream.Stream; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -44,6 +46,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; import reactor.test.util.RaceTestUtils; class RSocketRequesterSubscribersTest { @@ -60,11 +63,20 @@ class RSocketRequesterSubscribersTest { private LeaksTrackingByteBufAllocator allocator; private RSocket rSocketRequester; private TestDuplexConnection connection; + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; + + @AfterEach + void tearDownAndCheckNoLeaks() { + allocator.assertHasNoLeaks(); + } @BeforeEach void setUp() { allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); connection = new TestDuplexConnection(allocator); + this.thisClosedSink = Sinks.empty(); + this.otherClosedSink = Sinks.empty(); rSocketRequester = new RSocketRequester( connection, @@ -77,11 +89,14 @@ void setUp() { 0, null, __ -> null, - null); + null, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); } @ParameterizedTest @MethodSource("allInteractions") + @SuppressWarnings({"rawtypes", "unchecked"}) void singleSubscriber(Function> interaction, FrameType requestType) { Flux response = Flux.from(interaction.apply(rSocketRequester)); @@ -98,7 +113,11 @@ void singleSubscriber(Function> interaction, FrameType req assertSubscriberA.assertTerminated(); assertSubscriberB.assertTerminated(); - Assertions.assertThat(requestFramesCount(connection.getSent())).isEqualTo(1); + FrameAssert.assertThat(connection.pollFrame()).typeOf(requestType).hasNoLeaks(); + + if (requestType == FrameType.REQUEST_CHANNEL) { + FrameAssert.assertThat(connection.pollFrame()).typeOf(FrameType.COMPLETE).hasNoLeaks(); + } } @ParameterizedTest diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java index 6ccff3701..5cfa76a1c 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java @@ -1,15 +1,19 @@ package io.rsocket.core; +import io.rsocket.FrameAssert; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.core.RSocketRequesterTest.ClientSocketRule; +import io.rsocket.frame.FrameType; import io.rsocket.util.EmptyPayload; import java.nio.channels.ClosedChannelException; import java.time.Duration; import java.util.Arrays; import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -25,15 +29,23 @@ public void setup() { rule.init(); } + @AfterEach + public void tearDownAndCheckNoLeaks() { + rule.assertHasNoLeaks(); + } + @ParameterizedTest @MethodSource("rsocketInteractions") public void testCurrentStreamIsTerminatedOnConnectionClose( - Function> interaction) { + FrameType requestType, Function> interaction) { RSocketRequester rSocket = rule.socket; - Mono.delay(Duration.ofSeconds(1)).doOnNext(v -> rule.connection.dispose()).subscribe(); - StepVerifier.create(interaction.apply(rSocket)) + .then( + () -> { + FrameAssert.assertThat(rule.connection.pollFrame()).typeOf(requestType).hasNoLeaks(); + }) + .then(() -> rule.connection.dispose()) .expectError(ClosedChannelException.class) .verify(Duration.ofSeconds(5)); } @@ -41,7 +53,7 @@ public void testCurrentStreamIsTerminatedOnConnectionClose( @ParameterizedTest @MethodSource("rsocketInteractions") public void testSubsequentStreamIsTerminatedAfterConnectionClose( - Function> interaction) { + FrameType requestType, Function> interaction) { RSocketRequester rSocket = rule.socket; rule.connection.dispose(); @@ -50,46 +62,51 @@ public void testSubsequentStreamIsTerminatedAfterConnectionClose( .verify(Duration.ofSeconds(5)); } - public static Iterable>> rsocketInteractions() { + public static Iterable rsocketInteractions() { EmptyPayload payload = EmptyPayload.INSTANCE; - Publisher payloadStream = Flux.just(payload); - Function> resp = - new Function>() { - @Override - public Mono apply(RSocket rSocket) { - return rSocket.requestResponse(payload); - } + Arguments resp = + Arguments.of( + FrameType.REQUEST_RESPONSE, + new Function>() { + @Override + public Mono apply(RSocket rSocket) { + return rSocket.requestResponse(payload); + } - @Override - public String toString() { - return "Request Response"; - } - }; - Function> stream = - new Function>() { - @Override - public Flux apply(RSocket rSocket) { - return rSocket.requestStream(payload); - } + @Override + public String toString() { + return "Request Response"; + } + }); + Arguments stream = + Arguments.of( + FrameType.REQUEST_STREAM, + new Function>() { + @Override + public Flux apply(RSocket rSocket) { + return rSocket.requestStream(payload); + } - @Override - public String toString() { - return "Request Stream"; - } - }; - Function> channel = - new Function>() { - @Override - public Flux apply(RSocket rSocket) { - return rSocket.requestChannel(payloadStream); - } + @Override + public String toString() { + return "Request Stream"; + } + }); + Arguments channel = + Arguments.of( + FrameType.REQUEST_CHANNEL, + new Function>() { + @Override + public Flux apply(RSocket rSocket) { + return rSocket.requestChannel(Flux.never().startWith(payload)); + } - @Override - public String toString() { - return "Request Channel"; - } - }; + @Override + public String toString() { + return "Request Channel"; + } + }); return Arrays.asList(resp, stream, channel); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index 183785d2f..a1199f698 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -122,6 +122,7 @@ public void setUp() throws Throwable { public void tearDown() { Hooks.resetOnErrorDropped(); Hooks.resetOnNextDropped(); + rule.assertHasNoLeaks(); } @Test @@ -765,16 +766,21 @@ private static Stream racingCases() { @Test public void simpleOnDiscardRequestChannelTest() { AssertSubscriber assertSubscriber = AssertSubscriber.create(1); - TestPublisher testPublisher = TestPublisher.create(); + Sinks.Many testPublisher = Sinks.many().unicast().onBackpressureBuffer(); - Flux payloadFlux = rule.socket.requestChannel(testPublisher); + Flux payloadFlux = rule.socket.requestChannel(testPublisher.asFlux()); payloadFlux.subscribe(assertSubscriber); - testPublisher.next( - ByteBufPayload.create("d", "m"), - ByteBufPayload.create("d1", "m1"), - ByteBufPayload.create("d2", "m2")); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d"), ByteBufUtil.writeUtf8(rule.alloc(), "m"))); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d1"), ByteBufUtil.writeUtf8(rule.alloc(), "m1"))); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d2"), ByteBufUtil.writeUtf8(rule.alloc(), "m2"))); assertSubscriber.cancel(); @@ -787,16 +793,23 @@ public void simpleOnDiscardRequestChannelTest() { public void simpleOnDiscardRequestChannelTest2() { ByteBufAllocator allocator = rule.alloc(); AssertSubscriber assertSubscriber = AssertSubscriber.create(1); - TestPublisher testPublisher = TestPublisher.create(); + Sinks.Many testPublisher = Sinks.many().unicast().onBackpressureBuffer(); - Flux payloadFlux = rule.socket.requestChannel(testPublisher); + Flux payloadFlux = rule.socket.requestChannel(testPublisher.asFlux()); payloadFlux.subscribe(assertSubscriber); - testPublisher.next(ByteBufPayload.create("d", "m")); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d"), ByteBufUtil.writeUtf8(rule.alloc(), "m"))); int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); - testPublisher.next(ByteBufPayload.create("d1", "m1"), ByteBufPayload.create("d2", "m2")); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d1"), ByteBufUtil.writeUtf8(rule.alloc(), "m1"))); + testPublisher.tryEmitNext( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "d2"), ByteBufUtil.writeUtf8(rule.alloc(), "m2"))); rule.connection.addToReceivedBuffer( ErrorFrameCodec.encode( @@ -820,7 +833,7 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( switch (frameType) { case REQUEST_FNF: response = - testPublisher.mono().flatMap(p -> rule.socket.fireAndForget(p).then(Mono.empty())); + testPublisher.mono().flatMap(p -> rule.socket.fireAndForget(p)).then(Mono.empty()); break; case REQUEST_RESPONSE: response = testPublisher.mono().flatMap(p -> rule.socket.requestResponse(p)); @@ -836,7 +849,7 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( } response.subscribe(assertSubscriber); - testPublisher.next(ByteBufPayload.create("d")); + testPublisher.next(ByteBufPayload.create(ByteBufUtil.writeUtf8(rule.alloc(), "d"))); int streamId = rule.getStreamIdForRequestType(frameType); @@ -870,7 +883,7 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( } for (int i = 1; i < framesCnt; i++) { - testPublisher.next(ByteBufPayload.create("d" + i)); + testPublisher.next(ByteBufPayload.create(ByteBufUtil.writeUtf8(rule.alloc(), "d" + i))); } assertThat(rule.connection.getSent()) @@ -908,7 +921,10 @@ static Stream encodeDecodePayloadCases() { @MethodSource("refCntCases") public void ensureSendsErrorOnIllegalRefCntPayload( BiFunction> sourceProducer) { - Payload invalidPayload = ByteBufPayload.create("test", "test"); + Payload invalidPayload = + ByteBufPayload.create( + ByteBufUtil.writeUtf8(rule.alloc(), "test"), + ByteBufUtil.writeUtf8(rule.alloc(), "test")); invalidPayload.release(); Publisher source = sourceProducer.apply(invalidPayload, rule); @@ -926,7 +942,8 @@ private static Stream>> refCn (p, clientSocketRule) -> clientSocketRule.socket.requestChannel(Mono.just(p)), (p, clientSocketRule) -> { Flux.from(clientSocketRule.connection.getSentAsPublisher()) - .filter(bb -> FrameHeaderCodec.frameType(bb) == REQUEST_CHANNEL) + .filter(bb -> frameType(bb) == REQUEST_CHANNEL) + .doOnDiscard(ByteBuf.class, ReferenceCounted::release) .subscribe( bb -> { clientSocketRule.connection.addToReceivedBuffer( @@ -1453,8 +1470,14 @@ public void testWorkaround959(String type) { } public static class ClientSocketRule extends AbstractSocketRule { + + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; + @Override protected RSocketRequester newRSocket() { + this.thisClosedSink = Sinks.empty(); + this.otherClosedSink = Sinks.empty(); return new RSocketRequester( connection, PayloadDecoder.ZERO_COPY, @@ -1466,7 +1489,9 @@ protected RSocketRequester newRSocket() { Integer.MAX_VALUE, null, (__) -> null, - null); + null, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); } public int getStreamIdForRequestType(FrameType expectedFrameType) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index c0f64469c..4f689e396 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -113,6 +113,7 @@ public void setUp() { public void tearDown() { Hooks.resetOnErrorDropped(); Hooks.resetOnNextDropped(); + rule.assertHasNoLeaks(); } @Test @@ -146,9 +147,7 @@ public Mono requestResponse(Payload payload) { }); rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE); testPublisher.complete(); - assertThat(frameType(rule.connection.awaitFrame())) - .describedAs("Unexpected frame sent.") - .isIn(FrameType.COMPLETE, FrameType.NEXT_COMPLETE); + FrameAssert.assertThat(rule.connection.awaitFrame()).typeOf(FrameType.COMPLETE).hasNoLeaks(); testPublisher.assertWasNotCancelled(); } @@ -158,9 +157,10 @@ public void testHandlerEmitsError() { final int streamId = 4; rule.prefetch = 1; rule.sendRequest(streamId, FrameType.REQUEST_STREAM); - assertThat(frameType(rule.connection.awaitFrame())) - .describedAs("Unexpected frame sent.") - .isEqualTo(FrameType.ERROR); + FrameAssert.assertThat(rule.connection.awaitFrame()) + .typeOf(FrameType.ERROR) + .hasData("Request-Stream not implemented.") + .hasNoLeaks(); } @Test @@ -1184,6 +1184,7 @@ public static class ServerSocketRule extends AbstractSocketRule onCloseSink; @Override protected void doInit() { @@ -1220,6 +1221,7 @@ public void setAcceptingSocket(RSocket acceptingSocket, int prefetch) { @Override protected RSocketResponder newRSocket() { + onCloseSink = Sinks.empty(); return new RSocketResponder( connection, acceptingSocket, @@ -1228,7 +1230,8 @@ protected RSocketResponder newRSocket() { 0, maxFrameLength, maxInboundPayloadSize, - __ -> requestInterceptor); + __ -> requestInterceptor, + onCloseSink); } private void sendRequest(int streamId, FrameType frameType) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java index fd588cda3..90e881257 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java @@ -1,5 +1,8 @@ package io.rsocket.core; +import io.rsocket.Closeable; +import io.rsocket.FrameAssert; +import io.rsocket.frame.FrameType; import io.rsocket.test.util.TestClientTransport; import io.rsocket.test.util.TestServerTransport; import org.assertj.core.api.Assertions; @@ -16,12 +19,18 @@ public void serverErrorsWithEnabledFragmentationOnInsufficientMtu() { @Test public void serverSucceedsWithEnabledFragmentationOnSufficientMtu() { - RSocketServer.create().fragment(100).bind(new TestServerTransport()).block(); + TestServerTransport transport = new TestServerTransport(); + Closeable closeable = RSocketServer.create().fragment(100).bind(transport).block(); + closeable.dispose(); + transport.alloc().assertHasNoLeaks(); } @Test public void serverSucceedsWithDisabledFragmentation() { - RSocketServer.create().bind(new TestServerTransport()).block(); + TestServerTransport transport = new TestServerTransport(); + Closeable closeable = RSocketServer.create().bind(transport).block(); + closeable.dispose(); + transport.alloc().assertHasNoLeaks(); } @Test @@ -33,11 +42,23 @@ public void clientErrorsWithEnabledFragmentationOnInsufficientMtu() { @Test public void clientSucceedsWithEnabledFragmentationOnSufficientMtu() { - RSocketConnector.create().fragment(100).connect(new TestClientTransport()).block(); + TestClientTransport transport = new TestClientTransport(); + RSocketConnector.create().fragment(100).connect(transport).block(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .typeOf(FrameType.SETUP) + .hasNoLeaks(); + transport.testConnection().dispose(); + transport.alloc().assertHasNoLeaks(); } @Test public void clientSucceedsWithDisabledFragmentation() { - RSocketConnector.connectWith(new TestClientTransport()).block(); + TestClientTransport transport = new TestClientTransport(); + RSocketConnector.connectWith(transport).block(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .typeOf(FrameType.SETUP) + .hasNoLeaks(); + transport.testConnection().dispose(); + transport.alloc().assertHasNoLeaks(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java index 24bf95215..a335ac1f3 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2021 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,13 +20,17 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.rsocket.Closeable; import io.rsocket.FrameAssert; import io.rsocket.RSocket; +import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.frame.FrameType; import io.rsocket.frame.KeepAliveFrameCodec; import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.SetupFrameCodec; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestServerTransport; +import io.rsocket.util.EmptyPayload; import java.time.Duration; import java.util.Random; import org.assertj.core.api.Assertions; @@ -60,13 +64,14 @@ public void unexpectedFramesBeforeSetupFrame() { .hasData("SETUP or RESUME frame must be received before any others") .hasStreamIdZero() .hasNoLeaks(); + duplexConnection.alloc().assertHasNoLeaks(); } @Test public void timeoutOnNoFirstFrame() { final VirtualTimeScheduler scheduler = VirtualTimeScheduler.getOrSet(); + TestServerTransport transport = new TestServerTransport(); try { - TestServerTransport transport = new TestServerTransport(); RSocketServer.create().maxTimeToFirstFrame(Duration.ofMinutes(2)).bind(transport).block(); final TestDuplexConnection duplexConnection = transport.connect(); @@ -84,6 +89,7 @@ public void timeoutOnNoFirstFrame() { FrameAssert.assertThat(duplexConnection.pollFrame()).isNull(); } finally { + transport.alloc().assertHasNoLeaks(); VirtualTimeScheduler.reset(); } } @@ -128,14 +134,15 @@ public void unexpectedFramesBeforeSetup() { Sinks.Empty connectedSink = Sinks.empty(); TestServerTransport transport = new TestServerTransport(); - RSocketServer.create() - .acceptor( - (setup, sendingSocket) -> { - connectedSink.tryEmitEmpty(); - return Mono.just(new RSocket() {}); - }) - .bind(transport) - .block(); + Closeable server = + RSocketServer.create() + .acceptor( + (setup, sendingSocket) -> { + connectedSink.tryEmitEmpty(); + return Mono.just(new RSocket() {}); + }) + .bind(transport) + .block(); byte[] bytes = new byte[16_000_000]; new Random().nextBytes(bytes); @@ -153,5 +160,42 @@ public void unexpectedFramesBeforeSetup() { assertThat(connectedSink.scan(Scannable.Attr.TERMINATED)) .as("Connection should not succeed") .isFalse(); + FrameAssert.assertThat(connection.pollFrame()) + .hasStreamIdZero() + .hasData("SETUP or RESUME frame must be received before any others") + .hasNoLeaks(); + server.dispose(); + transport.alloc().assertHasNoLeaks(); + } + + @Test + public void ensuresErrorFrameDeliveredPriorConnectionDisposal() { + TestServerTransport transport = new TestServerTransport(); + Closeable server = + RSocketServer.create() + .acceptor( + (setup, sendingSocket) -> Mono.error(new RejectedSetupException("ACCESS_DENIED"))) + .bind(transport) + .block(); + + TestDuplexConnection connection = transport.connect(); + connection.addToReceivedBuffer( + SetupFrameCodec.encode( + ByteBufAllocator.DEFAULT, + false, + 0, + 1, + Unpooled.EMPTY_BUFFER, + "metadata_type", + "data_type", + EmptyPayload.INSTANCE)); + + StepVerifier.create(connection.onClose()).expectComplete().verify(Duration.ofSeconds(30)); + FrameAssert.assertThat(connection.pollFrame()) + .hasStreamIdZero() + .hasData("ACCESS_DENIED") + .hasNoLeaks(); + server.dispose(); + transport.alloc().assertHasNoLeaks(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index c9904d583..e01e6ebdc 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -35,6 +35,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicReference; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -56,6 +57,11 @@ public void setup() { rule.init(); } + @AfterEach + public void tearDownAndCheckOnLeaks() { + rule.alloc().assertHasNoLeaks(); + } + @Test public void rsocketDisposalShouldEndupWithNoErrorsOnClose() { RSocket requestHandlingRSocket = @@ -509,6 +515,8 @@ public static class SocketRule { private RSocket requestAcceptor; private LeaksTrackingByteBufAllocator allocator; + protected Sinks.Empty thisClosedSink; + protected Sinks.Empty otherClosedSink; public LeaksTrackingByteBufAllocator alloc() { return allocator; @@ -519,6 +527,9 @@ public void init() { serverProcessor = Sinks.many().multicast().directBestEffort(); clientProcessor = Sinks.many().multicast().directBestEffort(); + this.thisClosedSink = Sinks.empty(); + this.otherClosedSink = Sinks.empty(); + LocalDuplexConnection serverConnection = new LocalDuplexConnection("server", allocator, clientProcessor, serverProcessor); LocalDuplexConnection clientConnection = @@ -566,7 +577,8 @@ public Flux requestChannel(Publisher payloads) { 0, FRAME_LENGTH_MASK, Integer.MAX_VALUE, - __ -> null); + __ -> null, + otherClosedSink); crs = new RSocketRequester( @@ -580,7 +592,9 @@ public Flux requestChannel(Publisher payloads) { 0, null, __ -> null, - null); + null, + thisClosedSink, + otherClosedSink.asMono().and(thisClosedSink.asMono())); } public void setRequestAcceptor(RSocket requestAcceptor) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/SendUtilsTest.java b/rsocket-core/src/test/java/io/rsocket/core/SendUtilsTest.java new file mode 100644 index 000000000..9a51b9419 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/SendUtilsTest.java @@ -0,0 +1,31 @@ +package io.rsocket.core; + +import static org.mockito.Mockito.*; + +import io.netty.util.ReferenceCounted; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; + +public class SendUtilsTest { + + @Test + void droppedElementsConsumerShouldAcceptOtherTypesThanReferenceCounted() { + Consumer value = extractDroppedElementConsumer(); + value.accept(new Object()); + } + + @Test + void droppedElementsConsumerReleaseReference() { + ReferenceCounted referenceCounted = mock(ReferenceCounted.class); + when(referenceCounted.release()).thenReturn(true); + + Consumer value = extractDroppedElementConsumer(); + value.accept(referenceCounted); + + verify(referenceCounted).release(); + } + + private static Consumer extractDroppedElementConsumer() { + return (Consumer) SendUtils.DISCARD_CONTEXT.stream().findAny().get().getValue(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java index 44ff78a64..87c3a865f 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -58,10 +58,12 @@ void responderRejectSetup() { ByteBuf sentFrame = transport.awaitSent(); assertThat(FrameHeaderCodec.frameType(sentFrame)).isEqualTo(FrameType.ERROR); RuntimeException error = Exceptions.from(0, sentFrame); + sentFrame.release(); assertThat(errorMsg).isEqualTo(error.getMessage()); assertThat(error).isInstanceOf(RejectedSetupException.class); RSocket acceptorSender = acceptor.senderRSocket().block(); assertThat(acceptorSender.isDisposed()).isTrue(); + transport.allocator.assertHasNoLeaks(); } @Test @@ -69,6 +71,8 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); TestDuplexConnection conn = new TestDuplexConnection(allocator); + Sinks.Empty onThisSideClosedSink = Sinks.empty(); + RSocketRequester rSocket = new RSocketRequester( conn, @@ -81,7 +85,9 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { 0, null, __ -> null, - null); + null, + onThisSideClosedSink, + onThisSideClosedSink.asMono()); String errorMsg = "error"; @@ -100,6 +106,7 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { .verify(Duration.ofSeconds(5)); assertThat(rSocket.isDisposed()).isTrue(); + allocator.assertHasNoLeaks(); } @Test @@ -107,6 +114,7 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); TestDuplexConnection conn = new TestDuplexConnection(allocator); + Sinks.Empty onThisSideClosedSink = Sinks.empty(); RSocketRequester rSocket = new RSocketRequester( conn, @@ -119,7 +127,9 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { 0, null, __ -> null, - null); + null, + onThisSideClosedSink, + onThisSideClosedSink.asMono()); conn.addToReceivedBuffer( ErrorFrameCodec.encode(ByteBufAllocator.DEFAULT, 0, new RejectedSetupException("error"))); @@ -131,6 +141,7 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { .expectErrorMatches( err -> err instanceof RejectedSetupException && "error".equals(err.getMessage())) .verify(Duration.ofSeconds(5)); + allocator.assertHasNoLeaks(); } private static class RejectingAcceptor implements SocketAcceptor { diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java index ff7e4ff17..a316aed8b 100644 --- a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java @@ -43,14 +43,18 @@ final class ExceptionsTest { void fromApplicationException() { ByteBuf byteBuf = createErrorFrame(1, APPLICATION_ERROR, "test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(ApplicationErrorException.class) - .hasMessage("test-message"); - - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage( - "Invalid Error frame in Stream ID 0: 0x%08X '%s'", APPLICATION_ERROR, "test-message"); + try { + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(ApplicationErrorException.class) + .hasMessage("test-message"); + + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Invalid Error frame in Stream ID 0: 0x%08X '%s'", APPLICATION_ERROR, "test-message"); + } finally { + byteBuf.release(); + } } @DisplayName("from returns CanceledException") @@ -58,28 +62,37 @@ void fromApplicationException() { void fromCanceledException() { ByteBuf byteBuf = createErrorFrame(1, CANCELED, "test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(CanceledException.class) - .hasMessage("test-message"); + try { - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", CANCELED, "test-message"); + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(CanceledException.class) + .hasMessage("test-message"); + + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", CANCELED, "test-message"); + } finally { + byteBuf.release(); + } } @DisplayName("from returns ConnectionCloseException") @Test void fromConnectionCloseException() { ByteBuf byteBuf = createErrorFrame(0, CONNECTION_CLOSE, "test-message"); + try { - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(ConnectionCloseException.class) - .hasMessage("test-message"); + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(ConnectionCloseException.class) + .hasMessage("test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage( - "Invalid Error frame in Stream ID 1: 0x%08X '%s'", CONNECTION_CLOSE, "test-message"); + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", CONNECTION_CLOSE, "test-message"); + } finally { + byteBuf.release(); + } } @DisplayName("from returns ConnectionErrorException") @@ -87,116 +100,146 @@ void fromConnectionCloseException() { void fromConnectionErrorException() { ByteBuf byteBuf = createErrorFrame(0, CONNECTION_ERROR, "test-message"); - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(ConnectionErrorException.class) - .hasMessage("test-message"); + try { - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage( - "Invalid Error frame in Stream ID 1: 0x%08X '%s'", CONNECTION_ERROR, "test-message"); + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(ConnectionErrorException.class) + .hasMessage("test-message"); + + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", CONNECTION_ERROR, "test-message"); + } finally { + byteBuf.release(); + } } @DisplayName("from returns IllegalArgumentException if error frame has illegal error code") @Test void fromIllegalErrorFrame() { ByteBuf byteBuf = createErrorFrame(0, 0x00000000, "test-message"); + try { - assertThat(Exceptions.from(0, byteBuf)) - .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", 0, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", 0, "test-message") + .isInstanceOf(IllegalArgumentException.class); - assertThat(Exceptions.from(1, byteBuf)) - .hasMessage("Invalid Error frame in Stream ID 1: 0x%08X '%s'", 0x00000000, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 1: 0x%08X '%s'", 0x00000000, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns InvalidException") @Test void fromInvalidException() { ByteBuf byteBuf = createErrorFrame(1, INVALID, "test-message"); + try { + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(InvalidException.class) + .hasMessage("test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(InvalidException.class) - .hasMessage("test-message"); - - assertThat(Exceptions.from(0, byteBuf)) - .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", INVALID, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", INVALID, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns InvalidSetupException") @Test void fromInvalidSetupException() { ByteBuf byteBuf = createErrorFrame(0, INVALID_SETUP, "test-message"); + try { + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(InvalidSetupException.class) + .hasMessage("test-message"); - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(InvalidSetupException.class) - .hasMessage("test-message"); - - assertThat(Exceptions.from(1, byteBuf)) - .hasMessage( - "Invalid Error frame in Stream ID 1: 0x%08X '%s'", INVALID_SETUP, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", INVALID_SETUP, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns RejectedException") @Test void fromRejectedException() { ByteBuf byteBuf = createErrorFrame(1, REJECTED, "test-message"); + try { - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(RejectedException.class) - .withFailMessage("test-message"); + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(RejectedException.class) + .withFailMessage("test-message"); - assertThat(Exceptions.from(0, byteBuf)) - .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", REJECTED, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", REJECTED, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns RejectedResumeException") @Test void fromRejectedResumeException() { ByteBuf byteBuf = createErrorFrame(0, REJECTED_RESUME, "test-message"); + try { - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(RejectedResumeException.class) - .hasMessage("test-message"); + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(RejectedResumeException.class) + .hasMessage("test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .hasMessage( - "Invalid Error frame in Stream ID 1: 0x%08X '%s'", REJECTED_RESUME, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", REJECTED_RESUME, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns RejectedSetupException") @Test void fromRejectedSetupException() { ByteBuf byteBuf = createErrorFrame(0, REJECTED_SETUP, "test-message"); + try { - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(RejectedSetupException.class) - .withFailMessage("test-message"); + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(RejectedSetupException.class) + .withFailMessage("test-message"); - assertThat(Exceptions.from(1, byteBuf)) - .hasMessage( - "Invalid Error frame in Stream ID 1: 0x%08X '%s'", REJECTED_SETUP, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", REJECTED_SETUP, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns UnsupportedSetupException") @Test void fromUnsupportedSetupException() { ByteBuf byteBuf = createErrorFrame(0, UNSUPPORTED_SETUP, "test-message"); + try { + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(UnsupportedSetupException.class) + .hasMessage("test-message"); - assertThat(Exceptions.from(0, byteBuf)) - .isInstanceOf(UnsupportedSetupException.class) - .hasMessage("test-message"); - - assertThat(Exceptions.from(1, byteBuf)) - .hasMessage( - "Invalid Error frame in Stream ID 1: 0x%08X '%s'", UNSUPPORTED_SETUP, "test-message") - .isInstanceOf(IllegalArgumentException.class); + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", UNSUPPORTED_SETUP, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } @DisplayName("from returns CustomRSocketException") @@ -210,15 +253,18 @@ void fromCustomRSocketException() { : ThreadLocalRandom.current() .nextInt(ErrorFrameCodec.MIN_USER_ALLOWED_ERROR_CODE, Integer.MAX_VALUE); ByteBuf byteBuf = createErrorFrame(0, randomCode, "test-message"); - - assertThat(Exceptions.from(1, byteBuf)) - .isInstanceOf(CustomRSocketException.class) - .hasMessage("test-message"); - - assertThat(Exceptions.from(0, byteBuf)) - .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", randomCode, "test-message") - .isInstanceOf(IllegalArgumentException.class); - byteBuf.release(); + try { + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(CustomRSocketException.class) + .hasMessage("test-message"); + + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 0: 0x%08X '%s'", randomCode, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } finally { + byteBuf.release(); + } } } diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java new file mode 100644 index 000000000..a35e89391 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceRSocketClientTest.java @@ -0,0 +1,94 @@ +package io.rsocket.loadbalance; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketClient; +import io.rsocket.core.RSocketConnector; +import io.rsocket.transport.ClientTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +@ExtendWith(MockitoExtension.class) +class LoadbalanceRSocketClientTest { + + @Mock private ClientTransport clientTransport; + @Mock private RSocketConnector rSocketConnector; + + public static final Duration SHORT_DURATION = Duration.ofMillis(25); + public static final Duration LONG_DURATION = Duration.ofMillis(75); + + private static final Publisher SOURCE = + Flux.interval(SHORT_DURATION) + .onBackpressureBuffer() + .map(String::valueOf) + .map(DefaultPayload::create); + + private static final Mono PROGRESSING_HANDLER = + Mono.just( + new RSocket() { + private final AtomicInteger i = new AtomicInteger(); + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + .delayElements(SHORT_DURATION) + .map(Payload::getDataUtf8) + .map(DefaultPayload::create) + .take(i.incrementAndGet()); + } + }); + + @Test + void testChannelReconnection() { + when(rSocketConnector.connect(clientTransport)).thenReturn(PROGRESSING_HANDLER); + + RSocketClient client = + LoadbalanceRSocketClient.create( + rSocketConnector, + Mono.just(singletonList(LoadbalanceTarget.from("key", clientTransport)))); + + Publisher result = + client + .requestChannel(SOURCE) + .repeatWhen(longFlux -> longFlux.delayElements(LONG_DURATION).take(5)) + .map(Payload::getDataUtf8) + .log(); + + StepVerifier.create(result) + .expectSubscription() + .assertNext(s -> assertThat(s).isEqualTo("0")) + .assertNext(s -> assertThat(s).isEqualTo("0")) + .assertNext(s -> assertThat(s).isEqualTo("1")) + .assertNext(s -> assertThat(s).isEqualTo("0")) + .assertNext(s -> assertThat(s).isEqualTo("1")) + .assertNext(s -> assertThat(s).isEqualTo("2")) + .assertNext(s -> assertThat(s).isEqualTo("0")) + .assertNext(s -> assertThat(s).isEqualTo("1")) + .assertNext(s -> assertThat(s).isEqualTo("2")) + .assertNext(s -> assertThat(s).isEqualTo("3")) + .assertNext(s -> assertThat(s).isEqualTo("0")) + .assertNext(s -> assertThat(s).isEqualTo("1")) + .assertNext(s -> assertThat(s).isEqualTo("2")) + .assertNext(s -> assertThat(s).isEqualTo("3")) + .assertNext(s -> assertThat(s).isEqualTo("4")) + .verifyComplete(); + + verify(rSocketConnector).connect(clientTransport); + verifyNoMoreInteractions(rSocketConnector, clientTransport); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java index fcd3ae4a9..c1b509297 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/LoadbalanceTest.java @@ -20,7 +20,6 @@ import io.rsocket.RaceTestConstants; import io.rsocket.core.RSocketConnector; import io.rsocket.internal.subscriber.AssertSubscriber; -import io.rsocket.plugins.RSocketInterceptor; import io.rsocket.test.util.TestClientTransport; import io.rsocket.transport.ClientTransport; import io.rsocket.util.EmptyPayload; @@ -71,10 +70,11 @@ public Mono fireAndForget(Payload payload) { return Mono.empty(); } }; - final RSocketConnector rSocketConnectorMock = - RSocketConnector.create() - .interceptors( - ir -> ir.forRequester((RSocketInterceptor) socket -> new TestRSocket(rSocket))); + + final RSocketConnector rSocketConnectorMock = Mockito.mock(RSocketConnector.class); + final ClientTransport mockTransport1 = Mockito.mock(ClientTransport.class); + Mockito.when(rSocketConnectorMock.connect(Mockito.any(ClientTransport.class))) + .then(im -> Mono.just(new TestRSocket(rSocket))); final List collectionOfDestination1 = Collections.singletonList(LoadbalanceTarget.from("1", mockTransport)); diff --git a/rsocket-core/src/test/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategyTest.java b/rsocket-core/src/test/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategyTest.java index 6640aea4e..8cc254cbb 100644 --- a/rsocket-core/src/test/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategyTest.java +++ b/rsocket-core/src/test/java/io/rsocket/loadbalance/WeightedLoadbalanceStrategyTest.java @@ -80,8 +80,13 @@ public Mono fireAndForget(Payload payload) { LoadbalanceTarget.from("1", mockTransport), LoadbalanceTarget.from("2", mockTransport))); - Assertions.assertThat(counter1.get()).isCloseTo(1000, Offset.offset(1)); - Assertions.assertThat(counter2.get()).isCloseTo(0, Offset.offset(1)); + Assertions.assertThat(counter1.get()) + .describedAs("c1=" + counter1.get() + " c2=" + counter2.get()) + .isCloseTo( + RaceTestConstants.REPEATS, Offset.offset(Math.round(RaceTestConstants.REPEATS * 0.1f))); + Assertions.assertThat(counter2.get()) + .describedAs("c1=" + counter1.get() + " c2=" + counter2.get()) + .isCloseTo(0, Offset.offset(Math.round(RaceTestConstants.REPEATS * 0.1f))); } @Test @@ -165,8 +170,11 @@ public Mono fireAndForget(Payload payload) { } Assertions.assertThat(counter1.get()) - .isCloseTo(RaceTestConstants.REPEATS * 3, Offset.offset(100)); - Assertions.assertThat(counter2.get()).isCloseTo(0, Offset.offset(100)); + .isCloseTo( + RaceTestConstants.REPEATS * 3, + Offset.offset(Math.round(RaceTestConstants.REPEATS * 3 * 0.1f))); + Assertions.assertThat(counter2.get()) + .isCloseTo(0, Offset.offset(Math.round(RaceTestConstants.REPEATS * 3 * 0.1f))); rSocket2.updateAvailability(0.0); @@ -177,8 +185,13 @@ public Mono fireAndForget(Payload payload) { } Assertions.assertThat(counter1.get()) - .isCloseTo(RaceTestConstants.REPEATS * 3, Offset.offset(100)); - Assertions.assertThat(counter2.get()).isCloseTo(RaceTestConstants.REPEATS, Offset.offset(100)); + .isCloseTo( + RaceTestConstants.REPEATS * 3, + Offset.offset(Math.round(RaceTestConstants.REPEATS * 4 * 0.1f))); + Assertions.assertThat(counter2.get()) + .isCloseTo( + RaceTestConstants.REPEATS, + Offset.offset(Math.round(RaceTestConstants.REPEATS * 4 * 0.1f))); source.next( Arrays.asList( @@ -191,9 +204,13 @@ public Mono fireAndForget(Payload payload) { } Assertions.assertThat(counter1.get()) - .isCloseTo(RaceTestConstants.REPEATS * 3, Offset.offset(100)); + .isCloseTo( + RaceTestConstants.REPEATS * 3, + Offset.offset(Math.round(RaceTestConstants.REPEATS * 5 * 0.1f))); Assertions.assertThat(counter2.get()) - .isCloseTo(RaceTestConstants.REPEATS * 2, Offset.offset(100)); + .isCloseTo( + RaceTestConstants.REPEATS * 2, + Offset.offset(Math.round(RaceTestConstants.REPEATS * 5 * 0.1f))); } static class WeightedTestRSocket extends BaseWeightedStats implements RSocket { diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataCodecTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataCodecTest.java index 3ce07729d..a4e8fb2d8 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataCodecTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataCodecTest.java @@ -23,12 +23,22 @@ import io.netty.buffer.*; import io.netty.util.CharsetUtil; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.test.util.ByteBufUtils; import io.rsocket.util.NumberUtils; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; class CompositeMetadataCodecTest { + final LeaksTrackingByteBufAllocator testAllocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + + @AfterEach + void tearDownAndCheckForLeaks() { + testAllocator.assertHasNoLeaks(); + } + static String byteToBitsString(byte b) { return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'); } @@ -48,17 +58,14 @@ void customMimeHeaderLatin1_encodingFails() { assertThatIllegalArgumentException() .isThrownBy( - () -> - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + () -> CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeNotAscii, 0)) .withMessage("custom mime type must be US_ASCII characters only"); } @Test void customMimeHeaderLength0_encodingFails() { assertThatIllegalArgumentException() - .isThrownBy( - () -> CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "", 0)) + .isThrownBy(() -> CompositeMetadataCodec.encodeMetadataHeader(testAllocator, "", 0)) .withMessage( "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); } @@ -70,8 +77,7 @@ void customMimeHeaderLength127() { builder.append('a'); } String mimeString = builder.toString(); - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeString, 0); // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111110"); @@ -99,6 +105,7 @@ void customMimeHeaderLength127() { .hasToString(mimeString); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -108,8 +115,7 @@ void customMimeHeaderLength128() { builder.append('a'); } String mimeString = builder.toString(); - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeString, 0); // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111111"); @@ -137,6 +143,7 @@ void customMimeHeaderLength128() { .hasToString(mimeString); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -148,9 +155,7 @@ void customMimeHeaderLength129_encodingFails() { assertThatIllegalArgumentException() .isThrownBy( - () -> - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, builder.toString(), 0)) + () -> CompositeMetadataCodec.encodeMetadataHeader(testAllocator, builder.toString(), 0)) .withMessage( "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); } @@ -158,8 +163,7 @@ void customMimeHeaderLength129_encodingFails() { @Test void customMimeHeaderLengthOne() { String mimeString = "w"; - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeString, 0); // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000000"); @@ -185,13 +189,13 @@ void customMimeHeaderLengthOne() { .hasToString(mimeString); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test void customMimeHeaderLengthTwo() { String mimeString = "ww"; - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeString, 0); // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000001"); @@ -219,6 +223,7 @@ void customMimeHeaderLengthTwo() { .hasToString(mimeString); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -227,9 +232,7 @@ void customMimeHeaderUtf8_encodingFails() { "mime/tyࠒe"; // this is the SAMARITAN LETTER QUF u+0812 represented on 3 bytes assertThatIllegalArgumentException() .isThrownBy( - () -> - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + () -> CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mimeNotAscii, 0)) .withMessage("custom mime type must be US_ASCII characters only"); } @@ -317,72 +320,73 @@ void decodeTypeSkipsFirstByte() { @Test void encodeMetadataCustomTypeDelegates() { - ByteBuf expected = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo", 2); + ByteBuf expected = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, "foo", 2); - CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf test = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadata( - test, ByteBufAllocator.DEFAULT, "foo", ByteBufUtils.getRandomByteBuf(2)); + test, testAllocator, "foo", ByteBufUtils.getRandomByteBuf(2)); assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + test.release(); + expected.release(); } @Test void encodeMetadataKnownTypeDelegates() { ByteBuf expected = CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, - WellKnownMimeType.APPLICATION_OCTET_STREAM.getIdentifier(), - 2); + testAllocator, WellKnownMimeType.APPLICATION_OCTET_STREAM.getIdentifier(), 2); - CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf test = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadata( test, - ByteBufAllocator.DEFAULT, + testAllocator, WellKnownMimeType.APPLICATION_OCTET_STREAM, ByteBufUtils.getRandomByteBuf(2)); assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + test.release(); + expected.release(); } @Test void encodeMetadataReservedTypeDelegates() { - ByteBuf expected = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, (byte) 120, 2); + ByteBuf expected = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, (byte) 120, 2); - CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf test = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadata( - test, ByteBufAllocator.DEFAULT, (byte) 120, ByteBufUtils.getRandomByteBuf(2)); + test, testAllocator, (byte) 120, ByteBufUtils.getRandomByteBuf(2)); assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + test.release(); + expected.release(); } @Test void encodeTryCompressWithCompressableType() { ByteBuf metadata = ByteBufUtils.getRandomByteBuf(2); - CompositeByteBuf target = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf target = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadataWithCompression( - target, - UnpooledByteBufAllocator.DEFAULT, - WellKnownMimeType.APPLICATION_AVRO.getString(), - metadata); + target, testAllocator, WellKnownMimeType.APPLICATION_AVRO.getString(), metadata); assertThat(target.readableBytes()).as("readableBytes 1 + 3 + 2").isEqualTo(6); + target.release(); } @Test void encodeTryCompressWithCustomType() { ByteBuf metadata = ByteBufUtils.getRandomByteBuf(2); - CompositeByteBuf target = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeByteBuf target = testAllocator.compositeBuffer(); CompositeMetadataCodec.encodeAndAddMetadataWithCompression( - target, UnpooledByteBufAllocator.DEFAULT, "custom/example", metadata); + target, testAllocator, "custom/example", metadata); assertThat(target.readableBytes()).as("readableBytes 1 + 14 + 3 + 2").isEqualTo(20); + target.release(); } @Test @@ -390,19 +394,20 @@ void hasEntry() { WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; CompositeByteBuf buffer = - Unpooled.compositeBuffer() + testAllocator + .compositeBuffer() .addComponent( true, - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0)) + CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mime.getIdentifier(), 0)) .addComponent( true, CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0)); + testAllocator, mime.getIdentifier(), 0)); assertThat(CompositeMetadataCodec.hasEntry(buffer, 0)).isTrue(); assertThat(CompositeMetadataCodec.hasEntry(buffer, 4)).isTrue(); assertThat(CompositeMetadataCodec.hasEntry(buffer, 8)).isFalse(); + buffer.release(); } @Test @@ -417,8 +422,7 @@ void isWellKnownMimeType() { @Test void knownMimeHeader120_reserved() { byte mime = (byte) 120; - ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); + ByteBuf encoded = CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mime, 0); assertThat(mime) .as("smoke test RESERVED_120 unsigned 7 bits representation") @@ -443,6 +447,7 @@ void knownMimeHeader120_reserved() { assertThat(decodeMimeIdFromMimeBuffer(header)).as("decoded mime id").isEqualTo(mime); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -453,8 +458,7 @@ void knownMimeHeader127_compositeMetadata() { .isEqualTo((byte) 127) .isEqualTo((byte) 0b01111111); ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); + CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mime.getIdentifier(), 0); assertThat(toHeaderBits(encoded)) .startsWith("1") @@ -480,6 +484,7 @@ void knownMimeHeader127_compositeMetadata() { .isEqualTo(mime.getIdentifier()); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test @@ -490,8 +495,7 @@ void knownMimeHeaderZero_avro() { .isEqualTo((byte) 0) .isEqualTo((byte) 0b00000000); ByteBuf encoded = - CompositeMetadataCodec.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); + CompositeMetadataCodec.encodeMetadataHeader(testAllocator, mime.getIdentifier(), 0); assertThat(toHeaderBits(encoded)) .startsWith("1") @@ -517,6 +521,7 @@ void knownMimeHeaderZero_avro() { .isEqualTo(mime.getIdentifier()); assertThat(content.readableBytes()).as("no metadata content").isZero(); + encoded.release(); } @Test diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java index 9227bcaca..5c8d40306 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/MimeTypeMetadataCodecTest.java @@ -30,20 +30,28 @@ public class MimeTypeMetadataCodecTest { public void wellKnownMimeType() { WellKnownMimeType mimeType = WellKnownMimeType.APPLICATION_HESSIAN; ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, mimeType); - List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); + try { + List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); - assertThat(mimeTypes.size()).isEqualTo(1); - assertThat(WellKnownMimeType.fromString(mimeTypes.get(0))).isEqualTo(mimeType); + assertThat(mimeTypes.size()).isEqualTo(1); + assertThat(WellKnownMimeType.fromString(mimeTypes.get(0))).isEqualTo(mimeType); + } finally { + byteBuf.release(); + } } @Test public void customMimeType() { String mimeType = "aaa/bb"; ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, mimeType); - List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); + try { + List mimeTypes = MimeTypeMetadataCodec.decode(byteBuf); - assertThat(mimeTypes.size()).isEqualTo(1); - assertThat(mimeTypes.get(0)).isEqualTo(mimeType); + assertThat(mimeTypes.size()).isEqualTo(1); + assertThat(mimeTypes.get(0)).isEqualTo(mimeType); + } finally { + byteBuf.release(); + } } @Test @@ -51,6 +59,10 @@ public void multipleMimeTypes() { List mimeTypes = Lists.newArrayList("aaa/bbb", "application/x-hessian"); ByteBuf byteBuf = MimeTypeMetadataCodec.encode(ByteBufAllocator.DEFAULT, mimeTypes); - assertThat(MimeTypeMetadataCodec.decode(byteBuf)).isEqualTo(mimeTypes); + try { + assertThat(MimeTypeMetadataCodec.decode(byteBuf)).isEqualTo(mimeTypes); + } finally { + byteBuf.release(); + } } } diff --git a/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java b/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java index 2bb718ef7..9a19050f9 100644 --- a/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/plugins/RequestInterceptorTest.java @@ -1,16 +1,19 @@ package io.rsocket.plugins; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.Closeable; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.frame.FrameType; import io.rsocket.transport.local.LocalClientTransport; import io.rsocket.transport.local.LocalServerTransport; import io.rsocket.util.DefaultPayload; +import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.Function; @@ -29,6 +32,9 @@ public class RequestInterceptorTest { @ParameterizedTest @ValueSource(booleans = {true, false}) void interceptorShouldBeInstalledProperlyOnTheClientRequesterSide(boolean errorOutcome) { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); final Closeable closeable = RSocketServer.create( SocketAcceptor.with( @@ -69,7 +75,7 @@ public Flux requestChannel(Publisher payloads) { ir.forRequestsInRequester( (Function) (__) -> testRequestInterceptor)) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -130,6 +136,7 @@ public Flux requestChannel(Publisher payloads) { } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @@ -137,6 +144,10 @@ public Flux requestChannel(Publisher payloads) { @ValueSource(booleans = {true, false}) void interceptorShouldBeInstalledProperlyOnTheClientResponderSide(boolean errorOutcome) throws InterruptedException { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + CountDownLatch latch = new CountDownLatch(1); final Closeable closeable = RSocketServer.create( @@ -209,7 +220,7 @@ public Flux requestChannel(Publisher payloads) { ir.forRequestsInResponder( (Function) (__) -> testRequestInterceptor)) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -253,12 +264,17 @@ public Flux requestChannel(Publisher payloads) { } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @ParameterizedTest @ValueSource(booleans = {true, false}) void interceptorShouldBeInstalledProperlyOnTheServerRequesterSide(boolean errorOutcome) { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); final Closeable closeable = RSocketServer.create( @@ -297,7 +313,9 @@ public Flux requestChannel(Publisher payloads) { (__) -> testRequestInterceptor)) .bindNow(LocalServerTransport.create("test")); final RSocket rSocket = - RSocketConnector.create().connect(LocalClientTransport.create("test")).block(); + RSocketConnector.create() + .connect(LocalClientTransport.create("test", byteBufAllocator)) + .block(); try { rSocket @@ -357,6 +375,7 @@ public Flux requestChannel(Publisher payloads) { } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @@ -364,6 +383,10 @@ public Flux requestChannel(Publisher payloads) { @ValueSource(booleans = {true, false}) void interceptorShouldBeInstalledProperlyOnTheServerResponderSide(boolean errorOutcome) throws InterruptedException { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + CountDownLatch latch = new CountDownLatch(1); final TestRequestInterceptor testRequestInterceptor = new TestRequestInterceptor(); final Closeable closeable = @@ -437,7 +460,7 @@ public Flux requestChannel(Publisher payloads) { : Flux.from(payloads); } })) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -481,11 +504,16 @@ public Flux requestChannel(Publisher payloads) { } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @Test void ensuresExceptionInTheInterceptorIsHandledProperly() { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + final Closeable closeable = RSocketServer.create( SocketAcceptor.with( @@ -546,7 +574,7 @@ public void dispose() {} ir.forRequestsInRequester( (Function) (__) -> testRequestInterceptor)) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -575,12 +603,17 @@ public void dispose() {} } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } @ParameterizedTest @ValueSource(booleans = {true, false}) void shouldSupportMultipleInterceptors(boolean errorOutcome) { + final LeaksTrackingByteBufAllocator byteBufAllocator = + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "test"); + final Closeable closeable = RSocketServer.create( SocketAcceptor.with( @@ -655,7 +688,7 @@ public void dispose() {} .forRequestsInRequester( (Function) (__) -> testRequestInterceptor2)) - .connect(LocalClientTransport.create("test")) + .connect(LocalClientTransport.create("test", byteBufAllocator)) .block(); try { @@ -751,6 +784,7 @@ public void dispose() {} } finally { rSocket.dispose(); closeable.dispose(); + byteBufAllocator.assertHasNoLeaks(); } } } diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java index 34d8a7345..8229bf42b 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/ClientRSocketSessionTest.java @@ -29,418 +29,442 @@ public class ClientRSocketSessionTest { @Test void sessionTimeoutSmokeTest() { final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); - final TestClientTransport transport = new TestClientTransport(); - final InMemoryResumableFramesStore framesStore = - new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); - - transport.connect().subscribe(); - - final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection( - "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); - - resumableDuplexConnection.receive().subscribe(); - - final ClientRSocketSession session = - new ClientRSocketSession( - Unpooled.EMPTY_BUFFER, - resumableDuplexConnection, - transport.connect().delaySubscription(Duration.ofMillis(1)), - c -> { - AtomicBoolean firstHandled = new AtomicBoolean(); - return ((TestDuplexConnection) c) - .receive() - .next() - .doOnNext(__ -> firstHandled.set(true)) - .doOnCancel( - () -> { - if (firstHandled.compareAndSet(false, true)) { - c.dispose(); - } - }) - .map(b -> Tuples.of(b, c)); - }, - framesStore, - Duration.ofMinutes(1), - Retry.indefinitely(), - true); - - final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = - new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); - session.setKeepAliveSupport(keepAliveSupport); - - // connection is active. just advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - // deactivate connection - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time so new connection is received - virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); - // timeout should not terminate current connection - assertThat(transport.testConnection().isDisposed()).isFalse(); - - // send RESUME_OK frame - transport.testConnection().addToReceivedBuffer(ResumeOkFrameCodec.encode(transport.alloc(), 0)); - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be terminated - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME) - .matches(ReferenceCounted::release); - - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); - - // disconnects for the second time - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time so new connection is received - virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME) - .matches(ReferenceCounted::release); - - transport - .testConnection() - .addToReceivedBuffer( - ErrorFrameCodec.encode( - transport.alloc(), 0, new ConnectionCloseException("some message"))); - // connection should be closed because of the wrong first frame - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout is still in progress - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); - // should obtain new connection - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_OK frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME) - .matches(ReferenceCounted::release); - - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); - - assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); - assertThat(transport.testConnection().isDisposed()).isTrue(); - - assertThat(session.isDisposed()).isTrue(); - - resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); - transport.alloc().assertHasNoLeaks(); + try { + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ClientRSocketSession session = + new ClientRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.connect().delaySubscription(Duration.ofMillis(1)), + c -> { + AtomicBoolean firstHandled = new AtomicBoolean(); + return ((TestDuplexConnection) c) + .receive() + .next() + .doOnNext(__ -> firstHandled.set(true)) + .doOnCancel( + () -> { + if (firstHandled.compareAndSet(false, true)) { + c.dispose(); + } + }) + .map(b -> Tuples.of(b, c)); + }, + framesStore, + Duration.ofMinutes(1), + Retry.indefinitely(), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + // deactivate connection + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); + // timeout should not terminate current connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + + // send RESUME_OK frame + transport + .testConnection() + .addToReceivedBuffer(ResumeOkFrameCodec.encode(transport.alloc(), 0)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be terminated + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); + + // disconnects for the second time + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + transport + .testConnection() + .addToReceivedBuffer( + ErrorFrameCodec.encode( + transport.alloc(), 0, new ConnectionCloseException("some message"))); + // connection should be closed because of the wrong first frame + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout is still in progress + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); + // should obtain new connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_OK frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); + + assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); + assertThat(transport.testConnection().isDisposed()).isTrue(); + + assertThat(session.isDisposed()).isTrue(); + + resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); + keepAliveSupport.dispose(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } } @Test void sessionTerminationOnWrongFrameTest() { final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); - final TestClientTransport transport = new TestClientTransport(); - final InMemoryResumableFramesStore framesStore = - new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); - - transport.connect().subscribe(); - - final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection( - "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); - - resumableDuplexConnection.receive().subscribe(); - - final ClientRSocketSession session = - new ClientRSocketSession( - Unpooled.EMPTY_BUFFER, - resumableDuplexConnection, - transport.connect().delaySubscription(Duration.ofMillis(1)), - c -> { - AtomicBoolean firstHandled = new AtomicBoolean(); - return ((TestDuplexConnection) c) - .receive() - .next() - .doOnNext(__ -> firstHandled.set(true)) - .doOnCancel( - () -> { - if (firstHandled.compareAndSet(false, true)) { - c.dispose(); - } - }) - .map(b -> Tuples.of(b, c)); - }, - framesStore, - Duration.ofMinutes(1), - Retry.indefinitely(), - true); - - final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = - new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); - session.setKeepAliveSupport(keepAliveSupport); - - // connection is active. just advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - // deactivate connection - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time so new connection is received - virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME) - .matches(ReferenceCounted::release); - - // advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); - // timeout should not terminate current connection - assertThat(transport.testConnection().isDisposed()).isFalse(); - - // send RESUME_OK frame - transport.testConnection().addToReceivedBuffer(ResumeOkFrameCodec.encode(transport.alloc(), 0)); - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be terminated - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); - - // disconnects for the second time - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time so new connection is received - virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME) - .matches(ReferenceCounted::release); - - // Send KEEPALIVE frame as a first frame - transport - .testConnection() - .addToReceivedBuffer( - KeepAliveFrameCodec.encode(transport.alloc(), false, 0, Unpooled.EMPTY_BUFFER)); - - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); - - assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); - assertThat(transport.testConnection().isDisposed()).isTrue(); - assertThat(session.isDisposed()).isTrue(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.ERROR) - .matches(ReferenceCounted::release); - - resumableDuplexConnection - .onClose() - .as(StepVerifier::create) - .expectErrorMessage("RESUME_OK frame must be received before any others") - .verify(); - transport.alloc().assertHasNoLeaks(); + try { + + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ClientRSocketSession session = + new ClientRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.connect().delaySubscription(Duration.ofMillis(1)), + c -> { + AtomicBoolean firstHandled = new AtomicBoolean(); + return ((TestDuplexConnection) c) + .receive() + .next() + .doOnNext(__ -> firstHandled.set(true)) + .doOnCancel( + () -> { + if (firstHandled.compareAndSet(false, true)) { + c.dispose(); + } + }) + .map(b -> Tuples.of(b, c)); + }, + framesStore, + Duration.ofMinutes(1), + Retry.indefinitely(), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + // deactivate connection + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); + // timeout should not terminate current connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + + // send RESUME_OK frame + transport + .testConnection() + .addToReceivedBuffer(ResumeOkFrameCodec.encode(transport.alloc(), 0)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be terminated + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); + + // disconnects for the second time + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + // Send KEEPALIVE frame as a first frame + transport + .testConnection() + .addToReceivedBuffer( + KeepAliveFrameCodec.encode(transport.alloc(), false, 0, Unpooled.EMPTY_BUFFER)); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(30)); + + assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); + assertThat(transport.testConnection().isDisposed()).isTrue(); + assertThat(session.isDisposed()).isTrue(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.ERROR) + .matches(ReferenceCounted::release); + + resumableDuplexConnection + .onClose() + .as(StepVerifier::create) + .expectErrorMessage("RESUME_OK frame must be received before any others") + .verify(); + keepAliveSupport.dispose(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } } @Test void shouldErrorWithNoRetriesOnErrorFrameTest() { final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); - final TestClientTransport transport = new TestClientTransport(); - final InMemoryResumableFramesStore framesStore = - new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); - - transport.connect().subscribe(); - - final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection( - "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); - - resumableDuplexConnection.receive().subscribe(); - - final ClientRSocketSession session = - new ClientRSocketSession( - Unpooled.EMPTY_BUFFER, - resumableDuplexConnection, - transport.connect().delaySubscription(Duration.ofMillis(1)), - c -> { - AtomicBoolean firstHandled = new AtomicBoolean(); - return ((TestDuplexConnection) c) - .receive() - .next() - .doOnNext(__ -> firstHandled.set(true)) - .doOnCancel( - () -> { - if (firstHandled.compareAndSet(false, true)) { - c.dispose(); - } - }) - .map(b -> Tuples.of(b, c)); - }, - framesStore, - Duration.ofMinutes(1), - Retry.indefinitely(), - true); - - final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = - new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); - session.setKeepAliveSupport(keepAliveSupport); - - // connection is active. just advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - // deactivate connection - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time so new connection is received - virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME) - .matches(ReferenceCounted::release); - - // advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); - // timeout should not terminate current connection - assertThat(transport.testConnection().isDisposed()).isFalse(); - - // send REJECTED_RESUME_ERROR frame - transport - .testConnection() - .addToReceivedBuffer( - ErrorFrameCodec.encode( - transport.alloc(), 0, new RejectedResumeException("failed resumption"))); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // timeout should be terminated - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isTrue(); - - resumableDuplexConnection - .onClose() - .as(StepVerifier::create) - .expectError(RejectedResumeException.class) - .verify(); - transport.alloc().assertHasNoLeaks(); + try { + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ClientRSocketSession session = + new ClientRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.connect().delaySubscription(Duration.ofMillis(1)), + c -> { + AtomicBoolean firstHandled = new AtomicBoolean(); + return ((TestDuplexConnection) c) + .receive() + .next() + .doOnNext(__ -> firstHandled.set(true)) + .doOnCancel( + () -> { + if (firstHandled.compareAndSet(false, true)) { + c.dispose(); + } + }) + .map(b -> Tuples.of(b, c)); + }, + framesStore, + Duration.ofMinutes(1), + Retry.indefinitely(), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + // deactivate connection + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time so new connection is received + virtualTimeScheduler.advanceTimeBy(Duration.ofMillis(1)); + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME) + .matches(ReferenceCounted::release); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); + // timeout should not terminate current connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + + // send REJECTED_RESUME_ERROR frame + transport + .testConnection() + .addToReceivedBuffer( + ErrorFrameCodec.encode( + transport.alloc(), 0, new RejectedResumeException("failed resumption"))); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // timeout should be terminated + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isTrue(); + + resumableDuplexConnection + .onClose() + .as(StepVerifier::create) + .expectError(RejectedResumeException.class) + .verify(); + keepAliveSupport.dispose(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } } @Test void shouldTerminateConnectionOnIllegalStateInKeepAliveFrame() { final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); - final TestClientTransport transport = new TestClientTransport(); - final InMemoryResumableFramesStore framesStore = - new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); - - transport.connect().subscribe(); - - final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection( - "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); - - resumableDuplexConnection.receive().subscribe(); - - final ClientRSocketSession session = - new ClientRSocketSession( - Unpooled.EMPTY_BUFFER, - resumableDuplexConnection, - transport.connect().delaySubscription(Duration.ofMillis(1)), - c -> { - AtomicBoolean firstHandled = new AtomicBoolean(); - return ((TestDuplexConnection) c) - .receive() - .next() - .doOnNext(__ -> firstHandled.set(true)) - .doOnCancel( - () -> { - if (firstHandled.compareAndSet(false, true)) { - c.dispose(); - } - }) - .map(b -> Tuples.of(b, c)); - }, - framesStore, - Duration.ofMinutes(1), - Retry.indefinitely(), - true); - - final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = - new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); - keepAliveSupport.resumeState(session); - session.setKeepAliveSupport(keepAliveSupport); - - // connection is active. just advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - final ByteBuf keepAliveFrame = - KeepAliveFrameCodec.encode(transport.alloc(), false, 1529, Unpooled.EMPTY_BUFFER); - keepAliveSupport.receive(keepAliveFrame); - keepAliveFrame.release(); - - assertThat(transport.testConnection().isDisposed()).isTrue(); - // timeout should be terminated - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isTrue(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.ERROR) - .matches(ReferenceCounted::release); - - resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); - - transport.alloc().assertHasNoLeaks(); + try { + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ClientRSocketSession session = + new ClientRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.connect().delaySubscription(Duration.ofMillis(1)), + c -> { + AtomicBoolean firstHandled = new AtomicBoolean(); + return ((TestDuplexConnection) c) + .receive() + .next() + .doOnNext(__ -> firstHandled.set(true)) + .doOnCancel( + () -> { + if (firstHandled.compareAndSet(false, true)) { + c.dispose(); + } + }) + .map(b -> Tuples.of(b, c)); + }, + framesStore, + Duration.ofMinutes(1), + Retry.indefinitely(), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + keepAliveSupport.resumeState(session); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + final ByteBuf keepAliveFrame = + KeepAliveFrameCodec.encode(transport.alloc(), false, 1529, Unpooled.EMPTY_BUFFER); + keepAliveSupport.receive(keepAliveFrame); + keepAliveFrame.release(); + + assertThat(transport.testConnection().isDisposed()).isTrue(); + // timeout should be terminated + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isTrue(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.ERROR) + .matches(ReferenceCounted::release); + + resumableDuplexConnection.onClose().as(StepVerifier::create).expectError().verify(); + keepAliveSupport.dispose(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } } } diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java index a3a682d94..b5625bf8e 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/ServerRSocketSessionTest.java @@ -22,161 +22,169 @@ public class ServerRSocketSessionTest { @Test void sessionTimeoutSmokeTest() { final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); - final TestClientTransport transport = new TestClientTransport(); - final InMemoryResumableFramesStore framesStore = - new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); - - transport.connect().subscribe(); - - final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection( - "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); - - resumableDuplexConnection.receive().subscribe(); - - final ServerRSocketSession session = - new ServerRSocketSession( - Unpooled.EMPTY_BUFFER, - resumableDuplexConnection, - transport.testConnection(), - framesStore, - Duration.ofMinutes(1), - true); - - final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = - new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); - session.setKeepAliveSupport(keepAliveSupport); - - // connection is active. just advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - // deactivate connection - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // resubscribe so a new connection is generated - transport.connect().subscribe(); - - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); - // timeout should not terminate current connection - assertThat(transport.testConnection().isDisposed()).isFalse(); - - // send RESUME frame - final ByteBuf resumeFrame = - ResumeFrameCodec.encode(transport.alloc(), Unpooled.EMPTY_BUFFER, 0, 0); - session.resumeWith(resumeFrame, transport.testConnection()); - resumeFrame.release(); - - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be terminated - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.RESUME_OK) - .matches(ReferenceCounted::release); - - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); - - // disconnects for the second time - transport.testConnection().dispose(); - assertThat(transport.testConnection().isDisposed()).isTrue(); - // ensures timeout has been started - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - transport.connect().subscribe(); - - assertThat(transport.testConnection().isDisposed()).isFalse(); - // timeout should be still active since no RESUME_Ok frame has been received yet - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isFalse(); - - // advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(61)); - - final ByteBuf resumeFrame1 = - ResumeFrameCodec.encode(transport.alloc(), Unpooled.EMPTY_BUFFER, 0, 0); - session.resumeWith(resumeFrame1, transport.testConnection()); - resumeFrame1.release(); - - // should obtain new connection - assertThat(transport.testConnection().isDisposed()).isTrue(); - // timeout should be still active since no RESUME_OK frame has been received yet - assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); - assertThat(session.isDisposed()).isTrue(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.ERROR) - .matches(ReferenceCounted::release); - - resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); - transport.alloc().assertHasNoLeaks(); + try { + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ServerRSocketSession session = + new ServerRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.testConnection(), + framesStore, + Duration.ofMinutes(1), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + // deactivate connection + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // resubscribe so a new connection is generated + transport.connect().subscribe(); + + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(50)); + // timeout should not terminate current connection + assertThat(transport.testConnection().isDisposed()).isFalse(); + + // send RESUME frame + final ByteBuf resumeFrame = + ResumeFrameCodec.encode(transport.alloc(), Unpooled.EMPTY_BUFFER, 0, 0); + session.resumeWith(resumeFrame, transport.testConnection()); + resumeFrame.release(); + + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be terminated + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.RESUME_OK) + .matches(ReferenceCounted::release); + + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(15)); + + // disconnects for the second time + transport.testConnection().dispose(); + assertThat(transport.testConnection().isDisposed()).isTrue(); + // ensures timeout has been started + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + transport.connect().subscribe(); + + assertThat(transport.testConnection().isDisposed()).isFalse(); + // timeout should be still active since no RESUME_Ok frame has been received yet + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isFalse(); + + // advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(61)); + + final ByteBuf resumeFrame1 = + ResumeFrameCodec.encode(transport.alloc(), Unpooled.EMPTY_BUFFER, 0, 0); + session.resumeWith(resumeFrame1, transport.testConnection()); + resumeFrame1.release(); + + // should obtain new connection + assertThat(transport.testConnection().isDisposed()).isTrue(); + // timeout should be still active since no RESUME_OK frame has been received yet + assertThat(session.s).isEqualTo(Operators.cancelledSubscription()); + assertThat(session.isDisposed()).isTrue(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.ERROR) + .matches(ReferenceCounted::release); + + resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } } @Test void shouldTerminateConnectionOnIllegalStateInKeepAliveFrame() { final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); - final TestClientTransport transport = new TestClientTransport(); - final InMemoryResumableFramesStore framesStore = - new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); - - transport.connect().subscribe(); - - final ResumableDuplexConnection resumableDuplexConnection = - new ResumableDuplexConnection( - "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); - - resumableDuplexConnection.receive().subscribe(); - - final ServerRSocketSession session = - new ServerRSocketSession( - Unpooled.EMPTY_BUFFER, - resumableDuplexConnection, - transport.testConnection(), - framesStore, - Duration.ofMinutes(1), - true); - - final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = - new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); - keepAliveSupport.resumeState(session); - session.setKeepAliveSupport(keepAliveSupport); - - // connection is active. just advance time - virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); - assertThat(session.s).isNull(); - assertThat(session.isDisposed()).isFalse(); - - final ByteBuf keepAliveFrame = - KeepAliveFrameCodec.encode(transport.alloc(), false, 1529, Unpooled.EMPTY_BUFFER); - keepAliveSupport.receive(keepAliveFrame); - keepAliveFrame.release(); - - assertThat(transport.testConnection().isDisposed()).isTrue(); - // timeout should be terminated - assertThat(session.s).isNotNull(); - assertThat(session.isDisposed()).isTrue(); - - FrameAssert.assertThat(transport.testConnection().pollFrame()) - .hasStreamIdZero() - .typeOf(FrameType.ERROR) - .matches(ReferenceCounted::release); - - resumableDuplexConnection.onClose().as(StepVerifier::create).expectComplete().verify(); - - transport.alloc().assertHasNoLeaks(); + try { + final TestClientTransport transport = new TestClientTransport(); + final InMemoryResumableFramesStore framesStore = + new InMemoryResumableFramesStore("test", Unpooled.EMPTY_BUFFER, 100); + + transport.connect().subscribe(); + + final ResumableDuplexConnection resumableDuplexConnection = + new ResumableDuplexConnection( + "test", Unpooled.EMPTY_BUFFER, transport.testConnection(), framesStore); + + resumableDuplexConnection.receive().subscribe(); + + final ServerRSocketSession session = + new ServerRSocketSession( + Unpooled.EMPTY_BUFFER, + resumableDuplexConnection, + transport.testConnection(), + framesStore, + Duration.ofMinutes(1), + true); + + final KeepAliveSupport.ClientKeepAliveSupport keepAliveSupport = + new KeepAliveSupport.ClientKeepAliveSupport(transport.alloc(), 1000000, 10000000); + keepAliveSupport.resumeState(session); + session.setKeepAliveSupport(keepAliveSupport); + + // connection is active. just advance time + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(10)); + assertThat(session.s).isNull(); + assertThat(session.isDisposed()).isFalse(); + + final ByteBuf keepAliveFrame = + KeepAliveFrameCodec.encode(transport.alloc(), false, 1529, Unpooled.EMPTY_BUFFER); + keepAliveSupport.receive(keepAliveFrame); + keepAliveFrame.release(); + + assertThat(transport.testConnection().isDisposed()).isTrue(); + // timeout should be terminated + assertThat(session.s).isNotNull(); + assertThat(session.isDisposed()).isTrue(); + + FrameAssert.assertThat(transport.testConnection().pollFrame()) + .hasStreamIdZero() + .typeOf(FrameType.ERROR) + .matches(ReferenceCounted::release); + + resumableDuplexConnection.onClose().as(StepVerifier::create).expectError().verify(); + keepAliveSupport.dispose(); + transport.alloc().assertHasNoLeaks(); + } finally { + VirtualTimeScheduler.reset(); + } } } diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java index e307627ff..f02bc99a4 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java @@ -6,11 +6,13 @@ import io.rsocket.DuplexConnection; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.transport.ClientTransport; +import java.time.Duration; import reactor.core.publisher.Mono; public class TestClientTransport implements ClientTransport { private final LeaksTrackingByteBufAllocator allocator = - LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + LeaksTrackingByteBufAllocator.instrument( + ByteBufAllocator.DEFAULT, Duration.ofSeconds(1), "client"); private volatile TestDuplexConnection testDuplexConnection; diff --git a/rsocket-examples/build.gradle b/rsocket-examples/build.gradle index e339b170a..4059eb957 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -27,9 +27,11 @@ dependencies { implementation "io.micrometer:micrometer-core" implementation "io.micrometer:micrometer-tracing" implementation project(":rsocket-micrometer") - testImplementation 'org.awaitility:awaitility' implementation 'com.netflix.concurrency-limits:concurrency-limits-core' + implementation "io.micrometer:micrometer-core" + implementation "io.micrometer:micrometer-tracing" + implementation project(":rsocket-micrometer") runtimeOnly 'ch.qos.logback:logback-classic' @@ -38,6 +40,7 @@ dependencies { testImplementation 'org.mockito:mockito-core' testImplementation 'org.assertj:assertj-core' testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.awaitility:awaitility' testImplementation "io.micrometer:micrometer-test" testImplementation "io.micrometer:micrometer-tracing-integration-test" diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java index 625f8fcb1..cd96584ed 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java @@ -78,8 +78,6 @@ public void testRangeOfConsumers() { .block(); Flux.range(1, 6).flatMap(i -> consumer("connection number -> " + i)).blockLast(); - System.out.println("here"); - } finally { server.dispose(); } diff --git a/rsocket-micrometer/build.gradle b/rsocket-micrometer/build.gradle index 77bdcd08f..debf02f34 100644 --- a/rsocket-micrometer/build.gradle +++ b/rsocket-micrometer/build.gradle @@ -24,7 +24,7 @@ dependencies { api project(':rsocket-core') api 'io.micrometer:micrometer-observation' api 'io.micrometer:micrometer-core' - compileOnly 'io.micrometer:micrometer-tracing' + api 'io.micrometer:micrometer-tracing' implementation 'org.slf4j:slf4j-api' diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java index c7ba1c772..2c10fc78d 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketObservationConvention.java @@ -22,7 +22,7 @@ * Default {@link RSocketRequesterObservationConvention} implementation. * * @author Marcin Grzejszczak - * @since 2.0.0 + * @since 1.1.4 */ class DefaultRSocketObservationConvention { diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java index 824023b86..73e04b749 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketRequesterObservationConvention.java @@ -25,7 +25,7 @@ * Default {@link RSocketRequesterObservationConvention} implementation. * * @author Marcin Grzejszczak - * @since 2.0.0 + * @since 1.1.4 */ public class DefaultRSocketRequesterObservationConvention extends DefaultRSocketObservationConvention implements RSocketRequesterObservationConvention { @@ -38,11 +38,11 @@ public DefaultRSocketRequesterObservationConvention(RSocketContext rSocketContex public KeyValues getLowCardinalityKeyValues(RSocketContext context) { KeyValues values = KeyValues.of( - RSocketDocumentedObservation.ResponderTags.REQUEST_TYPE.withValue( + RSocketObservationDocumentation.ResponderTags.REQUEST_TYPE.withValue( context.frameType.name())); if (StringUtils.isNotBlank(context.route)) { values = - values.and(RSocketDocumentedObservation.ResponderTags.ROUTE.withValue(context.route)); + values.and(RSocketObservationDocumentation.ResponderTags.ROUTE.withValue(context.route)); } return values; } diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java index a6fa387b1..5318c1b37 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/DefaultRSocketResponderObservationConvention.java @@ -25,7 +25,7 @@ * Default {@link RSocketRequesterObservationConvention} implementation. * * @author Marcin Grzejszczak - * @since 2.0.0 + * @since 1.1.4 */ public class DefaultRSocketResponderObservationConvention extends DefaultRSocketObservationConvention implements RSocketResponderObservationConvention { @@ -38,10 +38,10 @@ public DefaultRSocketResponderObservationConvention(RSocketContext rSocketContex public KeyValues getLowCardinalityKeyValues(RSocketContext context) { KeyValues tags = KeyValues.of( - RSocketDocumentedObservation.ResponderTags.REQUEST_TYPE.withValue( + RSocketObservationDocumentation.ResponderTags.REQUEST_TYPE.withValue( context.frameType.name())); if (StringUtils.isNotBlank(context.route)) { - tags = tags.and(RSocketDocumentedObservation.ResponderTags.ROUTE.withValue(context.route)); + tags = tags.and(RSocketObservationDocumentation.ResponderTags.ROUTE.withValue(context.route)); } return tags; } diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java index f0d72cbee..fb80ea317 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationRequesterRSocketProxy.java @@ -19,7 +19,7 @@ import io.micrometer.common.util.StringUtils; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; -import io.micrometer.observation.docs.DocumentedObservation; +import io.micrometer.observation.docs.ObservationDocumentation; import io.netty.buffer.ByteBuf; import io.rsocket.Payload; import io.rsocket.RSocket; @@ -32,6 +32,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.annotation.Nullable; import reactor.util.context.ContextView; /** @@ -39,17 +40,28 @@ * * @author Marcin Grzejszczak * @author Oleh Dokuka - * @since 3.1.0 + * @since 1.1.4 */ public class ObservationRequesterRSocketProxy extends RSocketProxy { + /** Aligned with ObservationThreadLocalAccessor#KEY */ + private static final String MICROMETER_OBSERVATION_KEY = "micrometer.observation"; + private final ObservationRegistry observationRegistry; - private RSocketRequesterObservationConvention observationConvention; + @Nullable private final RSocketRequesterObservationConvention observationConvention; public ObservationRequesterRSocketProxy(RSocket source, ObservationRegistry observationRegistry) { + this(source, observationRegistry, null); + } + + public ObservationRequesterRSocketProxy( + RSocket source, + ObservationRegistry observationRegistry, + RSocketRequesterObservationConvention observationConvention) { super(source); this.observationRegistry = observationRegistry; + this.observationConvention = observationConvention; } @Override @@ -58,7 +70,7 @@ public Mono fireAndForget(Payload payload) { super::fireAndForget, payload, FrameType.REQUEST_FNF, - RSocketDocumentedObservation.RSOCKET_REQUESTER_FNF); + RSocketObservationDocumentation.RSOCKET_REQUESTER_FNF); } @Override @@ -67,24 +79,16 @@ public Mono requestResponse(Payload payload) { super::requestResponse, payload, FrameType.REQUEST_RESPONSE, - RSocketDocumentedObservation.RSOCKET_REQUESTER_REQUEST_RESPONSE); + RSocketObservationDocumentation.RSOCKET_REQUESTER_REQUEST_RESPONSE); } Mono setObservation( Function> input, Payload payload, FrameType frameType, - DocumentedObservation observation) { + ObservationDocumentation observation) { return Mono.deferContextual( - contextView -> { - if (contextView.hasKey(Observation.class)) { - Observation parent = contextView.get(Observation.class); - try (Observation.Scope scope = parent.openScope()) { - return observe(input, payload, frameType, observation); - } - } - return observe(input, payload, frameType, observation); - }); + contextView -> observe(input, payload, frameType, observation, contextView)); } private String route(Payload payload) { @@ -107,18 +111,22 @@ private Mono observe( Function> input, Payload payload, FrameType frameType, - DocumentedObservation obs) { + ObservationDocumentation obs, + ContextView contextView) { String route = route(payload); RSocketContext rSocketContext = new RSocketContext( payload, payload.sliceMetadata(), frameType, route, RSocketContext.Side.REQUESTER); + Observation parentObservation = contextView.getOrDefault(MICROMETER_OBSERVATION_KEY, null); Observation observation = - obs.start( - this.observationConvention, - new DefaultRSocketRequesterObservationConvention(rSocketContext), - rSocketContext, - observationRegistry); + obs.observation( + this.observationConvention, + new DefaultRSocketRequesterObservationConvention(rSocketContext), + () -> rSocketContext, + observationRegistry) + .parentObservation(parentObservation); setContextualName(frameType, route, observation); + observation.start(); Payload newPayload = payload; if (rSocketContext.modifiedPayload != null) { newPayload = rSocketContext.modifiedPayload; @@ -126,26 +134,17 @@ private Mono observe( return input .apply(newPayload) .doOnError(observation::error) - .doFinally(signalType -> observation.stop()); - } - - private Observation observation(ContextView contextView) { - if (contextView.hasKey(Observation.class)) { - return contextView.get(Observation.class); - } - return null; + .doFinally(signalType -> observation.stop()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, observation)); } @Override public Flux requestStream(Payload payload) { - return Flux.deferContextual( - contextView -> - setObservation( - super::requestStream, - payload, - contextView, - FrameType.REQUEST_STREAM, - RSocketDocumentedObservation.RSOCKET_REQUESTER_REQUEST_STREAM)); + return observationFlux( + super::requestStream, + payload, + FrameType.REQUEST_STREAM, + RSocketObservationDocumentation.RSOCKET_REQUESTER_REQUEST_STREAM); } @Override @@ -155,37 +154,21 @@ public Flux requestChannel(Publisher inbound) { (firstSignal, flux) -> { final Payload firstPayload = firstSignal.get(); if (firstPayload != null) { - return setObservation( + return observationFlux( p -> super.requestChannel(flux.skip(1).startWith(p)), firstPayload, - firstSignal.getContextView(), FrameType.REQUEST_CHANNEL, - RSocketDocumentedObservation.RSOCKET_REQUESTER_REQUEST_CHANNEL); + RSocketObservationDocumentation.RSOCKET_REQUESTER_REQUEST_CHANNEL); } return flux; }); } - private Flux setObservation( - Function> input, - Payload payload, - ContextView contextView, - FrameType frameType, - DocumentedObservation obs) { - Observation parentObservation = observation(contextView); - if (parentObservation == null) { - return observationFlux(input, payload, frameType, obs); - } - try (Observation.Scope scope = parentObservation.openScope()) { - return observationFlux(input, payload, frameType, obs); - } - } - private Flux observationFlux( Function> input, Payload payload, FrameType frameType, - DocumentedObservation obs) { + ObservationDocumentation obs) { return Flux.deferContextual( contextView -> { String route = route(payload); @@ -196,17 +179,22 @@ private Flux observationFlux( frameType, route, RSocketContext.Side.REQUESTER); + Observation parentObservation = + contextView.getOrDefault(MICROMETER_OBSERVATION_KEY, null); Observation newObservation = - obs.start( - this.observationConvention, - new DefaultRSocketRequesterObservationConvention(rSocketContext), - rSocketContext, - this.observationRegistry); + obs.observation( + this.observationConvention, + new DefaultRSocketRequesterObservationConvention(rSocketContext), + () -> rSocketContext, + this.observationRegistry) + .parentObservation(parentObservation); setContextualName(frameType, route, newObservation); + newObservation.start(); return input .apply(rSocketContext.modifiedPayload) .doOnError(newObservation::error) - .doFinally(signalType -> newObservation.stop()); + .doFinally(signalType -> newObservation.stop()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); }); } @@ -217,8 +205,4 @@ private void setContextualName(FrameType frameType, String route, Observation ne newObservation.contextualName(frameType.name()); } } - - public void setObservationConvention(RSocketRequesterObservationConvention convention) { - this.observationConvention = convention; - } } diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java index 968ddc014..9ed27adf3 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/ObservationResponderRSocketProxy.java @@ -30,23 +30,34 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.annotation.Nullable; /** * Tracing representation of a {@link RSocketProxy} for the responder. * * @author Marcin Grzejszczak * @author Oleh Dokuka - * @since 3.1.0 + * @since 1.1.4 */ public class ObservationResponderRSocketProxy extends RSocketProxy { + /** Aligned with ObservationThreadLocalAccessor#KEY */ + private static final String MICROMETER_OBSERVATION_KEY = "micrometer.observation"; private final ObservationRegistry observationRegistry; - private RSocketResponderObservationConvention observationConvention; + @Nullable private final RSocketResponderObservationConvention observationConvention; public ObservationResponderRSocketProxy(RSocket source, ObservationRegistry observationRegistry) { + this(source, observationRegistry, null); + } + + public ObservationResponderRSocketProxy( + RSocket source, + ObservationRegistry observationRegistry, + RSocketResponderObservationConvention observationConvention) { super(source); this.observationRegistry = observationRegistry; + this.observationConvention = observationConvention; } @Override @@ -63,18 +74,19 @@ public Mono fireAndForget(Payload payload) { route, RSocketContext.Side.RESPONDER); Observation newObservation = - startObservation(RSocketDocumentedObservation.RSOCKET_RESPONDER_FNF, rSocketContext); + startObservation(RSocketObservationDocumentation.RSOCKET_RESPONDER_FNF, rSocketContext); return super.fireAndForget(rSocketContext.modifiedPayload) .doOnError(newObservation::error) - .doFinally(signalType -> newObservation.stop()); + .doFinally(signalType -> newObservation.stop()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); } private Observation startObservation( - RSocketDocumentedObservation observation, RSocketContext rSocketContext) { + RSocketObservationDocumentation observation, RSocketContext rSocketContext) { return observation.start( this.observationConvention, new DefaultRSocketResponderObservationConvention(rSocketContext), - rSocketContext, + () -> rSocketContext, this.observationRegistry); } @@ -91,10 +103,11 @@ public Mono requestResponse(Payload payload) { RSocketContext.Side.RESPONDER); Observation newObservation = startObservation( - RSocketDocumentedObservation.RSOCKET_RESPONDER_REQUEST_RESPONSE, rSocketContext); + RSocketObservationDocumentation.RSOCKET_RESPONDER_REQUEST_RESPONSE, rSocketContext); return super.requestResponse(rSocketContext.modifiedPayload) .doOnError(newObservation::error) - .doFinally(signalType -> newObservation.stop()); + .doFinally(signalType -> newObservation.stop()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); } @Override @@ -106,10 +119,11 @@ public Flux requestStream(Payload payload) { payload, sliceMetadata, FrameType.REQUEST_STREAM, route, RSocketContext.Side.RESPONDER); Observation newObservation = startObservation( - RSocketDocumentedObservation.RSOCKET_RESPONDER_REQUEST_STREAM, rSocketContext); + RSocketObservationDocumentation.RSOCKET_RESPONDER_REQUEST_STREAM, rSocketContext); return super.requestStream(rSocketContext.modifiedPayload) .doOnError(newObservation::error) - .doFinally(signalType -> newObservation.stop()); + .doFinally(signalType -> newObservation.stop()) + .contextWrite(context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); } @Override @@ -130,14 +144,16 @@ public Flux requestChannel(Publisher payloads) { RSocketContext.Side.RESPONDER); Observation newObservation = startObservation( - RSocketDocumentedObservation.RSOCKET_RESPONDER_REQUEST_CHANNEL, + RSocketObservationDocumentation.RSOCKET_RESPONDER_REQUEST_CHANNEL, rSocketContext); if (StringUtils.isNotBlank(route)) { newObservation.contextualName(rSocketContext.frameType.name() + " " + route); } return super.requestChannel(flux.skip(1).startWith(rSocketContext.modifiedPayload)) .doOnError(newObservation::error) - .doFinally(signalType -> newObservation.stop()); + .doFinally(signalType -> newObservation.stop()) + .contextWrite( + context -> context.put(MICROMETER_OBSERVATION_KEY, newObservation)); } return flux; }); @@ -160,8 +176,4 @@ private String route(Payload payload, ByteBuf headers) { } return null; } - - public void setObservationConvention(RSocketResponderObservationConvention convention) { - this.observationConvention = convention; - } } diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketObservationDocumentation.java similarity index 97% rename from rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java rename to rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketObservationDocumentation.java index 5d6661baf..1be6b4599 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketDocumentedObservation.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketObservationDocumentation.java @@ -19,9 +19,9 @@ import io.micrometer.common.docs.KeyName; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationConvention; -import io.micrometer.observation.docs.DocumentedObservation; +import io.micrometer.observation.docs.ObservationDocumentation; -enum RSocketDocumentedObservation implements DocumentedObservation { +enum RSocketObservationDocumentation implements ObservationDocumentation { /** Observation created on the RSocket responder side. */ RSOCKET_RESPONDER { diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java index 83bbef9b1..d795f81b5 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterObservationConvention.java @@ -23,7 +23,7 @@ * {@link ObservationConvention} for RSocket requester {@link RSocketContext}. * * @author Marcin Grzejszczak - * @since 2.0.0 + * @since 1.1.4 */ public interface RSocketRequesterObservationConvention extends ObservationConvention { diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java index 3f6b5dc52..996267d4a 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketRequesterTracingObservationHandler.java @@ -69,6 +69,10 @@ public Tracer getTracer() { public void onStart(RSocketContext context) { Payload payload = context.payload; Span.Builder spanBuilder = this.tracer.spanBuilder(); + Span parentSpan = getParentSpan(context); + if (parentSpan != null) { + spanBuilder.setParent(parentSpan.context()); + } Span span = spanBuilder.kind(Span.Kind.PRODUCER).start(); log.debug("Extracted result from context or thread local {}", span); // TODO: newmetadata returns an empty composite byte buf @@ -85,7 +89,10 @@ public void onStart(RSocketContext context) { @Override public void onError(RSocketContext context) { - context.getError().ifPresent(throwable -> getRequiredSpan(context).error(throwable)); + Throwable error = context.getError(); + if (error != null) { + getRequiredSpan(context).error(error); + } } @Override diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java index f2bbf8716..a5d6808bd 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderObservationConvention.java @@ -23,7 +23,7 @@ * {@link ObservationConvention} for RSocket responder {@link RSocketContext}. * * @author Marcin Grzejszczak - * @since 2.0.0 + * @since 1.1.4 */ public interface RSocketResponderObservationConvention extends ObservationConvention { diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java index ae06a4307..e3975b577 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/observation/RSocketResponderTracingObservationHandler.java @@ -72,7 +72,10 @@ public void onStart(RSocketContext context) { @Override public void onError(RSocketContext context) { - context.getError().ifPresent(throwable -> getRequiredSpan(context).error(throwable)); + Throwable error = context.getError(); + if (error != null) { + getRequiredSpan(context).error(error); + } } @Override diff --git a/rsocket-test/build.gradle b/rsocket-test/build.gradle index d95e9bd41..bcdf88f28 100644 --- a/rsocket-test/build.gradle +++ b/rsocket-test/build.gradle @@ -28,6 +28,8 @@ dependencies { implementation 'io.projectreactor:reactor-test' implementation 'org.assertj:assertj-core' implementation 'org.mockito:mockito-core' + implementation 'org.awaitility:awaitility' + implementation 'org.slf4j:slf4j-api' } jar { diff --git a/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java b/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java index 139ae146b..46e807b09 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java +++ b/rsocket-test/src/main/java/io/rsocket/test/LeaksTrackingByteBufAllocator.java @@ -5,20 +5,25 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; +import io.netty.util.IllegalReferenceCountException; import io.netty.util.ResourceLeakDetector; import java.lang.reflect.Field; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import org.assertj.core.api.Assertions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Additional Utils which allows to decorate a ByteBufAllocator and track/assertOnLeaks all created * ByteBuffs */ public class LeaksTrackingByteBufAllocator implements ByteBufAllocator { + static final Logger LOGGER = LoggerFactory.getLogger(LeaksTrackingByteBufAllocator.class); /** * Allows to instrument any given the instance of ByteBufAllocator @@ -83,7 +88,7 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { return this; } - System.out.println(tag + " await buffers to be released"); + LOGGER.debug(tag + " await buffers to be released"); for (int i = 0; i < 100; i++) { System.gc(); parkNanos(1000); @@ -92,22 +97,31 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { } } - Assertions.assertThat(unreleased) - .allMatch( - bb -> { - final boolean checkResult = bb.refCnt() == 0; - - if (!checkResult) { - try { - System.out.println(tag + " " + resolveTrackingInfo(bb)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - return checkResult; - }, - tag); + Set collected = new HashSet<>(); + for (ByteBuf buf : unreleased) { + if (buf.refCnt() != 0) { + try { + collected.add(buf); + } catch (IllegalReferenceCountException ignored) { + // fine to ignore if throws because of refCnt + } + } + } + + Assertions.assertThat( + collected + .stream() + .filter(bb -> bb.refCnt() != 0) + .peek( + bb -> { + try { + LOGGER.debug(tag + " " + resolveTrackingInfo(bb)); + } catch (Exception e) { + e.printStackTrace(); + } + })) + .describedAs("[" + tag + "] all buffers expected to be released but got ") + .isEmpty(); } finally { tracker.clear(); } diff --git a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java index 570a7de2f..1fcca97db 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,12 +46,14 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; import org.assertj.core.api.Assertions; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.reactivestreams.Subscription; @@ -61,9 +63,10 @@ import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Operators; +import reactor.core.publisher.Sinks; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; @@ -80,7 +83,6 @@ public interface TransportTest { Payload LARGE_PAYLOAD = ByteBufPayload.create(LARGE_DATA, LARGE_DATA); static String read(String resourceName) { - try (BufferedReader br = new BufferedReader( new InputStreamReader( @@ -93,38 +95,55 @@ static String read(String resourceName) { } } + @BeforeEach + default void setup() { + Hooks.onOperatorDebug(); + } + @AfterEach default void close() { - getTransportPair().responder.awaitAllInteractionTermination(getTimeout()); - getTransportPair().dispose(); - getTransportPair().awaitClosed(); - RuntimeException throwable = - new RuntimeException() { - @Override - public synchronized Throwable fillInStackTrace() { - return this; - } - - @Override - public String getMessage() { - return Arrays.toString(getSuppressed()); - } - }; - try { - getTransportPair().byteBufAllocator2.assertHasNoLeaks(); - } catch (Throwable t) { - throwable = Exceptions.addSuppressed(throwable, t); - } - - try { - getTransportPair().byteBufAllocator1.assertHasNoLeaks(); - } catch (Throwable t) { - throwable = Exceptions.addSuppressed(throwable, t); - } - - if (throwable.getSuppressed().length > 0) { - throw throwable; + logger.debug("------------------Awaiting communication to finish------------------"); + getTransportPair().responder.awaitAllInteractionTermination(getTimeout()); + logger.debug("---------------------Disposing Client And Server--------------------"); + getTransportPair().dispose(); + getTransportPair().awaitClosed(getTimeout()); + logger.debug("------------------------Disposing Schedulers-------------------------"); + Schedulers.parallel().disposeGracefully().timeout(getTimeout(), Mono.empty()).block(); + Schedulers.boundedElastic().disposeGracefully().timeout(getTimeout(), Mono.empty()).block(); + Schedulers.single().disposeGracefully().timeout(getTimeout(), Mono.empty()).block(); + logger.debug("---------------------------Leaks Checking----------------------------"); + RuntimeException throwable = + new RuntimeException() { + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + + @Override + public String getMessage() { + return Arrays.toString(getSuppressed()); + } + }; + + try { + getTransportPair().byteBufAllocator2.assertHasNoLeaks(); + } catch (Throwable t) { + throwable = Exceptions.addSuppressed(throwable, t); + } + + try { + getTransportPair().byteBufAllocator1.assertHasNoLeaks(); + } catch (Throwable t) { + throwable = Exceptions.addSuppressed(throwable, t); + } + + if (throwable.getSuppressed().length > 0) { + throw throwable; + } + } finally { + Hooks.resetOnOperatorDebug(); + Schedulers.resetOnHandleError(); } } @@ -226,7 +245,7 @@ default void requestChannel1() { .requestChannel(Mono.just(createTestPayload(0))) .doOnNext(Payload::release) .as(StepVerifier::create) - .expectNextCount(1) + .thenConsumeWhile(new PayloadPredicate(1)) .expectComplete() .verify(getTimeout()); } @@ -241,7 +260,7 @@ default void requestChannel200_000() { .doOnNext(Payload::release) .limitRate(8) .as(StepVerifier::create) - .expectNextCount(200_000) + .thenConsumeWhile(new PayloadPredicate(200_000)) .expectComplete() .verify(getTimeout()); } @@ -255,7 +274,7 @@ default void largePayloadRequestChannel50() { .requestChannel(payloads) .doOnNext(Payload::release) .as(StepVerifier::create) - .expectNextCount(50) + .thenConsumeWhile(new PayloadPredicate(50)) .expectComplete() .verify(getTimeout()); } @@ -270,7 +289,7 @@ default void requestChannel20_000() { .doOnNext(this::assertChannelPayload) .doOnNext(Payload::release) .as(StepVerifier::create) - .expectNextCount(20_000) + .thenConsumeWhile(new PayloadPredicate(20_000)) .expectComplete() .verify(getTimeout()); } @@ -285,7 +304,7 @@ default void requestChannel2_000_000() { .doOnNext(Payload::release) .limitRate(8) .as(StepVerifier::create) - .expectNextCount(2_000_000) + .thenConsumeWhile(new PayloadPredicate(2_000_000)) .expectComplete() .verify(getTimeout()); } @@ -301,7 +320,7 @@ default void requestChannel3() { .requestChannel(payloads) .doOnNext(Payload::release) .as(publisher -> StepVerifier.create(publisher, 3)) - .expectNextCount(3) + .thenConsumeWhile(new PayloadPredicate(3)) .expectComplete() .verify(getTimeout()); @@ -322,9 +341,13 @@ default void requestChannel256() { }); final Scheduler scheduler = Schedulers.fromExecutorService(Executors.newFixedThreadPool(12)); - Flux.range(0, 1024) - .flatMap(v -> Mono.fromRunnable(() -> check(payloads)).subscribeOn(scheduler), 12) - .blockLast(); + try { + Flux.range(0, 1024) + .flatMap(v -> Mono.fromRunnable(() -> check(payloads)).subscribeOn(scheduler), 12) + .blockLast(); + } finally { + scheduler.disposeGracefully().block(); + } } default void check(Flux payloads) { @@ -333,7 +356,7 @@ default void check(Flux payloads) { .doOnNext(ReferenceCounted::release) .limitRate(8) .as(StepVerifier::create) - .expectNextCount(256) + .thenConsumeWhile(new PayloadPredicate(256)) .as("expected 256 items") .expectComplete() .verify(getTimeout()); @@ -465,6 +488,8 @@ class TransportPair implements Disposable { private static final String metadata = "metadata"; private final boolean withResumability; + private final boolean runClientWithAsyncInterceptors; + private final boolean runServerWithAsyncInterceptors; private final LeaksTrackingByteBufAllocator byteBufAllocator1 = LeaksTrackingByteBufAllocator.instrument( @@ -505,12 +530,15 @@ public TransportPair( BiFunction> serverTransportSupplier, boolean withRandomFragmentation, boolean withResumability) { + Schedulers.onHandleError((t, e) -> e.printStackTrace()); + Schedulers.resetFactory(); + this.withResumability = withResumability; T address = addressSupplier.get(); - final boolean runClientWithAsyncInterceptors = ThreadLocalRandom.current().nextBoolean(); - final boolean runServerWithAsyncInterceptors = ThreadLocalRandom.current().nextBoolean(); + this.runClientWithAsyncInterceptors = ThreadLocalRandom.current().nextBoolean(); + this.runServerWithAsyncInterceptors = ThreadLocalRandom.current().nextBoolean(); ByteBufAllocator allocatorToSupply1; ByteBufAllocator allocatorToSupply2; @@ -535,7 +563,7 @@ public TransportPair( registry .forConnection( (type, duplexConnection) -> - new AsyncDuplexConnection(duplexConnection)) + new AsyncDuplexConnection(duplexConnection, "server")) .forSocketAcceptor( delegate -> (connectionSetupPayload, sendingSocket) -> @@ -583,7 +611,7 @@ public TransportPair( registry .forConnection( (type, duplexConnection) -> - new AsyncDuplexConnection(duplexConnection)) + new AsyncDuplexConnection(duplexConnection, "client")) .forSocketAcceptor( delegate -> (connectionSetupPayload, sendingSocket) -> @@ -625,7 +653,7 @@ public TransportPair( @Override public void dispose() { - server.dispose(); + logger.info("terminating transport pair"); client.dispose(); } @@ -641,21 +669,46 @@ public String expectedPayloadMetadata() { return metadata; } - public void awaitClosed() { - server + public void awaitClosed(Duration timeout) { + logger.info("awaiting termination of transport pair"); + logger.info( + "wrappers combination: client{async=" + + runClientWithAsyncInterceptors + + "; resume=" + + withResumability + + "} server{async=" + + runServerWithAsyncInterceptors + + "; resume=" + + withResumability + + "}"); + client .onClose() - .onErrorResume(__ -> Mono.empty()) - .and(client.onClose().onErrorResume(__ -> Mono.empty())) - .block(Duration.ofMinutes(1)); + .doOnSubscribe(s -> logger.info("Client termination stage=onSubscribe(" + s + ")")) + .doOnEach(s -> logger.info("Client termination stage=" + s)) + .onErrorResume(t -> Mono.empty()) + .doOnTerminate(() -> logger.info("Client terminated. Terminating Server")) + .then(Mono.fromRunnable(server::dispose)) + .then( + server + .onClose() + .doOnSubscribe( + s -> logger.info("Server termination stage=onSubscribe(" + s + ")")) + .doOnEach(s -> logger.info("Server termination stage=" + s))) + .onErrorResume(t -> Mono.empty()) + .block(timeout); + + logger.info("TransportPair has been terminated"); } private static class AsyncDuplexConnection implements DuplexConnection { private final DuplexConnection duplexConnection; + private String tag; private final ByteBufReleaserOperator bufReleaserOperator; - public AsyncDuplexConnection(DuplexConnection duplexConnection) { + public AsyncDuplexConnection(DuplexConnection duplexConnection, String tag) { this.duplexConnection = duplexConnection; + this.tag = tag; this.bufReleaserOperator = new ByteBufReleaserOperator(); } @@ -673,9 +726,11 @@ public void sendErrorAndClose(RSocketErrorException e) { public Flux receive() { return duplexConnection .receive() + .doOnTerminate(() -> logger.info("[" + this + "] Receive is done before PO")) .subscribeOn(Schedulers.boundedElastic()) .doOnNext(ByteBuf::retain) .publishOn(Schedulers.boundedElastic(), Integer.MAX_VALUE) + .doOnTerminate(() -> logger.info("[" + this + "] Receive is done after PO")) .doOnDiscard(ReferenceCounted.class, ReferenceCountUtil::safeRelease) .transform( Operators.lift( @@ -697,13 +752,32 @@ public SocketAddress remoteAddress() { @Override public Mono onClose() { - return duplexConnection.onClose().and(bufReleaserOperator.onClose()); + return Mono.whenDelayError( + duplexConnection + .onClose() + .doOnTerminate(() -> logger.info("[" + this + "] Source Connection is done")), + bufReleaserOperator + .onClose() + .doOnTerminate(() -> logger.info("[" + this + "] BufferReleaser is done"))); } @Override public void dispose() { duplexConnection.dispose(); } + + @Override + public String toString() { + return "AsyncDuplexConnection{" + + "duplexConnection=" + + duplexConnection + + ", tag='" + + tag + + '\'' + + ", bufReleaserOperator=" + + bufReleaserOperator + + '}'; + } } private static class DisconnectingDuplexConnection implements DuplexConnection { @@ -727,7 +801,9 @@ public void dispose() { @Override public Mono onClose() { - return source.onClose(); + return source + .onClose() + .doOnTerminate(() -> logger.info("[" + this + "] Source Connection is done")); } @Override @@ -746,6 +822,8 @@ public void sendErrorAndClose(RSocketErrorException errorException) { public Flux receive() { return source .receive() + .doOnSubscribe( + __ -> logger.warn("Tag {}. Subscribing Connection[{}]", tag, source.hashCode())) .doOnNext( bb -> { if (!receivedFirst) { @@ -772,18 +850,31 @@ public ByteBufAllocator alloc() { public SocketAddress remoteAddress() { return source.remoteAddress(); } + + @Override + public String toString() { + return "DisconnectingDuplexConnection{" + + "tag='" + + tag + + '\'' + + ", source=" + + source + + ", disposables=" + + disposables + + '}'; + } } private static class ByteBufReleaserOperator implements CoreSubscriber, Subscription, Fuseable.QueueSubscription { CoreSubscriber actual; - final MonoProcessor closeableMono; + final Sinks.Empty closeableMonoSink; Subscription s; public ByteBufReleaserOperator() { - this.closeableMono = MonoProcessor.create(); + this.closeableMonoSink = Sinks.unsafe().empty(); } @Override @@ -804,19 +895,19 @@ public void onNext(ByteBuf buf) { } Mono onClose() { - return closeableMono; + return closeableMonoSink.asMono(); } @Override public void onError(Throwable t) { actual.onError(t); - closeableMono.onError(t); + closeableMonoSink.tryEmitError(t); } @Override public void onComplete() { actual.onComplete(); - closeableMono.onComplete(); + closeableMonoSink.tryEmitEmpty(); } @Override @@ -827,7 +918,7 @@ public void request(long n) { @Override public void cancel() { s.cancel(); - closeableMono.onComplete(); + closeableMonoSink.tryEmitEmpty(); } @Override @@ -854,6 +945,40 @@ public boolean isEmpty() { public void clear() { throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); } + + @Override + public String toString() { + return "ByteBufReleaserOperator{" + + "isActualPresent=" + + (actual != null) + + ", " + + "isSubscriptionPresent=" + + (s != null) + + '}'; + } + } + } + + class PayloadPredicate implements Predicate { + final int expectedCnt; + int cnt; + + public PayloadPredicate(int expectedCnt) { + this.expectedCnt = expectedCnt; + } + + @Override + public boolean test(Payload p) { + boolean shouldConsume = cnt++ < expectedCnt; + if (!shouldConsume) { + logger.info( + "Metadata: \n\r{}\n\rData:{}", + p.hasMetadata() + ? new ByteBufRepresentation().fallbackToStringOf(p.sliceMetadata()) + : "Empty", + new ByteBufRepresentation().fallbackToStringOf(p.sliceData())); + } + return shouldConsume; } } } diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java index 113b7a2f8..1b3779e85 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java @@ -79,10 +79,10 @@ public Mono connect() { Sinks.One inSink = Sinks.one(); Sinks.One outSink = Sinks.one(); - UnboundedProcessor in = new UnboundedProcessor(() -> inSink.tryEmitValue(inSink)); - UnboundedProcessor out = new UnboundedProcessor(() -> outSink.tryEmitValue(outSink)); + UnboundedProcessor in = new UnboundedProcessor(inSink::tryEmitEmpty); + UnboundedProcessor out = new UnboundedProcessor(outSink::tryEmitEmpty); - Mono onClose = inSink.asMono().zipWith(outSink.asMono()).then(); + Mono onClose = inSink.asMono().and(outSink.asMono()); server.apply(new LocalDuplexConnection(name, allocator, out, in, onClose)).subscribe(); diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java index 5c395156c..c1d0fd2a3 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java @@ -36,7 +36,7 @@ final class LocalDuplexConnection implements DuplexConnection { private final LocalSocketAddress address; private final ByteBufAllocator allocator; - private final Flux in; + private final UnboundedProcessor in; private final Mono onClose; @@ -54,7 +54,7 @@ final class LocalDuplexConnection implements DuplexConnection { LocalDuplexConnection( String name, ByteBufAllocator allocator, - Flux in, + UnboundedProcessor in, UnboundedProcessor out, Mono onClose) { this.address = new LocalSocketAddress(name); @@ -89,17 +89,16 @@ public Flux receive() { @Override public void sendFrame(int streamId, ByteBuf frame) { if (streamId == 0) { - out.onNextPrioritized(frame); + out.tryEmitPrioritized(frame); } else { - out.onNext(frame); + out.tryEmitNormal(frame); } } @Override public void sendErrorAndClose(RSocketErrorException e) { final ByteBuf errorFrame = ErrorFrameCodec.encode(allocator, 0, e); - out.onNext(errorFrame); - dispose(); + out.tryEmitFinal(errorFrame); } @Override @@ -112,6 +111,11 @@ public SocketAddress remoteAddress() { return address; } + @Override + public String toString() { + return "LocalDuplexConnection{" + "address=" + address + "hash=" + hashCode() + '}'; + } + static class ByteBufReleaserOperator implements CoreSubscriber, Subscription, Fuseable.QueueSubscription { diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java index 7ea1f8cda..975cb6793 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java @@ -17,12 +17,15 @@ package io.rsocket.transport.local; import io.rsocket.Closeable; +import io.rsocket.DuplexConnection; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import java.util.Objects; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; import reactor.core.Scannable; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -34,7 +37,7 @@ */ public final class LocalServerTransport implements ServerTransport { - private static final ConcurrentMap registry = + private static final ConcurrentMap registry = new ConcurrentHashMap<>(); private final String name; @@ -72,7 +75,10 @@ public static LocalServerTransport createEphemeral() { */ public static void dispose(String name) { Objects.requireNonNull(name, "name must not be null"); - registry.remove(name); + ServerCloseableAcceptor sca = registry.remove(name); + if (sca != null) { + sca.dispose(); + } } /** @@ -107,34 +113,55 @@ public Mono start(ConnectionAcceptor acceptor) { Objects.requireNonNull(acceptor, "acceptor must not be null"); return Mono.create( sink -> { - ServerCloseable closeable = new ServerCloseable(name, acceptor); - if (registry.putIfAbsent(name, acceptor) != null) { - throw new IllegalStateException("name already registered: " + name); + ServerCloseableAcceptor closeable = new ServerCloseableAcceptor(name, acceptor); + if (registry.putIfAbsent(name, closeable) != null) { + sink.error(new IllegalStateException("name already registered: " + name)); } sink.success(closeable); }); } - static class ServerCloseable implements Closeable { + @SuppressWarnings({"ReactorTransformationOnMonoVoid", "CallingSubscribeInNonBlockingScope"}) + static class ServerCloseableAcceptor implements ConnectionAcceptor, Closeable { private final LocalSocketAddress address; private final ConnectionAcceptor acceptor; - private final Sinks.Empty onClose = Sinks.empty(); + private final Set activeConnections = ConcurrentHashMap.newKeySet(); + + private final Sinks.Empty onClose = Sinks.unsafe().empty(); - ServerCloseable(String name, ConnectionAcceptor acceptor) { + ServerCloseableAcceptor(String name, ConnectionAcceptor acceptor) { Objects.requireNonNull(name, "name must not be null"); this.address = new LocalSocketAddress(name); this.acceptor = acceptor; } + @Override + public Mono apply(DuplexConnection duplexConnection) { + activeConnections.add(duplexConnection); + duplexConnection + .onClose() + .doFinally(__ -> activeConnections.remove(duplexConnection)) + .subscribe(); + return acceptor.apply(duplexConnection); + } + @Override public void dispose() { - if (!registry.remove(address.getName(), acceptor)) { - throw new AssertionError(); + if (!registry.remove(address.getName(), this)) { + // already disposed + return; } - onClose.tryEmitEmpty(); + + Mono.whenDelayError( + activeConnections + .stream() + .peek(DuplexConnection::dispose) + .map(DuplexConnection::onClose) + .collect(Collectors.toList())) + .subscribe(null, onClose::tryEmitError, onClose::tryEmitEmpty); } @Override diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientTransportTest.java index ac4c13efe..095de3f0e 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientTransportTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientTransportTest.java @@ -19,9 +19,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import io.rsocket.Closeable; +import java.time.Duration; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; import reactor.test.StepVerifier; final class LocalClientTransportTest { @@ -31,12 +32,20 @@ final class LocalClientTransportTest { void connect() { LocalServerTransport serverTransport = LocalServerTransport.createEphemeral(); - serverTransport - .start(duplexConnection -> Mono.empty()) - .flatMap(closeable -> LocalClientTransport.create(serverTransport.getName()).connect()) - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); + Closeable closeable = + serverTransport.start(duplexConnection -> duplexConnection.receive().then()).block(); + + try { + LocalClientTransport.create(serverTransport.getName()) + .connect() + .doOnNext(d -> d.receive().subscribe()) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } finally { + closeable.dispose(); + closeable.onClose().block(Duration.ofSeconds(5)); + } } @DisplayName("generates error if server not started") diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java index 51c812cc3..28c1dacac 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,20 +19,31 @@ import io.rsocket.test.TransportTest; import java.time.Duration; import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; final class LocalResumableTransportTest implements TransportTest { - private final TransportPair transportPair = - new TransportPair<>( - () -> "test-" + UUID.randomUUID(), - (address, server, allocator) -> LocalClientTransport.create(address, allocator), - (address, allocator) -> LocalServerTransport.create(address), - false, - true); + private TransportPair transportPair; + + @BeforeEach + void createTestPair(TestInfo testInfo) { + transportPair = + new TransportPair<>( + () -> + "LocalResumableTransportTest-" + + testInfo.getDisplayName() + + "-" + + UUID.randomUUID(), + (address, server, allocator) -> LocalClientTransport.create(address, allocator), + (address, allocator) -> LocalServerTransport.create(address), + false, + true); + } @Override public Duration getTimeout() { - return Duration.ofSeconds(10); + return Duration.ofMinutes(1); } @Override diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableWithFragmentationTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableWithFragmentationTransportTest.java index 124cecec9..8ae16a0a5 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableWithFragmentationTransportTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalResumableWithFragmentationTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,20 +19,31 @@ import io.rsocket.test.TransportTest; import java.time.Duration; import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; final class LocalResumableWithFragmentationTransportTest implements TransportTest { - private final TransportPair transportPair = - new TransportPair<>( - () -> "test-" + UUID.randomUUID(), - (address, server, allocator) -> LocalClientTransport.create(address, allocator), - (address, allocator) -> LocalServerTransport.create(address), - true, - true); + private TransportPair transportPair; + + @BeforeEach + void createTestPair(TestInfo testInfo) { + transportPair = + new TransportPair<>( + () -> + "LocalResumableWithFragmentationTransportTest-" + + testInfo.getDisplayName() + + "-" + + UUID.randomUUID(), + (address, server, allocator) -> LocalClientTransport.create(address, allocator), + (address, allocator) -> LocalServerTransport.create(address), + true, + true); + } @Override public Duration getTimeout() { - return Duration.ofSeconds(10); + return Duration.ofMinutes(1); } @Override diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalServerTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalServerTransportTest.java index ed906f65b..e4edafc39 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalServerTransportTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalServerTransportTest.java @@ -96,11 +96,16 @@ void named() { @DisplayName("starts local server transport") @Test void start() { - LocalServerTransport.createEphemeral() - .start(duplexConnection -> Mono.empty()) - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); + LocalServerTransport ephemeral = LocalServerTransport.createEphemeral(); + try { + ephemeral + .start(duplexConnection -> Mono.empty()) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } finally { + LocalServerTransport.dispose(ephemeral.getName()); + } } @DisplayName("start throws NullPointerException with null acceptor") diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportTest.java index e9c137255..87ad2105b 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,18 +19,25 @@ import io.rsocket.test.TransportTest; import java.time.Duration; import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; final class LocalTransportTest implements TransportTest { - private final TransportPair transportPair = - new TransportPair<>( - () -> "test-" + UUID.randomUUID(), - (address, server, allocator) -> LocalClientTransport.create(address, allocator), - (address, allocator) -> LocalServerTransport.create(address)); + private TransportPair transportPair; + + @BeforeEach + void createTestPair(TestInfo testInfo) { + transportPair = + new TransportPair<>( + () -> "LocalTransportTest-" + testInfo.getDisplayName() + "-" + UUID.randomUUID(), + (address, server, allocator) -> LocalClientTransport.create(address, allocator), + (address, allocator) -> LocalServerTransport.create(address)); + } @Override public Duration getTimeout() { - return Duration.ofSeconds(10); + return Duration.ofMinutes(1); } @Override diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportWithFragmentationTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportWithFragmentationTest.java index 4c2f47771..3ca5f5911 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportWithFragmentationTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalTransportWithFragmentationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,19 +19,30 @@ import io.rsocket.test.TransportTest; import java.time.Duration; import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; final class LocalTransportWithFragmentationTest implements TransportTest { - private final TransportPair transportPair = - new TransportPair<>( - () -> "test-" + UUID.randomUUID(), - (address, server, allocator) -> LocalClientTransport.create(address, allocator), - (address, allocator) -> LocalServerTransport.create(address), - true); + private TransportPair transportPair; + + @BeforeEach + void createTestPair(TestInfo testInfo) { + transportPair = + new TransportPair<>( + () -> + "LocalTransportWithFragmentationTest-" + + testInfo.getDisplayName() + + "-" + + UUID.randomUUID(), + (address, server, allocator) -> LocalClientTransport.create(address, allocator), + (address, allocator) -> LocalServerTransport.create(address), + true); + } @Override public Duration getTimeout() { - return Duration.ofSeconds(10); + return Duration.ofMinutes(1); } @Override diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 0dd02b180..39a5ceac5 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -22,7 +22,7 @@ plugins { } def os_suffix = "" -if (osdetector.classifier in ["linux-x86_64", "osx-x86_64", "windows-x86_64"]) { +if (osdetector.classifier in ["linux-x86_64", "linux-aarch_64", "osx-x86_64", "osx-aarch_64", "windows-x86_64"]) { os_suffix = "::" + osdetector.classifier } @@ -52,4 +52,9 @@ jar { } } +test { + minHeapSize = "512m" + maxHeapSize = "4096m" +} + description = 'Reactor Netty RSocket transport implementations (TCP, Websocket)' diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java index 901d1ba9a..f5d36269c 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java @@ -31,7 +31,7 @@ /** An implementation of {@link DuplexConnection} that connects via TCP. */ public final class TcpDuplexConnection extends BaseDuplexConnection { - + private final String side; private final Connection connection; /** @@ -40,17 +40,19 @@ public final class TcpDuplexConnection extends BaseDuplexConnection { * @param connection the {@link Connection} for managing the server */ public TcpDuplexConnection(Connection connection) { - this.connection = Objects.requireNonNull(connection, "connection must not be null"); + this("unknown", connection); + } - connection - .channel() - .closeFuture() - .addListener( - future -> { - if (!isDisposed()) dispose(); - }); + /** + * Creates a new instance + * + * @param connection the {@link Connection} for managing the server + */ + public TcpDuplexConnection(String side, Connection connection) { + this.connection = Objects.requireNonNull(connection, "connection must not be null"); + this.side = side; - connection.outbound().send(sender).then().subscribe(); + connection.outbound().send(sender).then().doFinally(__ -> connection.dispose()).subscribe(); } @Override @@ -70,17 +72,13 @@ protected void doOnClose() { @Override public Mono onClose() { - return super.onClose().and(connection.onDispose()); + return Mono.whenDelayError(super.onClose(), connection.onTerminate()); } @Override public void sendErrorAndClose(RSocketErrorException e) { final ByteBuf errorFrame = ErrorFrameCodec.encode(alloc(), 0, e); - connection - .outbound() - .sendObject(FrameLengthCodec.encode(alloc(), errorFrame.readableBytes(), errorFrame)) - .subscribe(connection.disposeSubscriber()); - sender.onComplete(); + sender.tryEmitFinal(FrameLengthCodec.encode(alloc(), errorFrame.readableBytes(), errorFrame)); } @Override @@ -92,4 +90,9 @@ public Flux receive() { public void sendFrame(int streamId, ByteBuf frame) { super.sendFrame(streamId, FrameLengthCodec.encode(alloc(), frame.readableBytes(), frame)); } + + @Override + public String toString() { + return "TcpDuplexConnection{" + "side='" + side + '\'' + ", connection=" + connection + '}'; + } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java index 542ff3599..8f1170c5b 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java @@ -36,7 +36,7 @@ * stitched back on for frames received. */ public final class WebsocketDuplexConnection extends BaseDuplexConnection { - + private final String side; private final Connection connection; /** @@ -45,17 +45,24 @@ public final class WebsocketDuplexConnection extends BaseDuplexConnection { * @param connection the {@link Connection} to for managing the server */ public WebsocketDuplexConnection(Connection connection) { + this("unknown", connection); + } + + /** + * Creates a new instance + * + * @param connection the {@link Connection} to for managing the server + */ + public WebsocketDuplexConnection(String side, Connection connection) { this.connection = Objects.requireNonNull(connection, "connection must not be null"); + this.side = side; connection - .channel() - .closeFuture() - .addListener( - future -> { - if (!isDisposed()) dispose(); - }); - - connection.outbound().sendObject(sender.map(BinaryWebSocketFrame::new)).then().subscribe(); + .outbound() + .sendObject(sender.map(BinaryWebSocketFrame::new)) + .then() + .doFinally(__ -> connection.dispose()) + .subscribe(); } @Override @@ -75,7 +82,7 @@ protected void doOnClose() { @Override public Mono onClose() { - return super.onClose().and(connection.onDispose()); + return Mono.whenDelayError(super.onClose(), connection.onTerminate()); } @Override @@ -86,10 +93,17 @@ public Flux receive() { @Override public void sendErrorAndClose(RSocketErrorException e) { final ByteBuf errorFrame = ErrorFrameCodec.encode(alloc(), 0, e); - connection - .outbound() - .sendObject(new BinaryWebSocketFrame(errorFrame)) - .subscribe(connection.disposeSubscriber()); - sender.onComplete(); + sender.tryEmitFinal(errorFrame); + } + + @Override + public String toString() { + return "WebsocketDuplexConnection{" + + "side='" + + side + + '\'' + + ", connection=" + + connection + + '}'; } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java index f64c6063c..84214b98c 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java @@ -116,6 +116,6 @@ public Mono connect() { return client .doOnConnected(c -> c.addHandlerLast(new RSocketLengthCodec(maxFrameLength))) .connect() - .map(TcpDuplexConnection::new); + .map(connection -> new TcpDuplexConnection("client", connection)); } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/WebsocketClientTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/WebsocketClientTransport.java index fe66da50a..86be47893 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/WebsocketClientTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/WebsocketClientTransport.java @@ -172,6 +172,6 @@ public Mono connect() { .websocket(specBuilder.build()) .uri(path) .connect() - .map(WebsocketDuplexConnection::new); + .map(connection -> new WebsocketDuplexConnection("client", connection)); } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/CloseableChannel.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/CloseableChannel.java index 3c8192eb3..7e98905ff 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/CloseableChannel.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/CloseableChannel.java @@ -29,7 +29,7 @@ */ public final class CloseableChannel implements Closeable { - /** For 1.0 and 1.1 compatibility: remove when RSocket requires Reactor Netty 1.0+. */ + /** For forward compatibility: remove when RSocket compiles against Reactor 1.0. */ private static final Method channelAddressMethod; static { @@ -61,7 +61,7 @@ public final class CloseableChannel implements Closeable { public InetSocketAddress address() { try { return (InetSocketAddress) channel.address(); - } catch (NoSuchMethodError e) { + } catch (ClassCastException | NoSuchMethodError e) { try { return (InetSocketAddress) channelAddressMethod.invoke(this.channel); } catch (Exception ex) { diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java index effc7bed5..32562c4a4 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java @@ -114,7 +114,7 @@ public Mono start(ConnectionAcceptor acceptor) { c -> { c.addHandlerLast(new RSocketLengthCodec(maxFrameLength)); acceptor - .apply(new TcpDuplexConnection(c)) + .apply(new TcpDuplexConnection("server", c)) .then(Mono.never()) .subscribe(c.disposeSubscriber()); }) diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java index 38344c472..db13720e7 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java @@ -80,6 +80,8 @@ public Mono start(ConnectionAcceptor acceptor) { public static BiFunction> newHandler( ConnectionAcceptor acceptor) { return (in, out) -> - acceptor.apply(new WebsocketDuplexConnection((Connection) in)).then(out.neverComplete()); + acceptor + .apply(new WebsocketDuplexConnection("server", (Connection) in)) + .then(out.neverComplete()); } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java index 81ac8dcb6..4fe736fad 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java @@ -117,7 +117,7 @@ public Mono start(ConnectionAcceptor acceptor) { return response.sendWebsocket( (in, out) -> acceptor - .apply(new WebsocketDuplexConnection((Connection) in)) + .apply(new WebsocketDuplexConnection("server", (Connection) in)) .then(out.neverComplete()), specBuilder.build()); }) diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java new file mode 100644 index 000000000..f05713215 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/integration/KeepaliveTest.java @@ -0,0 +1,190 @@ +package io.rsocket.integration; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketClient; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.tcp.TcpClient; +import reactor.netty.tcp.TcpServer; +import reactor.test.StepVerifier; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; + +/** + * Test case that reproduces the following GitHub Issue + */ +public class KeepaliveTest { + + private static final Logger LOG = LoggerFactory.getLogger(KeepaliveTest.class); + private static final int PORT = 23200; + + private CloseableChannel server; + + @BeforeEach + void setUp() { + server = createServer().block(); + } + + @AfterEach + void tearDown() { + server.dispose(); + server.onClose().block(); + } + + @Test + void keepAliveTest() { + RSocketClient rsocketClient = createClient(); + + int expectedCount = 4; + AtomicBoolean sleepOnce = new AtomicBoolean(true); + StepVerifier.create( + Flux.range(0, expectedCount) + .delayElements(Duration.ofMillis(2000)) + .concatMap( + i -> + rsocketClient + .requestResponse(Mono.just(DefaultPayload.create(""))) + .doOnNext( + __ -> { + if (sleepOnce.getAndSet(false)) { + try { + LOG.info("Sleeping..."); + Thread.sleep(1_000); + LOG.info("Waking up."); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }) + .log("id " + i) + .onErrorComplete())) + .expectSubscription() + .expectNextCount(expectedCount) + .verifyComplete(); + } + + @Test + void keepAliveTestLazy() { + Mono rsocketMono = createClientLazy(); + + int expectedCount = 4; + AtomicBoolean sleepOnce = new AtomicBoolean(true); + StepVerifier.create( + Flux.range(0, expectedCount) + .delayElements(Duration.ofMillis(2000)) + .concatMap( + i -> + rsocketMono.flatMap( + rsocket -> + rsocket + .requestResponse(DefaultPayload.create("")) + .doOnNext( + __ -> { + if (sleepOnce.getAndSet(false)) { + try { + LOG.info("Sleeping..."); + Thread.sleep(1_000); + LOG.info("Waking up."); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }) + .log("id " + i) + .onErrorComplete()))) + .expectSubscription() + .expectNextCount(expectedCount) + .verifyComplete(); + } + + private static Mono createServer() { + LOG.info("Starting server at port {}", PORT); + + TcpServer tcpServer = TcpServer.create().host("localhost").port(PORT); + + return RSocketServer.create( + (setupPayload, rSocket) -> { + rSocket + .onClose() + .doFirst(() -> LOG.info("Connected on server side.")) + .doOnTerminate(() -> LOG.info("Connection closed on server side.")) + .subscribe(); + + return Mono.just(new MyServerRsocket()); + }) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(TcpServerTransport.create(tcpServer)) + .doOnNext(closeableChannel -> LOG.info("RSocket server started.")); + } + + private static RSocketClient createClient() { + LOG.info("Connecting...."); + + Function reconnectSpec = + reason -> + Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(10L)) + .doBeforeRetry(retrySignal -> LOG.info("Reconnecting. Reason: {}", reason)); + + Mono rsocketMono = + RSocketConnector.create() + .fragment(16384) + .reconnect(reconnectSpec.apply("connector-close")) + .keepAlive(Duration.ofMillis(100L), Duration.ofMillis(900L)) + .connect(TcpClientTransport.create(TcpClient.create().host("localhost").port(PORT))); + + RSocketClient client = RSocketClient.from(rsocketMono); + + client + .source() + .doOnNext(r -> LOG.info("Got RSocket")) + .flatMap(RSocket::onClose) + .doOnError(err -> LOG.error("Error during onClose.", err)) + .retryWhen(reconnectSpec.apply("client-close")) + .doFirst(() -> LOG.info("Connected on client side.")) + .doOnTerminate(() -> LOG.info("Connection closed on client side.")) + .repeat() + .subscribe(); + + return client; + } + + private static Mono createClientLazy() { + LOG.info("Connecting...."); + + Function reconnectSpec = + reason -> + Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(10L)) + .doBeforeRetry(retrySignal -> LOG.info("Reconnecting. Reason: {}", reason)); + + return RSocketConnector.create() + .fragment(16384) + .reconnect(reconnectSpec.apply("connector-close")) + .keepAlive(Duration.ofMillis(100L), Duration.ofMillis(900L)) + .connect(TcpClientTransport.create(TcpClient.create().host("localhost").port(PORT))); + } + + public static class MyServerRsocket implements RSocket { + + @Override + public Mono requestResponse(Payload payload) { + return Mono.just("Pong").map(DefaultPayload::create); + } + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpFragmentationTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpFragmentationTransportTest.java index 299ea96c0..b17da654f 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpFragmentationTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpFragmentationTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,25 +22,31 @@ import io.rsocket.transport.netty.server.TcpServerTransport; import java.net.InetSocketAddress; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.netty.tcp.TcpClient; import reactor.netty.tcp.TcpServer; final class TcpFragmentationTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server, allocator) -> - TcpClientTransport.create( - TcpClient.create() - .remoteAddress(server::address) - .option(ChannelOption.ALLOCATOR, allocator)), - (address, allocator) -> - TcpServerTransport.create( + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + TcpClientTransport.create( + TcpClient.create() + .remoteAddress(server::address) + .option(ChannelOption.ALLOCATOR, allocator)), + (address, allocator) -> { + return TcpServerTransport.create( TcpServer.create() .bindAddress(() -> address) - .option(ChannelOption.ALLOCATOR, allocator)), - true); + .option(ChannelOption.ALLOCATOR, allocator)); + }, + true); + } @Override public Duration getTimeout() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableTransportTest.java index cf9e0540c..7be1c1c54 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,26 +22,32 @@ import io.rsocket.transport.netty.server.TcpServerTransport; import java.net.InetSocketAddress; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.netty.tcp.TcpClient; import reactor.netty.tcp.TcpServer; final class TcpResumableTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server, allocator) -> - TcpClientTransport.create( - TcpClient.create() - .remoteAddress(server::address) - .option(ChannelOption.ALLOCATOR, allocator)), - (address, allocator) -> - TcpServerTransport.create( + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + TcpClientTransport.create( + TcpClient.create() + .remoteAddress(server::address) + .option(ChannelOption.ALLOCATOR, allocator)), + (address, allocator) -> { + return TcpServerTransport.create( TcpServer.create() .bindAddress(() -> address) - .option(ChannelOption.ALLOCATOR, allocator)), - false, - true); + .option(ChannelOption.ALLOCATOR, allocator)); + }, + false, + true); + } @Override public Duration getTimeout() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableWithFragmentationTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableWithFragmentationTransportTest.java index 7d9d80542..39b3cec67 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableWithFragmentationTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpResumableWithFragmentationTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,26 +22,32 @@ import io.rsocket.transport.netty.server.TcpServerTransport; import java.net.InetSocketAddress; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.netty.tcp.TcpClient; import reactor.netty.tcp.TcpServer; final class TcpResumableWithFragmentationTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server, allocator) -> - TcpClientTransport.create( - TcpClient.create() - .remoteAddress(server::address) - .option(ChannelOption.ALLOCATOR, allocator)), - (address, allocator) -> - TcpServerTransport.create( + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + TcpClientTransport.create( + TcpClient.create() + .remoteAddress(server::address) + .option(ChannelOption.ALLOCATOR, allocator)), + (address, allocator) -> { + return TcpServerTransport.create( TcpServer.create() .bindAddress(() -> address) - .option(ChannelOption.ALLOCATOR, allocator)), - true, - true); + .option(ChannelOption.ALLOCATOR, allocator)); + }, + true, + true); + } @Override public Duration getTimeout() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java index 85481924a..ee49b83cd 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.rsocket.transport.netty; import io.netty.channel.ChannelOption; @@ -10,41 +26,47 @@ import java.net.InetSocketAddress; import java.security.cert.CertificateException; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.core.Exceptions; import reactor.netty.tcp.TcpClient; import reactor.netty.tcp.TcpServer; public class TcpSecureTransportTest implements TransportTest { - private final TransportPair transportPair = - new TransportPair<>( - () -> new InetSocketAddress("localhost", 0), - (address, server, allocator) -> - TcpClientTransport.create( - TcpClient.create() - .option(ChannelOption.ALLOCATOR, allocator) - .remoteAddress(server::address) - .secure( - ssl -> - ssl.sslContext( - SslContextBuilder.forClient() - .trustManager(InsecureTrustManagerFactory.INSTANCE)))), - (address, allocator) -> { - try { - SelfSignedCertificate ssc = new SelfSignedCertificate(); - TcpServer server = - TcpServer.create() - .option(ChannelOption.ALLOCATOR, allocator) - .bindAddress(() -> address) - .secure( - ssl -> - ssl.sslContext( - SslContextBuilder.forServer( - ssc.certificate(), ssc.privateKey()))); - return TcpServerTransport.create(server); - } catch (CertificateException e) { - throw Exceptions.propagate(e); - } - }); + private TransportPair transportPair; + + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> new InetSocketAddress("localhost", 0), + (address, server, allocator) -> + TcpClientTransport.create( + TcpClient.create() + .option(ChannelOption.ALLOCATOR, allocator) + .remoteAddress(server::address) + .secure( + ssl -> + ssl.sslContext( + SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE)))), + (address, allocator) -> { + try { + SelfSignedCertificate ssc = new SelfSignedCertificate(); + TcpServer server = + TcpServer.create() + .option(ChannelOption.ALLOCATOR, allocator) + .bindAddress(() -> address) + .secure( + ssl -> + ssl.sslContext( + SslContextBuilder.forServer( + ssc.certificate(), ssc.privateKey()))); + return TcpServerTransport.create(server); + } catch (CertificateException e) { + throw Exceptions.propagate(e); + } + }); + } @Override public Duration getTimeout() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpTransportTest.java index c474f9b0b..428681f3e 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,24 +22,30 @@ import io.rsocket.transport.netty.server.TcpServerTransport; import java.net.InetSocketAddress; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.netty.tcp.TcpClient; import reactor.netty.tcp.TcpServer; final class TcpTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server, allocator) -> - TcpClientTransport.create( - TcpClient.create() - .remoteAddress(server::address) - .option(ChannelOption.ALLOCATOR, allocator)), - (address, allocator) -> - TcpServerTransport.create( + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + TcpClientTransport.create( + TcpClient.create() + .remoteAddress(server::address) + .option(ChannelOption.ALLOCATOR, allocator)), + (address, allocator) -> { + return TcpServerTransport.create( TcpServer.create() .bindAddress(() -> address) - .option(ChannelOption.ALLOCATOR, allocator))); + .option(ChannelOption.ALLOCATOR, allocator)); + }); + } @Override public Duration getTimeout() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableTransportTest.java index 34dc99ae0..043f6bc64 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,29 +22,35 @@ import io.rsocket.transport.netty.server.WebsocketServerTransport; import java.net.InetSocketAddress; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.netty.http.client.HttpClient; import reactor.netty.http.server.HttpServer; final class WebsocketResumableTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server, allocator) -> - WebsocketClientTransport.create( - HttpClient.create() - .host(server.address().getHostName()) - .port(server.address().getPort()) - .option(ChannelOption.ALLOCATOR, allocator), - ""), - (address, allocator) -> - WebsocketServerTransport.create( + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + WebsocketClientTransport.create( + HttpClient.create() + .host(server.address().getHostName()) + .port(server.address().getPort()) + .option(ChannelOption.ALLOCATOR, allocator), + ""), + (address, allocator) -> { + return WebsocketServerTransport.create( HttpServer.create() .host(address.getHostName()) .port(address.getPort()) - .option(ChannelOption.ALLOCATOR, allocator)), - false, - true); + .option(ChannelOption.ALLOCATOR, allocator)); + }, + false, + true); + } @Override public Duration getTimeout() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableWithFragmentationTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableWithFragmentationTransportTest.java index 21c027e88..b1ca65fcc 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableWithFragmentationTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketResumableWithFragmentationTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,29 +22,35 @@ import io.rsocket.transport.netty.server.WebsocketServerTransport; import java.net.InetSocketAddress; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.netty.http.client.HttpClient; import reactor.netty.http.server.HttpServer; final class WebsocketResumableWithFragmentationTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server, allocator) -> - WebsocketClientTransport.create( - HttpClient.create() - .host(server.address().getHostName()) - .port(server.address().getPort()) - .option(ChannelOption.ALLOCATOR, allocator), - ""), - (address, allocator) -> - WebsocketServerTransport.create( + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + WebsocketClientTransport.create( + HttpClient.create() + .host(server.address().getHostName()) + .port(server.address().getPort()) + .option(ChannelOption.ALLOCATOR, allocator), + ""), + (address, allocator) -> { + return WebsocketServerTransport.create( HttpServer.create() .host(address.getHostName()) .port(address.getPort()) - .option(ChannelOption.ALLOCATOR, allocator)), - true, - true); + .option(ChannelOption.ALLOCATOR, allocator)); + }, + true, + true); + } @Override public Duration getTimeout() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketSecureTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketSecureTransportTest.java index 9777c8bfa..81f7ffb95 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketSecureTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketSecureTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,45 +26,50 @@ import java.net.InetSocketAddress; import java.security.cert.CertificateException; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.core.Exceptions; import reactor.netty.http.client.HttpClient; import reactor.netty.http.server.HttpServer; final class WebsocketSecureTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> new InetSocketAddress("localhost", 0), - (address, server, allocator) -> - WebsocketClientTransport.create( - HttpClient.create() - .option(ChannelOption.ALLOCATOR, allocator) - .remoteAddress(server::address) - .secure( - ssl -> - ssl.sslContext( - SslContextBuilder.forClient() - .trustManager(InsecureTrustManagerFactory.INSTANCE))), - String.format( - "https://%s:%d/", - server.address().getHostName(), server.address().getPort())), - (address, allocator) -> { - try { - SelfSignedCertificate ssc = new SelfSignedCertificate(); - HttpServer server = - HttpServer.create() - .option(ChannelOption.ALLOCATOR, allocator) - .bindAddress(() -> address) - .secure( - ssl -> - ssl.sslContext( - SslContextBuilder.forServer( - ssc.certificate(), ssc.privateKey()))); - return WebsocketServerTransport.create(server); - } catch (CertificateException e) { - throw Exceptions.propagate(e); - } - }); + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> new InetSocketAddress("localhost", 0), + (address, server, allocator) -> + WebsocketClientTransport.create( + HttpClient.create() + .option(ChannelOption.ALLOCATOR, allocator) + .remoteAddress(server::address) + .secure( + ssl -> + ssl.sslContext( + SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE))), + String.format( + "https://%s:%d/", + server.address().getHostName(), server.address().getPort())), + (address, allocator) -> { + try { + SelfSignedCertificate ssc = new SelfSignedCertificate(); + HttpServer server = + HttpServer.create() + .option(ChannelOption.ALLOCATOR, allocator) + .bindAddress(() -> address) + .secure( + ssl -> + ssl.sslContext( + SslContextBuilder.forServer( + ssc.certificate(), ssc.privateKey()))); + return WebsocketServerTransport.create(server); + } catch (CertificateException e) { + throw Exceptions.propagate(e); + } + }); + } @Override public Duration getTimeout() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketTransportTest.java index 93d7bdb2f..cdd507456 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,27 +22,33 @@ import io.rsocket.transport.netty.server.WebsocketServerTransport; import java.net.InetSocketAddress; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import reactor.netty.http.client.HttpClient; import reactor.netty.http.server.HttpServer; final class WebsocketTransportTest implements TransportTest { + private TransportPair transportPair; - private final TransportPair transportPair = - new TransportPair<>( - () -> InetSocketAddress.createUnresolved("localhost", 0), - (address, server, allocator) -> - WebsocketClientTransport.create( - HttpClient.create() - .host(server.address().getHostName()) - .port(server.address().getPort()) - .option(ChannelOption.ALLOCATOR, allocator), - ""), - (address, allocator) -> - WebsocketServerTransport.create( + @BeforeEach + void createTestPair() { + transportPair = + new TransportPair<>( + () -> InetSocketAddress.createUnresolved("localhost", 0), + (address, server, allocator) -> + WebsocketClientTransport.create( + HttpClient.create() + .host(server.address().getHostName()) + .port(server.address().getPort()) + .option(ChannelOption.ALLOCATOR, allocator), + ""), + (address, allocator) -> { + return WebsocketServerTransport.create( HttpServer.create() .host(address.getHostName()) .port(address.getPort()) - .option(ChannelOption.ALLOCATOR, allocator))); + .option(ChannelOption.ALLOCATOR, allocator)); + }); + } @Override public Duration getTimeout() { diff --git a/rsocket-transport-netty/src/test/resources/logback-test.xml b/rsocket-transport-netty/src/test/resources/logback-test.xml index b42db6df6..981d6d0b6 100644 --- a/rsocket-transport-netty/src/test/resources/logback-test.xml +++ b/rsocket-transport-netty/src/test/resources/logback-test.xml @@ -27,7 +27,6 @@ -