From f2eeed5c64464dd177f436d77fc0d3a84dc7fc60 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Tue, 10 Dec 2019 14:06:08 -0600 Subject: [PATCH 01/74] Backport Jenkins migration to 0.8.x. [resolves #50] --- Jenkinsfile | 184 ++++++++++++++++++++++++++++++++++++ ci/Dockerfile | 23 ----- ci/build.sh | 14 +-- ci/build.yml | 20 ---- ci/builder.yml | 22 ----- ci/create-release.sh | 4 +- ci/promote-to-bintray.sh | 42 ++++++++ ci/release.sh | 13 --- ci/release.yml | 19 ---- ci/sync-to-maven-central.sh | 23 +++++ ci/test.sh | 5 + pom.xml | 137 ++++++++++++++++++++++++--- 12 files changed, 381 insertions(+), 125 deletions(-) create mode 100644 Jenkinsfile delete mode 100644 ci/Dockerfile delete mode 100644 ci/build.yml delete mode 100644 ci/builder.yml create mode 100755 ci/promote-to-bintray.sh delete mode 100755 ci/release.sh delete mode 100644 ci/release.yml create mode 100755 ci/sync-to-maven-central.sh create mode 100755 ci/test.sh diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..1fb8a3b8 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,184 @@ +pipeline { + agent none + + triggers { + pollSCM 'H/10 * * * *' + upstream(upstreamProjects: "r2dbc-spi/0.8.x", threshold: hudson.model.Result.SUCCESS) + } + + options { + disableConcurrentBuilds() + buildDiscarder(logRotator(numToKeepStr: '14')) + } + + stages { + stage("test: baseline (jdk8)") { + agent { + docker { + image 'adoptopenjdk/openjdk8:latest' + args '-v $HOME/.m2:/tmp/jenkins-home/.m2' + } + } + options { timeout(time: 30, unit: 'MINUTES') } + steps { + sh 'rm -rf ?' + sh 'PROFILE=none ci/test.sh' + } + } + + stage('Deploy to Artifactory') { + when { + anyOf { + branch '0.8.x' + branch 'release-0.x' + } + } + agent { + docker { + image 'adoptopenjdk/openjdk8:latest' + args '-v $HOME/.m2:/tmp/jenkins-home/.m2' + } + } + options { timeout(time: 20, unit: 'MINUTES') } + + environment { + ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c') + } + + steps { + script { + sh 'rm -rf ?' + + // Warm up this plugin quietly before using it. + sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -q org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version' + + // Extract project's version number + PROJECT_VERSION = sh( + script: 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version -o | grep -v INFO', + returnStdout: true + ).trim() + + RELEASE_TYPE = 'milestone' // .RC? or .M? + + if (PROJECT_VERSION.endsWith('SNAPSHOT')) { + RELEASE_TYPE = 'snapshot' + } else if (PROJECT_VERSION.endsWith('RELEASE')) { + RELEASE_TYPE = 'release' + } + + // Capture build output... + OUTPUT = sh( + script: "PROFILE=ci,${RELEASE_TYPE} ci/build.sh", + returnStdout: true + ).trim() + + echo "$OUTPUT" + + // ...to extract artifactory build info + build_info_path = OUTPUT.split('\n') + .find { it.contains('Artifactory Build Info Recorder') } + .split('Saving Build Info to ')[1] + .trim()[1..-2] + + // Stash the JSON build info to support promotion to bintray + dir(build_info_path + '/..') { + stash name: 'build_info', includes: "*.json" + } + } + } + } + + stage('Promote to Bintray') { + when { + branch 'release-0.x' + } + agent { + docker { + image 'adoptopenjdk/openjdk8:latest' + args '-v $HOME/.m2:/tmp/jenkins-home/.m2' + } + } + options { timeout(time: 20, unit: 'MINUTES') } + + environment { + ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c') + } + + steps { + script { + sh 'rm -rf ?' + + // Warm up this plugin quietly before using it. + sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -q org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version' + + PROJECT_VERSION = sh( + script: 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version -o | grep -v INFO', + returnStdout: true + ).trim() + + if (PROJECT_VERSION.endsWith('RELEASE')) { + unstash name: 'build_info' + sh "ci/promote-to-bintray.sh" + } else { + echo "${PROJECT_VERSION} is not a candidate for promotion to Bintray." + } + } + } + } + + stage('Sync to Maven Central') { + when { + branch 'release-0.x' + } + agent { + docker { + image 'adoptopenjdk/openjdk8:latest' + args '-v $HOME/.m2:/tmp/jenkins-home/.m2' + } + } + options { timeout(time: 20, unit: 'MINUTES') } + + environment { + BINTRAY = credentials('Bintray-spring-operator') + SONATYPE = credentials('oss-token') + } + + steps { + script { + sh 'rm -rf ?' + + // Warm up this plugin quietly before using it. + sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -q org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version' + + PROJECT_VERSION = sh( + script: 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version -o | grep -v INFO', + returnStdout: true + ).trim() + + if (PROJECT_VERSION.endsWith('RELEASE')) { + unstash name: 'build_info' + sh "ci/sync-to-maven-central.sh" + } else { + echo "${PROJECT_VERSION} is not a candidate for syncing to Maven Central." + } + } + } + } + } + + post { + changed { + script { + slackSend( + color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', + channel: '#r2dbc', + message: "${currentBuild.fullDisplayName} - `${currentBuild.currentResult}`\n${env.BUILD_URL}") + emailext( + subject: "[${currentBuild.fullDisplayName}] ${currentBuild.currentResult}", + mimeType: 'text/html', + recipientProviders: [[$class: 'CulpritsRecipientProvider'], [$class: 'RequesterRecipientProvider']], + body: "${currentBuild.fullDisplayName} is reported as ${currentBuild.currentResult}") + } + } + } +} diff --git a/ci/Dockerfile b/ci/Dockerfile deleted file mode 100644 index f95d77a0..00000000 --- a/ci/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM openjdk:8-jdk - -RUN apt-get update && apt-get install --no-install-recommends -y \ - apt-transport-https \ - ca-certificates \ - curl \ - gnupg2 \ - software-properties-common \ - && rm -rf /var/lib/apt/lists/* - -RUN curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - - -RUN add-apt-repository \ - "deb [arch=amd64] https://download.docker.com/linux/debian \ - $(lsb_release -cs) \ - stable" - -RUN apt-get update && apt-get install --no-install-recommends -y \ - docker-ce \ - && rm -rf /var/lib/apt/lists/* - -RUN mkdir -p /root/.docker \ - && echo "{}" > /root/.docker/config.json diff --git a/ci/build.sh b/ci/build.sh index 39c510d2..4ff384cf 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -1,15 +1,5 @@ -#!/usr/bin/env bash +#!/bin/bash set -euo pipefail -[[ -d $PWD/maven && ! -d $HOME/.m2 ]] && ln -s $PWD/maven $HOME/.m2 - -r2dbc_proxy_artifactory=$(pwd)/r2dbc-proxy-artifactory -r2dbc_spi_artifactory=$(pwd)/r2dbc-spi-artifactory - -rm -rf $HOME/.m2/repository/io/r2dbc 2> /dev/null || : - -cd r2dbc-proxy -./mvnw deploy \ - -DaltDeploymentRepository=distribution::default::file://${r2dbc_proxy_artifactory} \ - -Dr2dbcSpiArtifactory=file://${r2dbc_spi_artifactory} +MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/r2dbc-proxy-maven-repository" ./mvnw -P${PROFILE} -Dmaven.test.skip=true clean deploy -B diff --git a/ci/build.yml b/ci/build.yml deleted file mode 100644 index d8b82c9a..00000000 --- a/ci/build.yml +++ /dev/null @@ -1,20 +0,0 @@ ---- -platform: linux - -image_resource: - type: registry-image - source: - repository: r2dbc/r2dbc-proxy - -inputs: -- name: r2dbc-proxy -- name: r2dbc-spi-artifactory - -outputs: -- name: r2dbc-proxy-artifactory - -caches: -- path: maven - -run: - path: r2dbc-proxy/ci/build.sh diff --git a/ci/builder.yml b/ci/builder.yml deleted file mode 100644 index 3f637a2f..00000000 --- a/ci/builder.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -platform: linux - -image_resource: - type: registry-image - source: - repository: concourse/builder - -inputs: -- name: builder - -outputs: -- name: image - -caches: -- path: cache - -run: - path: build - -params: - CONTEXT: builder/ci diff --git a/ci/create-release.sh b/ci/create-release.sh index 3597667f..d0b26dd7 100755 --- a/ci/create-release.sh +++ b/ci/create-release.sh @@ -8,9 +8,11 @@ SNAPSHOT=$2 ./mvnw versions:set -DnewVersion=$RELEASE -DgenerateBackupPoms=false git add . git commit --message "v$RELEASE Release" + +# Tag the release git tag -s v$RELEASE -m "v$RELEASE" -git reset --hard HEAD^1 +# Bump up the version in pom.xml to the next snapshot ./mvnw versions:set -DnewVersion=$SNAPSHOT -DgenerateBackupPoms=false git add . git commit --message "v$SNAPSHOT Development" diff --git a/ci/promote-to-bintray.sh b/ci/promote-to-bintray.sh new file mode 100755 index 00000000..a00bb8d6 --- /dev/null +++ b/ci/promote-to-bintray.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +set -e -u + +buildName=`jq -r '.name' build-info.json` +buildNumber=`jq -r '.number' build-info.json` +groupId=`jq -r '.modules[0].id' build-info.json | sed 's/\(.*\):.*:.*/\1/'` +version=`jq -r '.modules[0].id' build-info.json | sed 's/.*:.*:\(.*\)/\1/'` + +echo "Promoting ${buildName}/${buildNumber}/${groupId}/${version} to libs-release-local" + +curl \ + -s \ + --connect-timeout 240 \ + --max-time 2700 \ + -u ${ARTIFACTORY_USR}:${ARTIFACTORY_PSW} \ + -H 'Content-type:application/json' \ + -d '{"sourceRepos": ["libs-release-local"], "targetRepo" : "spring-distributions", "async":"true"}' \ + -f \ + -X \ + POST "https://repo.spring.io/api/build/distribute/${buildName}/${buildNumber}" > /dev/null || { echo "Failed to distribute" >&2; exit 1; } + +echo "Waiting for artifacts to be published" + +ARTIFACTS_PUBLISHED=false +WAIT_TIME=10 +COUNTER=0 + +while [ $ARTIFACTS_PUBLISHED == "false" ] && [ $COUNTER -lt 120 ]; do + + result=$( curl -s https://api.bintray.com/packages/spring/jars/"${groupId}" ) + versions=$( echo "$result" | jq -r '.versions' ) + exists=$( echo "$versions" | grep "$version" -o || true ) + + if [ "$exists" = "$version" ]; then + ARTIFACTS_PUBLISHED=true + fi + + COUNTER=$(( COUNTER + 1 )) + sleep $WAIT_TIME + +done diff --git a/ci/release.sh b/ci/release.sh deleted file mode 100755 index 437ff949..00000000 --- a/ci/release.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -[[ -d $PWD/maven && ! -d $HOME/.m2 ]] && ln -s $PWD/maven $HOME/.m2 - -r2dbc_proxy_artifactory=$(pwd)/r2dbc-proxy-artifactory - -rm -rf $HOME/.m2/repository/io/r2dbc 2> /dev/null || : - -cd r2dbc-proxy -./mvnw deploy \ - -DaltDeploymentRepository=distribution::default::file://${r2dbc_proxy_artifactory} diff --git a/ci/release.yml b/ci/release.yml deleted file mode 100644 index 6c745b32..00000000 --- a/ci/release.yml +++ /dev/null @@ -1,19 +0,0 @@ ---- -platform: linux - -image_resource: - type: registry-image - source: - repository: r2dbc/r2dbc-proxy - -inputs: -- name: r2dbc-proxy - -outputs: -- name: r2dbc-proxy-artifactory - -caches: -- path: maven - -run: - path: r2dbc-proxy/ci/release.sh diff --git a/ci/sync-to-maven-central.sh b/ci/sync-to-maven-central.sh new file mode 100755 index 00000000..7150ca76 --- /dev/null +++ b/ci/sync-to-maven-central.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e -u + +buildName=`jq -r '.name' build-info.json` +buildNumber=`jq -r '.number' build-info.json` +groupId=`jq -r '.modules[0].id' build-info.json | sed 's/\(.*\):.*:.*/\1/'` +version=`jq -r '.modules[0].id' build-info.json | sed 's/.*:.*:\(.*\)/\1/'` + +echo "Syncing ${buildName}/${buildNumber}/${groupId}/${version} to Maven Central..." + +curl \ + -s \ + --connect-timeout 240 \ + --max-time 2700 \ + -u ${BINTRAY_USR}:${BINTRAY_PSW} \ + -H 'Content-Type: application/json' \ + -d "{ \"username\": \"${SONATYPE_USR}\", \"password\": \"${SONATYPE_PSW}\"}" \ + -f \ + -X \ + POST "https://api.bintray.com/maven_central_sync/spring/jars/${groupId}/versions/${version}" > /dev/null || { echo "Failed to sync" >&2; exit 1; } + +echo "Sync complete" diff --git a/ci/test.sh b/ci/test.sh new file mode 100755 index 00000000..656ce88e --- /dev/null +++ b/ci/test.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -euo pipefail + +MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/r2dbc-proxy-maven-repository" ./mvnw -P${PROFILE} clean dependency:list test -Dsort -B diff --git a/pom.xml b/pom.xml index 5b02ea92..6c63d491 100644 --- a/pom.xml +++ b/pom.xml @@ -332,23 +332,130 @@ + - r2dbc-spi-artifactory - - - r2dbcSpiArtifactory - - - - - r2dbc-spi-artifactory - ${r2dbcSpiArtifactory} - - true - - - + snapshot + + + + + org.jfrog.buildinfo + artifactory-maven-plugin + 2.6.1 + false + + + build-info + + publish + + + + {{BUILD_URL}} + + + r2dbc-proxy + r2dbc-proxy + false + *:*:*:*@zip + + + https://repo.spring.io + {{ARTIFACTORY_USR}} + {{ARTIFACTORY_PSW}} + libs-snapshot-local + libs-snapshot-local + + + + + + + + + + + milestone + + + + + org.jfrog.buildinfo + artifactory-maven-plugin + 2.6.1 + false + + + build-info + + publish + + + + {{BUILD_URL}} + + + r2dbc-proxy + r2dbc-proxy + false + *:*:*:*@zip + + + https://repo.spring.io + {{ARTIFACTORY_USR}} + {{ARTIFACTORY_PSW}} + libs-milestone-local + libs-milestone-local + + + + + + + + + + release + + + + + org.jfrog.buildinfo + artifactory-maven-plugin + 2.6.1 + false + + + build-info + + publish + + + + {{BUILD_URL}} + + + r2dbc-proxy + r2dbc-proxy + false + *:*:*:*@zip + + + https://repo.spring.io + {{ARTIFACTORY_USR}} + {{ARTIFACTORY_PSW}} + libs-release-local + libs-release-local + + + + + + + + + From c0efb8719ceea4de1a6f6422d0c5ba9d24238bcf Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Tue, 10 Dec 2019 14:13:12 -0600 Subject: [PATCH 02/74] Enable Travis for pull requests. [resolves #51] --- .travis.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..61d61bad --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: java +jdk: +- openjdk8 +cache: + directories: + - $HOME/.m2 +notifications: + slack: + secure: QU4hnz7qoNW1B57WOv1VDWnjJRLQYV+sfBQvdQsrq89rAmDAX1fZptlQ/vPwwFz/X83VK0Ks3PY5WO4AGHWkKkqMcnOURQM31UJtEykS6EX0A7eivg5I6Q9stNEBSzyqK/qdDSqFUXp3i1Z9dk0hCnKURQ1Z5LpoIhnNsaQ+31/ZxrVqPTLqUlHkVUMKOS9J5YOxpjxl5S7L4vXLRCvn82/NzwZ0szZwQH29vkWy4j+eNyEC/Jc9TnHPdd+07mOq5Cl59c5UynJCFZ9M1r1C5/n0vNkRuUw15U1j6JKbr9pYVfNfAtMrOR4EDtOhtobUeXyUcd2TTqUByNCVpufb4iNABP9JyDeArRPw5K1C1YupfHuo1QwpAu6F5g68ru3/e9t/lwUFiv+5Vaj2hRwjrNXez9EViV1GeMm1ovvB/NFToHl6ckcpxPTlwYqyYrCgexRy6lCElP1dVU0icQL4nUcy4Fx726o4pqiSKFubbdNOQIVosyMgJktZlCEdnKK4yX2zgbbK7MNMRQnL3Nz/SjVD1UhEhR+e/nn4xtI6SDA+gxco1TPWl9VCWq4/dvrg7l+43eserdf/kCNZTHRZoZMI392oF6THfxQuRZOTjUD89nGq6Z3GL/WJdIsjOjg3upJN+Q5YgcNOus39m8WyPuva4o8yfyorx9E+cGkzE2c= From 60956af1a9459bc42050aca23f2b4c0d367b04e3 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Tue, 10 Dec 2019 14:28:55 -0600 Subject: [PATCH 03/74] Polishing --- pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pom.xml b/pom.xml index 6c63d491..4bdf1d28 100644 --- a/pom.xml +++ b/pom.xml @@ -458,4 +458,11 @@ + + + spring-plugins-release + https://repo.spring.io/plugins-release + + + From 991364c3624abc7397f78e42085bb18fbddcd66f Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Thu, 16 Jan 2020 15:54:47 -0800 Subject: [PATCH 04/74] Make query execution success when at least one element is emitted Update "QueryExecutionInfo#isSuccess" to consider success when not only completion of source publisher, but also at least one element is emitted. This is because downstream consumer might cancel the publisher after they have received sufficient data. [resolves #55] --- .../callback/CallbackHandlerSupport.java | 14 ++- .../r2dbc/proxy/core/QueryExecutionInfo.java | 6 +- .../StatementCallbackHandlerTest.java | 89 ++++++++++++++++++- 3 files changed, 103 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java index 2ca9c410..22a99fad 100644 --- a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java +++ b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -114,6 +114,9 @@ public Duration getElapsedDuration() { return Duration.between(this.startTime, this.clock.instant()); } + public boolean isStarted() { + return this.startTime != null; + } } protected final ProxyConfig proxyConfig; @@ -289,7 +292,11 @@ protected Flux interceptQueryExecution(Publisher { + .doOnNext(result -> { + // When at least one element is emitted, consider query execution is success, even when + // the publisher is canceled. see https://github.com/r2dbc/r2dbc-proxy/issues/55 + executionInfo.setSuccess(true); + }).doOnComplete(() -> { executionInfo.setSuccess(true); }) .doOnError(throwable -> { @@ -297,8 +304,7 @@ protected Flux interceptQueryExecution(Publisher { - - executionInfo.setExecuteDuration(stopWatch.getElapsedDuration()); + executionInfo.setExecuteDuration(stopWatch.isStarted() ? stopWatch.getElapsedDuration() : Duration.ZERO); executionInfo.setThreadName(Thread.currentThread().getName()); executionInfo.setThreadId(Thread.currentThread().getId()); executionInfo.setCurrentMappedResult(null); diff --git a/src/main/java/io/r2dbc/proxy/core/QueryExecutionInfo.java b/src/main/java/io/r2dbc/proxy/core/QueryExecutionInfo.java index 3a03b0b6..0cbfbeb3 100644 --- a/src/main/java/io/r2dbc/proxy/core/QueryExecutionInfo.java +++ b/src/main/java/io/r2dbc/proxy/core/QueryExecutionInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,6 +68,10 @@ public interface QueryExecutionInfo { * Indicate whether the query execution was successful or not. * Contains valid value only after the query execution. * + * Query execution is considered successful when the {@link org.reactivestreams.Publisher} + * returned from {@link Statement#execute()} either received completion + * or at least one element is emitted regardless of it has received cancellation. + * * @return true when query has successfully executed */ boolean isSuccess(); diff --git a/src/test/java/io/r2dbc/proxy/callback/StatementCallbackHandlerTest.java b/src/test/java/io/r2dbc/proxy/callback/StatementCallbackHandlerTest.java index 7172daf3..e7a4922e 100644 --- a/src/test/java/io/r2dbc/proxy/callback/StatementCallbackHandlerTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/StatementCallbackHandlerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,10 +28,13 @@ import io.r2dbc.proxy.test.MockStatementInfo; import io.r2dbc.spi.Statement; import io.r2dbc.spi.Wrapped; +import io.r2dbc.spi.test.MockResult; import io.r2dbc.spi.test.MockStatement; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import org.springframework.util.ReflectionUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.lang.reflect.Method; @@ -273,6 +276,90 @@ void executeOperationWithBindByName() throws Throwable { } + @Test + void executeThenCancel() throws Throwable { + LastExecutionAwareListener testListener = new LastExecutionAwareListener(); + + ConnectionInfo connectionInfo = mock(ConnectionInfo.class); + StatementInfo statementInfo = MockStatementInfo.builder().updatedQuery("QUERY").build(); + ProxyConfig proxyConfig = ProxyConfig.builder().listener(testListener).build(); + + Statement statement = MockStatement.builder().result(MockResult.empty()).build(); + StatementCallbackHandler callback = new StatementCallbackHandler(statement, statementInfo, connectionInfo, proxyConfig); + + Object result = callback.invoke(statement, EXECUTE_METHOD, new Object[]{}); + + StepVerifier.create((Publisher) result) + .expectSubscription() + .expectNextCount(1) + .thenCancel()// cancel after consuming one result + .verify(); + + QueryExecutionInfo afterQueryInfo = testListener.getAfterQueryExecutionInfo(); + + assertThat(afterQueryInfo).isNotNull(); + assertThat(afterQueryInfo.isSuccess()) + .as("Consuming at least one result is considered to query execution success") + .isTrue(); + + } + + @Test + void executeThenImmediatelyCancel() throws Throwable { + LastExecutionAwareListener testListener = new LastExecutionAwareListener(); + + ConnectionInfo connectionInfo = mock(ConnectionInfo.class); + StatementInfo statementInfo = MockStatementInfo.builder().updatedQuery("QUERY").build(); + ProxyConfig proxyConfig = ProxyConfig.builder().listener(testListener).build(); + + Statement statement = MockStatement.builder().result(MockResult.empty()).build(); + StatementCallbackHandler callback = new StatementCallbackHandler(statement, statementInfo, connectionInfo, proxyConfig); + + Object result = callback.invoke(statement, EXECUTE_METHOD, new Object[]{}); + + StepVerifier.create((Publisher) result) + .expectSubscription() + .thenCancel()// immediately cancel + .verify(); + + QueryExecutionInfo afterQueryInfo = testListener.getAfterQueryExecutionInfo(); + + assertThat(afterQueryInfo).isNotNull(); + assertThat(afterQueryInfo.isSuccess()) + .as("Not consuming any result is considered to query execution failure") + .isFalse(); + + } + + @Test + void executeThenNext() throws Throwable { + LastExecutionAwareListener testListener = new LastExecutionAwareListener(); + + ConnectionInfo connectionInfo = mock(ConnectionInfo.class); + StatementInfo statementInfo = MockStatementInfo.builder().updatedQuery("QUERY").build(); + ProxyConfig proxyConfig = ProxyConfig.builder().listener(testListener).build(); + + Statement statement = MockStatement.builder().result(MockResult.empty()).build(); + StatementCallbackHandler callback = new StatementCallbackHandler(statement, statementInfo, connectionInfo, proxyConfig); + + Object result = callback.invoke(statement, EXECUTE_METHOD, new Object[]{}); + + // Flux.next() cancels upstream publisher + Mono mono = ((Flux) result).next(); + + StepVerifier.create(mono) + .expectSubscription() + .expectNextCount(1) + .verifyComplete(); + + QueryExecutionInfo afterQueryInfo = testListener.getAfterQueryExecutionInfo(); + + assertThat(afterQueryInfo).isNotNull(); + assertThat(afterQueryInfo.isSuccess()) + .as("Consuming at least one result is considered to query execution success") + .isTrue(); + } + @Test void unwrap() throws Throwable { Statement statement = MockStatement.empty(); From d61f2a48e4f43b263583e183328b053d6aa60eb5 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Fri, 17 Jan 2020 14:29:48 -0800 Subject: [PATCH 05/74] Add more @Nullable annotation Add more @Nullable annotation where it applies. --- .../io/r2dbc/proxy/callback/CallbackHandler.java | 6 ++++-- .../proxy/callback/CallbackHandlerSupport.java | 15 ++++++++------- .../callback/MutableMethodExecutionInfo.java | 15 ++++++++++----- .../proxy/callback/MutableQueryExecutionInfo.java | 12 ++++++++---- .../io/r2dbc/proxy/core/MethodExecutionInfo.java | 9 +++++++-- .../io/r2dbc/proxy/core/QueryExecutionInfo.java | 4 ++++ 6 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src/main/java/io/r2dbc/proxy/callback/CallbackHandler.java b/src/main/java/io/r2dbc/proxy/callback/CallbackHandler.java index 9f1d74af..4e27b719 100644 --- a/src/main/java/io/r2dbc/proxy/callback/CallbackHandler.java +++ b/src/main/java/io/r2dbc/proxy/callback/CallbackHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ package io.r2dbc.proxy.callback; +import reactor.util.annotation.Nullable; + import java.lang.reflect.Method; /** @@ -42,6 +44,6 @@ public interface CallbackHandler { * @throws IllegalArgumentException if {@code proxy} is {@code null} * @throws IllegalArgumentException if {@code method} is {@code null} */ - Object invoke(Object proxy, Method method, Object[] args) throws Throwable; + Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable; } diff --git a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java index 22a99fad..120e4c57 100644 --- a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java +++ b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java @@ -21,6 +21,7 @@ import io.r2dbc.proxy.core.ProxyEventType; import io.r2dbc.proxy.listener.ProxyExecutionListener; import io.r2dbc.proxy.util.Assert; +import io.r2dbc.spi.Connection; import io.r2dbc.spi.Result; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -57,11 +58,11 @@ public interface MethodInvocationStrategy { * * @param method invocation method * @param target invocation target instance - * @param args invocation arguments + * @param args invocation arguments. {@code null} when invocation didn't take any arguments. * @return actual invocation result (not a proxy object) * @throws Throwable actual thrown exception */ - Object invoke(Method method, Object target, Object[] args) throws Throwable; + Object invoke(Method method, Object target, @Nullable Object[] args) throws Throwable; } protected static final MethodInvocationStrategy DEFAULT_INVOCATION_STRATEGY = (method, target, args) -> { @@ -132,9 +133,9 @@ public CallbackHandlerSupport(ProxyConfig proxyConfig) { * * @param method method to invoke on target * @param target an object being invoked - * @param args arguments for the method - * @param listener listener that before/aftre method callbacks will be called - * @param connectionInfo current connection information + * @param args arguments for the method. {@code null} if the method doesn't take any arguments. + * @param listener listener that before/after method callbacks will be called + * @param connectionInfo current connection information. {@code null} when invoked operation is not associated to the {@link Connection}. * @param onMap a callback that will be chained on "map()" right after the result of the method invocation * @param onComplete a callback that will be chained as the first doOnComplete on the result of the method invocation * @return result of invoking the original object @@ -143,8 +144,8 @@ public CallbackHandlerSupport(ProxyConfig proxyConfig) { * @throws IllegalArgumentException if {@code target} is {@code null} * @throws IllegalArgumentException if {@code listener} is {@code null} */ - protected Object proceedExecution(Method method, Object target, Object[] args, - ProxyExecutionListener listener, ConnectionInfo connectionInfo, + protected Object proceedExecution(Method method, Object target, @Nullable Object[] args, + ProxyExecutionListener listener, @Nullable ConnectionInfo connectionInfo, @Nullable BiFunction onMap, @Nullable Consumer onComplete) throws Throwable { Assert.requireNonNull(method, "method must not be null"); diff --git a/src/main/java/io/r2dbc/proxy/callback/MutableMethodExecutionInfo.java b/src/main/java/io/r2dbc/proxy/callback/MutableMethodExecutionInfo.java index f39644f7..90346bb6 100644 --- a/src/main/java/io/r2dbc/proxy/callback/MutableMethodExecutionInfo.java +++ b/src/main/java/io/r2dbc/proxy/callback/MutableMethodExecutionInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import io.r2dbc.proxy.core.MethodExecutionInfo; import io.r2dbc.proxy.core.ProxyEventType; import io.r2dbc.proxy.core.ValueStore; +import reactor.util.annotation.Nullable; import java.lang.reflect.Method; import java.time.Duration; @@ -36,12 +37,16 @@ final class MutableMethodExecutionInfo implements MethodExecutionInfo { private Method method; + @Nullable private Object[] methodArgs; + @Nullable private Object result; + @Nullable private Throwable thrown; + @Nullable private ConnectionInfo connectionInfo; private Duration executeDuration = Duration.ZERO; @@ -62,19 +67,19 @@ public void setMethod(Method method) { this.method = method; } - public void setMethodArgs(Object[] methodArgs) { + public void setMethodArgs(@Nullable Object[] methodArgs) { this.methodArgs = methodArgs; } - public void setResult(Object result) { + public void setResult(@Nullable Object result) { this.result = result; } - public void setThrown(Throwable thrown) { + public void setThrown(@Nullable Throwable thrown) { this.thrown = thrown; } - public void setConnectionInfo(ConnectionInfo connectionInfo) { + public void setConnectionInfo(@Nullable ConnectionInfo connectionInfo) { this.connectionInfo = connectionInfo; } diff --git a/src/main/java/io/r2dbc/proxy/callback/MutableQueryExecutionInfo.java b/src/main/java/io/r2dbc/proxy/callback/MutableQueryExecutionInfo.java index 9fb747e4..4de35c1d 100644 --- a/src/main/java/io/r2dbc/proxy/callback/MutableQueryExecutionInfo.java +++ b/src/main/java/io/r2dbc/proxy/callback/MutableQueryExecutionInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import io.r2dbc.proxy.core.QueryExecutionInfo; import io.r2dbc.proxy.core.QueryInfo; import io.r2dbc.proxy.core.ValueStore; +import reactor.util.annotation.Nullable; import java.lang.reflect.Method; import java.time.Duration; @@ -40,8 +41,10 @@ final class MutableQueryExecutionInfo implements QueryExecutionInfo { private Method method; + @Nullable private Object[] methodArgs; + @Nullable private Throwable throwable; private boolean isSuccess; @@ -62,6 +65,7 @@ final class MutableQueryExecutionInfo implements QueryExecutionInfo { private int currentResultCount; + @Nullable private Object currentMappedResult; private List queries = new ArrayList<>(); @@ -72,7 +76,7 @@ public void setMethod(Method method) { this.method = method; } - public void setMethodArgs(Object[] methodArgs) { + public void setMethodArgs(@Nullable Object[] methodArgs) { this.methodArgs = methodArgs; } @@ -80,7 +84,7 @@ public void setConnectionInfo(ConnectionInfo connectionInfo) { this.connectionInfo = connectionInfo; } - public void setThrowable(Throwable throwable) { + public void setThrowable(@Nullable Throwable throwable) { this.throwable = throwable; } @@ -132,7 +136,7 @@ public void setCurrentResultCount(int currentResultCount) { this.currentResultCount = currentResultCount; } - public void setCurrentMappedResult(Object currentResult) { + public void setCurrentMappedResult(@Nullable Object currentResult) { this.currentMappedResult = currentResult; } diff --git a/src/main/java/io/r2dbc/proxy/core/MethodExecutionInfo.java b/src/main/java/io/r2dbc/proxy/core/MethodExecutionInfo.java index 6a7b125a..dfc0c39f 100644 --- a/src/main/java/io/r2dbc/proxy/core/MethodExecutionInfo.java +++ b/src/main/java/io/r2dbc/proxy/core/MethodExecutionInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import io.r2dbc.proxy.listener.ProxyExecutionListener; import io.r2dbc.spi.Connection; +import reactor.util.annotation.Nullable; import java.lang.reflect.Method; import java.time.Duration; @@ -50,6 +51,7 @@ public interface MethodExecutionInfo { * * @return argument lists or {@code null} if the invoked method did not take any arguments */ + @Nullable Object[] getMethodArgs(); /** @@ -58,6 +60,7 @@ public interface MethodExecutionInfo { * * @return result */ + @Nullable Object getResult(); /** @@ -67,6 +70,7 @@ public interface MethodExecutionInfo { * * @return thrown exception */ + @Nullable Throwable getThrown(); /** @@ -75,11 +79,12 @@ public interface MethodExecutionInfo { * * @return connection info */ + @Nullable ConnectionInfo getConnectionInfo(); /** * Get the duration of the method invocation. - * For {@link ProxyExecutionListener#beforeMethod(MethodExecutionInfo)} callback, this returns {@code null}. + * For {@link ProxyExecutionListener#beforeMethod(MethodExecutionInfo)} callback, this returns {@link Duration#ZERO}. * * @return execution duration */ diff --git a/src/main/java/io/r2dbc/proxy/core/QueryExecutionInfo.java b/src/main/java/io/r2dbc/proxy/core/QueryExecutionInfo.java index 0cbfbeb3..68c0d823 100644 --- a/src/main/java/io/r2dbc/proxy/core/QueryExecutionInfo.java +++ b/src/main/java/io/r2dbc/proxy/core/QueryExecutionInfo.java @@ -20,6 +20,7 @@ import io.r2dbc.spi.Batch; import io.r2dbc.spi.Result; import io.r2dbc.spi.Statement; +import reactor.util.annotation.Nullable; import java.lang.reflect.Method; import java.time.Duration; @@ -46,6 +47,7 @@ public interface QueryExecutionInfo { * * @return argument lists or {@code null} if the invoked method did not take any arguments */ + @Nullable Object[] getMethodArgs(); /** @@ -55,6 +57,7 @@ public interface QueryExecutionInfo { * * @return thrown exception */ + @Nullable Throwable getThrowable(); /** @@ -163,6 +166,7 @@ public interface QueryExecutionInfo { * * @return currently mapped result */ + @Nullable Object getCurrentMappedResult(); /** From e7efcb3ffe5d0c31926a0a44739d3f2581bb778f Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Fri, 17 Jan 2020 14:32:23 -0800 Subject: [PATCH 06/74] Update CHANGELOG --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index e5e21773..3b20cfcc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ R2DBC Proxy Changelog ============================= +0.8.1.RELEASE +------------------ +* Make query execution success when at least one element is emitted #55. + 0.8.0.RELEASE ------------------ * Upgrade to Reactor Dysprosium SR2 #47. From 50abfa47c391331749abe6dcc7e9f818013b91e5 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Sun, 26 Jan 2020 21:43:27 -0800 Subject: [PATCH 07/74] Avoid NPE when publisher operation is cancelled Avoid NPE when stopwatch is not started due to operation cancellation. [resolves #56] --- .../callback/CallbackHandlerSupport.java | 33 ++++------ .../callback/CallbackHandlerSupportTest.java | 66 +++++++++++++++++++ 2 files changed, 80 insertions(+), 19 deletions(-) diff --git a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java index 120e4c57..6f7f4bc8 100644 --- a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java +++ b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java @@ -98,8 +98,9 @@ public interface MethodInvocationStrategy { */ private static class StopWatch { - private Clock clock; + private final Clock clock; + @Nullable private Instant startTime; private StopWatch(Clock clock) { @@ -112,12 +113,11 @@ public StopWatch start() { } public Duration getElapsedDuration() { + if (this.startTime == null) { + return Duration.ZERO; // when stopwatch has not started + } return Duration.between(this.startTime, this.clock.instant()); } - - public boolean isStarted() { - return this.startTime != null; - } } protected final ProxyConfig proxyConfig; @@ -185,18 +185,17 @@ protected Object proceedExecution(Method method, Object target, @Nullable Object Publisher result = (Publisher) this.methodInvocationStrategy.invoke(method, target, args); - return Flux.empty() - .doOnSubscribe(s -> { - + return Flux.from(result) + .doFirst(() -> { executionInfo.setThreadName(Thread.currentThread().getName()); executionInfo.setThreadId(Thread.currentThread().getId()); executionInfo.setProxyEventType(ProxyEventType.BEFORE_METHOD); listener.beforeMethod(executionInfo); - + }) + .doOnSubscribe(s -> { stopWatch.start(); }) - .concatWith(result) .map(resultObj -> { // set produced object as result @@ -219,7 +218,6 @@ protected Object proceedExecution(Method method, Object target, @Nullable Object executionInfo.setThrown(throwable); }) .doFinally(signalType -> { - executionInfo.setExecuteDuration(stopWatch.getElapsedDuration()); executionInfo.setThreadName(Thread.currentThread().getName()); executionInfo.setThreadId(Thread.currentThread().getId()); @@ -278,21 +276,18 @@ protected Flux interceptQueryExecution(Publisher queryExecutionFlux = Flux.empty() - .ofType(Result.class) - .doOnSubscribe(s -> { - + Flux queryExecutionFlux = Flux.from(flux) + .doFirst(() -> { executionInfo.setThreadName(Thread.currentThread().getName()); executionInfo.setThreadId(Thread.currentThread().getId()); executionInfo.setCurrentMappedResult(null); executionInfo.setProxyEventType(ProxyEventType.BEFORE_QUERY); listener.beforeQuery(executionInfo); - + }) + .doOnSubscribe(s -> { stopWatch.start(); - }) - .concatWith(flux) .doOnNext(result -> { // When at least one element is emitted, consider query execution is success, even when // the publisher is canceled. see https://github.com/r2dbc/r2dbc-proxy/issues/55 @@ -305,7 +300,7 @@ protected Flux interceptQueryExecution(Publisher { - executionInfo.setExecuteDuration(stopWatch.isStarted() ? stopWatch.getElapsedDuration() : Duration.ZERO); + executionInfo.setExecuteDuration(stopWatch.getElapsedDuration()); executionInfo.setThreadName(Thread.currentThread().getName()); executionInfo.setThreadId(Thread.currentThread().getId()); executionInfo.setCurrentMappedResult(null); diff --git a/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java b/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java index c5f11b43..26f37de3 100644 --- a/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java @@ -192,6 +192,36 @@ void interceptQueryExecutionWithFailure() { assertThat(executionInfo.getCurrentResultCount()).isEqualTo(0); } + @Test + void interceptQueryExecutionWithImmediateCancel() { + LastExecutionAwareListener listener = new LastExecutionAwareListener(); + MutableQueryExecutionInfo executionInfo = new MutableQueryExecutionInfo(); + + ProxyFactory proxyFactory = mock(ProxyFactory.class); + + CompositeProxyExecutionListener compositeListener = new CompositeProxyExecutionListener(listener); + when(this.proxyConfig.getListeners()).thenReturn(compositeListener); + when(this.proxyConfig.getProxyFactory()).thenReturn(proxyFactory); + + // produce single result + Result mockResult = MockResult.empty(); + Mono resultPublisher = Mono.just(mockResult); + + Flux result = this.callbackHandlerSupport.interceptQueryExecution(resultPublisher, executionInfo); + + // Cancels immediately + StepVerifier.create(result) + .thenCancel() + .verify(); + + assertThat(listener.getBeforeMethodExecutionInfo()).isNull(); + assertThat(listener.getAfterMethodExecutionInfo()).isNull(); + assertThat(listener.getBeforeQueryExecutionInfo()).isSameAs(executionInfo); + assertThat(listener.getAfterQueryExecutionInfo()).isSameAs(executionInfo); + + assertThat(executionInfo.getExecuteDuration()).isEqualTo(Duration.ZERO); + } + @Test void interceptQueryExecutionWithMultipleResult() { @@ -450,6 +480,42 @@ void proceedExecutionWithPublisherThrowsException() throws Throwable { assertThat(afterMethodExecution.getThrown()).isSameAs(exception); } + @SuppressWarnings("unchecked") + @Test + void proceedExecutionWithPublisherImmediateCancel() throws Throwable { + + // target method returns Publisher + Method executeMethod = ReflectionUtils.findMethod(Batch.class, "execute"); + Batch target = mock(Batch.class); + Object[] args = new Object[]{}; + LastExecutionAwareListener listener = new LastExecutionAwareListener(); + ConnectionInfo connectionInfo = MockConnectionInfo.empty(); + + // produce single result in order to trigger StepVerifier#consumeNextWith. + Result mockResult = MockResult.empty(); + Mono publisher = Mono.just(mockResult); + + doReturn(publisher).when(target).execute(); + + Object result = this.callbackHandlerSupport.proceedExecution(executeMethod, target, args, listener, connectionInfo, null, null); + + // verify method on target is invoked + verify(target).execute(); + + StepVerifier.create((Publisher) result) + .thenCancel() + .verify(); + + + MethodExecutionInfo beforeMethodExecution = listener.getBeforeMethodExecutionInfo(); + MethodExecutionInfo afterMethodExecution = listener.getAfterMethodExecutionInfo(); + assertThat(afterMethodExecution).isSameAs(beforeMethodExecution); + + assertThat(listener.getBeforeQueryExecutionInfo()).isNull(); + assertThat(listener.getAfterQueryExecutionInfo()).isNull(); + + assertThat(afterMethodExecution.getExecuteDuration()).isEqualTo(Duration.ZERO); + } @Test void proceedExecutionWithNonPublisher() throws Throwable { From 2c60858e8123ba018d545a32508424bf9f184cd4 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Sun, 26 Jan 2020 21:44:20 -0800 Subject: [PATCH 08/74] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 3b20cfcc..182a746f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ R2DBC Proxy Changelog 0.8.1.RELEASE ------------------ * Make query execution success when at least one element is emitted #55. +* Avoid NPE when publisher operation is cancelled #56. 0.8.0.RELEASE ------------------ From 2f95425d065bd582b3e13e4474504380ad77cdfb Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Fri, 31 Jan 2020 11:13:11 -0600 Subject: [PATCH 09/74] Upgrade to SPI 0.8.1.RELEASE. [resolves #58] --- pom.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4bdf1d28..3eeb82e7 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ 1.2.3 UTF-8 UTF-8 - 0.8.0.RELEASE + 0.8.1.RELEASE Dysprosium-SR2 2.1.8.RELEASE 3.1.0 @@ -313,6 +313,14 @@ + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + spring-milestones Spring Milestones From 0f24754cdfae1ac899d638d6716511c0c6b79539 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Fri, 31 Jan 2020 11:14:28 -0600 Subject: [PATCH 10/74] Use different container when deploying to maven central. [resolves #59] --- Jenkinsfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1fb8a3b8..5b3f6fca 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -94,7 +94,7 @@ pipeline { } agent { docker { - image 'adoptopenjdk/openjdk8:latest' + image 'springci/spring-hateoas-openjdk8-with-graphviz-and-jq:latest' args '-v $HOME/.m2:/tmp/jenkins-home/.m2' } } @@ -132,7 +132,7 @@ pipeline { } agent { docker { - image 'adoptopenjdk/openjdk8:latest' + image 'springci/spring-hateoas-openjdk8-with-graphviz-and-jq:latest' args '-v $HOME/.m2:/tmp/jenkins-home/.m2' } } @@ -171,7 +171,7 @@ pipeline { script { slackSend( color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', - channel: '#r2dbc', + channel: '#r2dbc-dev', message: "${currentBuild.fullDisplayName} - `${currentBuild.currentResult}`\n${env.BUILD_URL}") emailext( subject: "[${currentBuild.fullDisplayName}] ${currentBuild.currentResult}", From 5e2b5ab7b87fe7388c294e2c6e251137c2e3bc15 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Fri, 31 Jan 2020 11:15:25 -0600 Subject: [PATCH 11/74] Upgrade to Reactor Dysprosium-SR4. [resolves #60] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3eeb82e7..2ecfa666 100644 --- a/pom.xml +++ b/pom.xml @@ -40,7 +40,7 @@ UTF-8 UTF-8 0.8.1.RELEASE - Dysprosium-SR2 + Dysprosium-SR4 2.1.8.RELEASE 3.1.0 From e95a371d6c84e574466008bef1d284dda2355db1 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Fri, 31 Jan 2020 11:15:49 -0600 Subject: [PATCH 12/74] v0.8.1.RELEASE Release --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2ecfa666..5c3cab14 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ io.r2dbc r2dbc-proxy - 0.8.1.BUILD-SNAPSHOT + 0.8.1.RELEASE jar Reactive Relational Database Connectivity - Proxy From acc3a02030998a4188bede05bf636a8550c2b6d9 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Fri, 31 Jan 2020 11:15:51 -0600 Subject: [PATCH 13/74] v0.8.2.BUILD-SNAPSHOT Development --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5c3cab14..75c6e9b5 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ io.r2dbc r2dbc-proxy - 0.8.1.RELEASE + 0.8.2.BUILD-SNAPSHOT jar Reactive Relational Database Connectivity - Proxy From 1ea380493e1ed0fd31fed026aee127a777fec473 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Fri, 6 Mar 2020 09:32:02 -0600 Subject: [PATCH 14/74] Tune deployment process so releases are staged on maven central. Stop using bintray as the vehicle to reach maven central. bintray caches as the group level, so an r2dbc-h2 0.9.2 release ends up blocking all other modules if they are at, say, 0.9.1. Instead, stage releases directly on sonatype. Let the user manually drop. Patch maven caching so we get better leverage out of our resources. Also handle service releases. [#62] --- CI.adoc | 23 +++++ Jenkinsfile | 117 ++++------------------ ci/build-and-deploy-to-artifactory.sh | 10 ++ ci/build-and-deploy-to-maven-central.sh | 22 +++++ ci/build.sh | 5 - ci/promote-to-bintray.sh | 42 -------- ci/sync-to-maven-central.sh | 23 ----- ci/test.sh | 2 +- pom.xml | 123 ++++++++++++++++++------ settings.xml | 12 +++ 10 files changed, 179 insertions(+), 200 deletions(-) create mode 100644 CI.adoc create mode 100755 ci/build-and-deploy-to-artifactory.sh create mode 100755 ci/build-and-deploy-to-maven-central.sh delete mode 100755 ci/build.sh delete mode 100755 ci/promote-to-bintray.sh delete mode 100755 ci/sync-to-maven-central.sh create mode 100644 settings.xml diff --git a/CI.adoc b/CI.adoc new file mode 100644 index 00000000..de8cd6be --- /dev/null +++ b/CI.adoc @@ -0,0 +1,23 @@ += Continuous Integration + +== Running CI tasks locally + +Since this pipeline is purely Docker-based, it's easy to: + +* Debug what went wrong on your local machine. +* Test out a a tweak to your test routine before sending it out. +* Experiment against a new image before submitting your pull request. + +All of these use cases are great reasons to essentially run what the CI server does on your local machine. + +IMPORTANT: To do this you must have Docker installed on your machine. + +1. `docker run -it -u 1001:1001 --mount type=bind,source="$(pwd)",target=/r2dbc-h2-github springci/r2dbc-openjdk8-with-gpg:latest /bin/bash` ++ +This will launch the Docker image and mount your source code at `r2dbc-h2-github`. ++ +2. `cd r2dbc-h2-github + +You're all set! Since the container is binding to your source, you can make edits from your IDE and continue to run build jobs. + +NOTE: Docker containers can eat up disk space fast! From time to time, run `docker system prune` to clean out old images. \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index 5b3f6fca..615c65c9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -25,8 +25,8 @@ pipeline { sh 'PROFILE=none ci/test.sh' } } - - stage('Deploy to Artifactory') { + + stage('Deploy') { when { anyOf { branch '0.8.x' @@ -35,7 +35,7 @@ pipeline { } agent { docker { - image 'adoptopenjdk/openjdk8:latest' + image 'springci/r2dbc-openjdk8-with-gpg:latest' args '-v $HOME/.m2:/tmp/jenkins-home/.m2' } } @@ -43,12 +43,13 @@ pipeline { environment { ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c') + SONATYPE = credentials('oss-token') + KEYRING = credentials('spring-signing-secring.gpg') + PASSPHRASE = credentials('spring-gpg-passphrase') } steps { script { - sh 'rm -rf ?' - // Warm up this plugin quietly before using it. sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -q org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version' @@ -60,106 +61,22 @@ pipeline { RELEASE_TYPE = 'milestone' // .RC? or .M? - if (PROJECT_VERSION.endsWith('SNAPSHOT')) { + if (PROJECT_VERSION.endsWith('SNAPSHOT')) { // .SNAPSHOT RELEASE_TYPE = 'snapshot' - } else if (PROJECT_VERSION.endsWith('RELEASE')) { + } else if (PROJECT_VERSION.endsWith('RELEASE') || PROJECT_VERSION ==~ /.*SR[0-9]+/) { // .RELEASE or .SR? RELEASE_TYPE = 'release' } - // Capture build output... - OUTPUT = sh( - script: "PROFILE=ci,${RELEASE_TYPE} ci/build.sh", - returnStdout: true - ).trim() - - echo "$OUTPUT" - - // ...to extract artifactory build info - build_info_path = OUTPUT.split('\n') - .find { it.contains('Artifactory Build Info Recorder') } - .split('Saving Build Info to ')[1] - .trim()[1..-2] - - // Stash the JSON build info to support promotion to bintray - dir(build_info_path + '/..') { - stash name: 'build_info', includes: "*.json" - } - } - } - } - - stage('Promote to Bintray') { - when { - branch 'release-0.x' - } - agent { - docker { - image 'springci/spring-hateoas-openjdk8-with-graphviz-and-jq:latest' - args '-v $HOME/.m2:/tmp/jenkins-home/.m2' - } - } - options { timeout(time: 20, unit: 'MINUTES') } - - environment { - ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c') - } - - steps { - script { - sh 'rm -rf ?' - - // Warm up this plugin quietly before using it. - sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -q org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version' - - PROJECT_VERSION = sh( - script: 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version -o | grep -v INFO', - returnStdout: true - ).trim() - - if (PROJECT_VERSION.endsWith('RELEASE')) { - unstash name: 'build_info' - sh "ci/promote-to-bintray.sh" - } else { - echo "${PROJECT_VERSION} is not a candidate for promotion to Bintray." - } - } - } - } - - stage('Sync to Maven Central') { - when { - branch 'release-0.x' - } - agent { - docker { - image 'springci/spring-hateoas-openjdk8-with-graphviz-and-jq:latest' - args '-v $HOME/.m2:/tmp/jenkins-home/.m2' - } - } - options { timeout(time: 20, unit: 'MINUTES') } - - environment { - BINTRAY = credentials('Bintray-spring-operator') - SONATYPE = credentials('oss-token') - } - - steps { - script { - sh 'rm -rf ?' - - // Warm up this plugin quietly before using it. - sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -q org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version' - - PROJECT_VERSION = sh( - script: 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version -o | grep -v INFO', - returnStdout: true - ).trim() - - if (PROJECT_VERSION.endsWith('RELEASE')) { - unstash name: 'build_info' - sh "ci/sync-to-maven-central.sh" + if (RELEASE_TYPE == 'release') { + sh "PROFILE=central ci/build-and-deploy-to-maven-central.sh" + script { + slackSend( + color: 'warning', + channel: '#r2dbc-dev', + message: "WORKING ON: ${currentBuild.fullDisplayName} - `${currentBuild.currentResult}`\n${env.BUILD_URL} staged on Maven Central, awaiting final release.") + } } else { - echo "${PROJECT_VERSION} is not a candidate for syncing to Maven Central." + sh "PROFILE=${RELEASE_TYPE} ci/build-and-deploy-to-artifactory.sh" } } } diff --git a/ci/build-and-deploy-to-artifactory.sh b/ci/build-and-deploy-to-artifactory.sh new file mode 100755 index 00000000..1c6b6d1e --- /dev/null +++ b/ci/build-and-deploy-to-artifactory.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -euo pipefail + +# +# Deploy the artifactory +# +echo 'Deploying to Artifactory...' + +MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -P${PROFILE} -Dmaven.test.skip=true clean deploy -B diff --git a/ci/build-and-deploy-to-maven-central.sh b/ci/build-and-deploy-to-maven-central.sh new file mode 100755 index 00000000..6af5b16f --- /dev/null +++ b/ci/build-and-deploy-to-maven-central.sh @@ -0,0 +1,22 @@ +#!/bin/bash -x + +set -euo pipefail + +# +# Stage on Maven Central +# +echo 'Staging on Maven Central...' + +GNUPGHOME=/tmp/gpghome +export GNUPGHOME + +mkdir $GNUPGHOME +cp $KEYRING $GNUPGHOME + +MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw \ + -s settings.xml \ + -P${PROFILE} \ + -Dmaven.test.skip=true \ + -Dgpg.passphrase=${PASSPHRASE} \ + -Dgpg.secretKeyring=${GNUPGHOME}/secring.gpg \ + clean deploy -B diff --git a/ci/build.sh b/ci/build.sh deleted file mode 100755 index 4ff384cf..00000000 --- a/ci/build.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/r2dbc-proxy-maven-repository" ./mvnw -P${PROFILE} -Dmaven.test.skip=true clean deploy -B diff --git a/ci/promote-to-bintray.sh b/ci/promote-to-bintray.sh deleted file mode 100755 index a00bb8d6..00000000 --- a/ci/promote-to-bintray.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -set -e -u - -buildName=`jq -r '.name' build-info.json` -buildNumber=`jq -r '.number' build-info.json` -groupId=`jq -r '.modules[0].id' build-info.json | sed 's/\(.*\):.*:.*/\1/'` -version=`jq -r '.modules[0].id' build-info.json | sed 's/.*:.*:\(.*\)/\1/'` - -echo "Promoting ${buildName}/${buildNumber}/${groupId}/${version} to libs-release-local" - -curl \ - -s \ - --connect-timeout 240 \ - --max-time 2700 \ - -u ${ARTIFACTORY_USR}:${ARTIFACTORY_PSW} \ - -H 'Content-type:application/json' \ - -d '{"sourceRepos": ["libs-release-local"], "targetRepo" : "spring-distributions", "async":"true"}' \ - -f \ - -X \ - POST "https://repo.spring.io/api/build/distribute/${buildName}/${buildNumber}" > /dev/null || { echo "Failed to distribute" >&2; exit 1; } - -echo "Waiting for artifacts to be published" - -ARTIFACTS_PUBLISHED=false -WAIT_TIME=10 -COUNTER=0 - -while [ $ARTIFACTS_PUBLISHED == "false" ] && [ $COUNTER -lt 120 ]; do - - result=$( curl -s https://api.bintray.com/packages/spring/jars/"${groupId}" ) - versions=$( echo "$result" | jq -r '.versions' ) - exists=$( echo "$versions" | grep "$version" -o || true ) - - if [ "$exists" = "$version" ]; then - ARTIFACTS_PUBLISHED=true - fi - - COUNTER=$(( COUNTER + 1 )) - sleep $WAIT_TIME - -done diff --git a/ci/sync-to-maven-central.sh b/ci/sync-to-maven-central.sh deleted file mode 100755 index 7150ca76..00000000 --- a/ci/sync-to-maven-central.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -set -e -u - -buildName=`jq -r '.name' build-info.json` -buildNumber=`jq -r '.number' build-info.json` -groupId=`jq -r '.modules[0].id' build-info.json | sed 's/\(.*\):.*:.*/\1/'` -version=`jq -r '.modules[0].id' build-info.json | sed 's/.*:.*:\(.*\)/\1/'` - -echo "Syncing ${buildName}/${buildNumber}/${groupId}/${version} to Maven Central..." - -curl \ - -s \ - --connect-timeout 240 \ - --max-time 2700 \ - -u ${BINTRAY_USR}:${BINTRAY_PSW} \ - -H 'Content-Type: application/json' \ - -d "{ \"username\": \"${SONATYPE_USR}\", \"password\": \"${SONATYPE_PSW}\"}" \ - -f \ - -X \ - POST "https://api.bintray.com/maven_central_sync/spring/jars/${groupId}/versions/${version}" > /dev/null || { echo "Failed to sync" >&2; exit 1; } - -echo "Sync complete" diff --git a/ci/test.sh b/ci/test.sh index 656ce88e..9effba2f 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -2,4 +2,4 @@ set -euo pipefail -MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/r2dbc-proxy-maven-repository" ./mvnw -P${PROFILE} clean dependency:list test -Dsort -B +MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -P${PROFILE} clean dependency:list test -Dsort -B diff --git a/pom.xml b/pom.xml index 75c6e9b5..608ed26f 100644 --- a/pom.xml +++ b/pom.xml @@ -311,36 +311,9 @@ - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - false - - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - true - - - - + - + snapshot @@ -464,8 +437,100 @@ + + central + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + sonatype + https://oss.sonatype.org/ + false + true + true + + + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + + + + + + + sonatype + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-plugins-release diff --git a/settings.xml b/settings.xml new file mode 100644 index 00000000..95700a15 --- /dev/null +++ b/settings.xml @@ -0,0 +1,12 @@ + + + + + sonatype + ${env.SONATYPE_USR} + ${env.SONATYPE_PSW} + + + \ No newline at end of file From bc925374b21411abccf0d365387bb80c1fbcf09c Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Tue, 17 Mar 2020 15:51:46 -0500 Subject: [PATCH 15/74] Only deploy if CI job not triggered by upstream event. --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index 615c65c9..5deb66a6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -32,6 +32,7 @@ pipeline { branch '0.8.x' branch 'release-0.x' } + not { triggeredBy 'UpstreamCause' } } agent { docker { From 81d33d066cce53b2ff524847d0e6dde2a6b616f0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 24 Mar 2020 12:20:55 +0100 Subject: [PATCH 16/74] Upgrade to Reactor Dysprosium-SR6 [resolves #63] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 608ed26f..9dc5c164 100644 --- a/pom.xml +++ b/pom.xml @@ -40,7 +40,7 @@ UTF-8 UTF-8 0.8.1.RELEASE - Dysprosium-SR4 + Dysprosium-SR6 2.1.8.RELEASE 3.1.0 @@ -311,7 +311,7 @@ - + From f97a2b1ccea71b58e5b8cdc9fc819d6c61b843a2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 24 Mar 2020 12:22:22 +0100 Subject: [PATCH 17/74] Upgrade build and test dependencies [resolves #65] --- pom.xml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index 9dc5c164..1cea9652 100644 --- a/pom.xml +++ b/pom.xml @@ -32,17 +32,17 @@ https://github.com/r2dbc/r2dbc-proxy - 3.14.0 + 3.15.0 1.8 3.0.2 - 5.5.2 + 5.6.1 1.2.3 UTF-8 UTF-8 0.8.1.RELEASE Dysprosium-SR6 2.1.8.RELEASE - 3.1.0 + 3.3.3 @@ -183,7 +183,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.0 + 3.8.1 -Werror @@ -200,7 +200,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.1.2 + 3.2.0 @@ -222,7 +222,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.1.1 + 3.2.0 https://r2dbc.io/spec/${r2dbc-spi.version}/api/ @@ -242,7 +242,7 @@ org.apache.maven.plugins maven-source-plugin - 3.2.0 + 3.2.1 attach-javadocs @@ -255,7 +255,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.1 + 2.22.2 random @@ -267,7 +267,7 @@ org.codehaus.mojo flatten-maven-plugin - 1.1.0 + 1.2.1 flatten From 9ffaec9bb0d1b44511873893f27e5d4255fe1630 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 24 Mar 2020 12:24:52 +0100 Subject: [PATCH 18/74] Polishing Update badges and versions in readme [#64] --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c0e3baf3..4565b270 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ -# Reactive Relational Database Connectivity Proxy Framework +# Reactive Relational Database Connectivity Proxy Framework [![Build Status](https://travis-ci.org/r2dbc/r2dbc-postgresql.svg?branch=master)](https://travis-ci.org/r2dbc/r2dbc-proxy) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.r2dbc/r2dbc-proxy/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.r2dbc/r2dbc-proxy) This project contains the proxy framework of the [R2DBC SPI][r]. [r]: https://github.com/r2dbc/r2dbc-spi -## Maven -Both milestone and snapshot artifacts (library, source, and javadoc) can be found in Maven repositories. +### Maven configuration + +Artifacts can be found on [Maven Central](https://search.maven.org/search?q=r2dbc-proxy): ```xml io.r2dbc r2dbc-proxy - 0.8.0.RC1 + ${version} ``` @@ -432,3 +433,44 @@ ConnectionFactory proxyConnectionFactory = [r2dbc-proxy-samples]: https://github.com/ttddyy/r2dbc-proxy-examples [TracingExecutionListener]: https://github.com/ttddyy/r2dbc-proxy-examples/blob/master/listener-example/src/main/java/io/r2dbc/examples/TracingExecutionListener.java [MetricsExecutionListener]: https://github.com/ttddyy/r2dbc-proxy-examples/blob/master/listener-example/src/main/java/io/r2dbc/examples/MetricsExecutionListener.java + +## Getting Help + +Having trouble with R2DBC? We'd love to help! + +* Check the [spec documentation](https://r2dbc.io/spec/0.8.1.RELEASE/spec/html/), and [Javadoc](https://r2dbc.io/spec/0.8.1.RELEASE/api/). +* If you are upgrading, check out the [changelog](https://r2dbc.io/spec/0.8.1.RELEASE/CHANGELOG.txt) for "new and noteworthy" features. +* Ask a question - we monitor [stackoverflow.com](https://stackoverflow.com) for questions + tagged with [`r2dbc`](https://stackoverflow.com/tags/r2dbc). + You can also chat with the community on [Gitter](https://gitter.im/r2dbc/r2dbc). +* Report bugs with R2DBC Proxy at [github.com/r2dbc/r2dbc-proxy/issues](https://github.com/r2dbc/r2dbc-proxy/issues). + +## Reporting Issues + +R2DBC uses GitHub as issue tracking system to record bugs and feature requests. +If you want to raise an issue, please follow the recommendations below: + +* Before you log a bug, please search the [issue tracker](https://github.com/r2dbc/r2dbc-proxy/issues) to see if someone has already reported the problem. +* If the issue doesn't already exist, [create a new issue](https://github.com/r2dbc/r2dbc-proxy/issues/new). +* Please provide as much information as possible with the issue report, we like to know the version of R2DBC Proxy that you are using and JVM version. +* If you need to paste code, or include a stack trace use Markdown ``` escapes before and after your text. +* If possible try to create a test-case or project that replicates the issue. +Attach a link to your code or a compressed file containing your code. + +## Building from Source + +You don't need to build from source to use R2DBC Proxy (binaries in Maven Central), but if you want to try out the latest and greatest, R2DBC Proxy can be easily built with the +[maven wrapper](https://github.com/takari/maven-wrapper). You also need JDK 1.8 and Docker to run integration tests. + +```bash + $ ./mvnw clean install +``` + +If you want to build with the regular `mvn` command, you will need [Maven v3.5.0 or above](https://maven.apache.org/run-maven/index.html). + +_Also see [CONTRIBUTING.adoc](CONTRIBUTING.adoc) if you wish to submit pull requests, and in particular please sign the [Contributor's Agreement](https://cla.pivotal.io/sign/spring) before your first change, however trivial._ + +## License +This project is released under version 2.0 of the [Apache License][l]. + +[l]: https://www.apache.org/licenses/LICENSE-2.0 From d2ea693ae2f9d580db346d60dfb8d3e64543030c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 24 Mar 2020 12:28:03 +0100 Subject: [PATCH 19/74] Update changelog [#64] --- CHANGELOG | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 182a746f..5b8a0a08 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ R2DBC Proxy Changelog ============================= +0.8.2.RELEASE +------------------ +* Upgrade build and test dependencies #65 +* Release 0.8.2.RELEASE #64 +* Upgrade to Reactor Dysprosium-SR6 #63 +* Stage releases directly on maven central #62 + 0.8.1.RELEASE ------------------ * Make query execution success when at least one element is emitted #55. From c3ca8c8427bcbe6a28e50992dd25e70824efb448 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Wed, 25 Mar 2020 11:30:09 -0500 Subject: [PATCH 20/74] v0.8.2.RELEASE Release --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1cea9652..0d8c2340 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ io.r2dbc r2dbc-proxy - 0.8.2.BUILD-SNAPSHOT + 0.8.2.RELEASE jar Reactive Relational Database Connectivity - Proxy From 1dcadca8af491a9969abf069a31ca57d51d951a9 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Wed, 25 Mar 2020 11:30:16 -0500 Subject: [PATCH 21/74] v0.8.3.BUILD-SNAPSHOT Development --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0d8c2340..c4495985 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ io.r2dbc r2dbc-proxy - 0.8.2.RELEASE + 0.8.3.BUILD-SNAPSHOT jar Reactive Relational Database Connectivity - Proxy From 4f354348d6441876b05146f52d7f72087aeebfd4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 May 2020 10:31:19 +0200 Subject: [PATCH 22/74] Upgrade dependencies * assertj 3.15.0 -> 3.16.1 * Reactor BOM Dysprosium SR6 -> Dysprosium SR7 * Spring Boot 2.1.8.RELEASE 2.3.0.RELEASE [closes #66] --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index c4495985..3a744d15 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ https://github.com/r2dbc/r2dbc-proxy - 3.15.0 + 3.16.1 1.8 3.0.2 5.6.1 @@ -40,8 +40,8 @@ UTF-8 UTF-8 0.8.1.RELEASE - Dysprosium-SR6 - 2.1.8.RELEASE + Dysprosium-SR7 + 2.2.0.RELEASE 3.3.3 From e3f16ef4885960959a56b9ebb23debbaf3039405 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 24 Jun 2020 12:12:37 +0200 Subject: [PATCH 23/74] Rename master branch to main [closes #67] --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4565b270..069e48da 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ This project contains the proxy framework of the [R2DBC SPI][r]. [r]: https://github.com/r2dbc/r2dbc-spi +## Code of Conduct + +This project is governed by the [R2DBC Code of Conduct](https://github.com/r2dbc/.github/blob/main/CODE_OF_CONDUCT.adoc). By participating, you are expected to uphold this code of conduct. Please report unacceptable behavior to [info@r2dbc.io](mailto:info@r2dbc.io). + + ### Maven configuration Artifacts can be found on [Maven Central](https://search.maven.org/search?q=r2dbc-proxy): @@ -88,11 +93,6 @@ Publisher connectionPublisher = connectionFactory.create() Mono connectionMono = Mono.from(connectionFactory.create()); ``` -## License -This project is released under version 2.0 of the [Apache License][l]. - -[l]: https://www.apache.org/licenses/LICENSE-2.0 - ---- ## Use cases @@ -468,7 +468,7 @@ You don't need to build from source to use R2DBC Proxy (binaries in Maven Centra If you want to build with the regular `mvn` command, you will need [Maven v3.5.0 or above](https://maven.apache.org/run-maven/index.html). -_Also see [CONTRIBUTING.adoc](CONTRIBUTING.adoc) if you wish to submit pull requests, and in particular please sign the [Contributor's Agreement](https://cla.pivotal.io/sign/spring) before your first change, however trivial._ +_Also see [CONTRIBUTING.adoc](https://github.com/r2dbc/.github/blob/main/CONTRIBUTING.adoc) if you wish to submit pull requests, and in particular please sign the [Contributor's Agreement](https://cla.pivotal.io/sign/reactor) before your first change, however trivial._ ## License This project is released under version 2.0 of the [Apache License][l]. From 33984e2ed33c5ca606b02587c3cb92d0c92db5ef Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Tue, 15 Sep 2020 16:00:06 -0700 Subject: [PATCH 24/74] Fix "ConnectionFactory#create" after-method callback Wrap the return of `ConnectionFactory#create` with Mono. This fixes after-method callback to be invoked immediately after original `create` method is invoked in case when "usingWhen" is used. [closes #68] --- .../ConnectionFactoryCallbackHandler.java | 20 ++++- .../ConnectionFactoryCallbackHandlerTest.java | 82 ++++++++++++++++++- 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java b/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java index 07f5b5f4..bea7a2dc 100644 --- a/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java +++ b/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import io.r2dbc.proxy.util.Assert; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; import java.lang.reflect.Method; import java.util.function.BiFunction; @@ -37,6 +39,7 @@ public ConnectionFactoryCallbackHandler(ConnectionFactory connectionFactory, Pro this.connectionFactory = Assert.requireNonNull(connectionFactory, "connectionFactory must not be null"); } + @SuppressWarnings("unchecked") @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Assert.requireNonNull(proxy, "proxy must not be null"); @@ -51,7 +54,6 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl BiFunction onMap = null; if ("create".equals(methodName)) { - // callback for creating connection proxy onMap = (resultObj, executionInfo) -> { executionInfo.setResult(resultObj); @@ -67,13 +69,23 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl executionInfo.setConnectionInfo(connectionInfo); Connection proxyConnection = this.proxyConfig.getProxyFactory().wrapConnection(connection, connectionInfo); - return proxyConnection; }; - } Object result = proceedExecution(method, this.connectionFactory, args, this.proxyConfig.getListeners(), null, onMap, null); + + if ("create".equals(methodName)) { + // gh-68: + // "proceedExecution" returns a Flux that has logic to performs before/after method callbacks. + // When "usingWhen" is used with "create" method (e.g.: "Mono.usingWhen(connectionFactory.create(), resourceClosure, ...)"), + // the calling order becomes "before-method", actual "create", "resource-closure", then "after-method". + // Instead, we want "before-method", actual "create", *"after-method"*, then "resource-closure" + // By wrapping the Flux with Mono, when a connection is emitted (onNext), the Mono sends "cancel" to the Flux which triggers + // "doFinally" on Flux and calls "after-method" callback before "resource-closure" is called. + return Mono.from((Publisher) result); + } + return result; } diff --git a/src/test/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandlerTest.java b/src/test/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandlerTest.java index f7ed44da..bd55127e 100644 --- a/src/test/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandlerTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandlerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import io.r2dbc.proxy.core.ConnectionInfo; import io.r2dbc.proxy.core.MethodExecutionInfo; import io.r2dbc.proxy.listener.LastExecutionAwareListener; +import io.r2dbc.proxy.listener.LifeCycleListener; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryMetadata; @@ -26,10 +27,17 @@ import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import org.springframework.util.ReflectionUtils; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -130,4 +138,76 @@ void unwrap() throws Throwable { assertThat(result).isSameAs(connectionFactory); } + // gh-68 + @Test + void createConnectionWithUsingWhen() throws Throwable { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + Connection mockedConnection = mock(Connection.class); + doReturn(Mono.just(mockedConnection)).when(connectionFactory).create(); + + List list = Collections.synchronizedList(new ArrayList<>()); + AtomicReference createdConnectionHolder = new AtomicReference<>(); + + LifeCycleListener listener = new LifeCycleListener() { + + @Override + public void beforeCreateOnConnectionFactory(MethodExecutionInfo methodExecutionInfo) { + list.add("listener-before-create"); + } + + @Override + public void afterCreateOnConnectionFactory(MethodExecutionInfo methodExecutionInfo) { + createdConnectionHolder.set(methodExecutionInfo.getResult()); + list.add("listener-after-create"); + } + }; + + Function> resourceClosure = (conn) -> { + list.add("resource-closure"); + return Mono.just("foo"); + }; + Function> asyncComplete = (conn) -> { + list.add("async-complete"); + return Mono.empty(); + }; + BiFunction> asyncError = (conn, thr) -> { + list.add("async-error"); + return Mono.empty(); + }; + Function> asyncCancel = (conn) -> { + list.add("resource-cancel"); + return Mono.empty(); + }; + + ProxyConfig proxyConfig = ProxyConfig.builder() + .listener(listener) + .build(); + + ProxyFactory proxyFactory = proxyConfig.getProxyFactory(); + ConnectionFactory proxyConnectionFactory = proxyFactory.wrapConnectionFactory(connectionFactory); + + // Mono#usingWhen + Mono mono = Mono.usingWhen(proxyConnectionFactory.create(), resourceClosure, asyncComplete, asyncError, asyncCancel); + StepVerifier.create(mono) + .expectSubscription() + .expectNextCount(1) + .verifyComplete(); + + assertThat(list).containsExactly("listener-before-create", "listener-after-create", "resource-closure", "async-complete"); + assertThat(createdConnectionHolder).hasValue(mockedConnection); + + list.clear(); + createdConnectionHolder.set(null); + + // Flux#usingWhen + Flux flux = Flux.usingWhen(proxyConnectionFactory.create(), resourceClosure, asyncComplete, asyncError, asyncCancel); + StepVerifier.create(flux) + .expectSubscription() + .expectNextCount(1) + .verifyComplete(); + + assertThat(list).containsExactly("listener-before-create", "listener-after-create", "resource-closure", "async-complete"); + assertThat(createdConnectionHolder).hasValue(mockedConnection); + } + } From dfe9e2a218ad52e2f90d6ca280481a22257add2c Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Sat, 19 Sep 2020 11:52:48 -0700 Subject: [PATCH 25/74] Add ProxyConfigHolder Add `ProxyConfigHolder` interface and handling logic to the proxy handler. [closes #69] --- .../callback/CallbackHandlerSupport.java | 5 ++ .../r2dbc/proxy/callback/JdkProxyFactory.java | 16 ++--- .../proxy/callback/ProxyConfigHolder.java | 33 ++++++++++ .../callback/BatchCallbackHandlerTest.java | 18 +++++- .../ConnectionCallbackHandlerTest.java | 18 +++++- .../ConnectionFactoryCallbackHandlerTest.java | 15 +++++ .../proxy/callback/JdkProxyFactoryTest.java | 63 ++++++++++++------- .../callback/ResultCallbackHandlerTest.java | 18 +++++- .../StatementCallbackHandlerTest.java | 17 +++++ 9 files changed, 169 insertions(+), 34 deletions(-) create mode 100644 src/main/java/io/r2dbc/proxy/callback/ProxyConfigHolder.java diff --git a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java index 6f7f4bc8..64f8ea83 100644 --- a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java +++ b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java @@ -170,6 +170,11 @@ protected Object proceedExecution(Method method, Object target, @Nullable Object return sb.toString(); // differentiate toString message. } + // special handling for "ProxyConfigHolder#getProxyConfig" + if ("getProxyConfig".equals(method.getName())) { + return this.proxyConfig; + } + StopWatch stopWatch = new StopWatch(this.proxyConfig.getClock()); diff --git a/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java b/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java index 0a3ac14b..660b7266 100644 --- a/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java +++ b/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ package io.r2dbc.proxy.callback; import io.r2dbc.proxy.core.ConnectionInfo; -import io.r2dbc.proxy.core.StatementInfo; import io.r2dbc.proxy.core.QueryExecutionInfo; +import io.r2dbc.proxy.core.StatementInfo; import io.r2dbc.proxy.util.Assert; import io.r2dbc.spi.Batch; import io.r2dbc.spi.Connection; @@ -60,7 +60,7 @@ public ConnectionFactory wrapConnectionFactory(ConnectionFactory connectionFacto CallbackHandler logic = new ConnectionFactoryCallbackHandler(connectionFactory, this.proxyConfig); CallbackInvocationHandler invocationHandler = new CallbackInvocationHandler(logic); return (ConnectionFactory) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), - new Class[]{ConnectionFactory.class, Wrapped.class}, invocationHandler); + new Class[]{ConnectionFactory.class, Wrapped.class, ProxyConfigHolder.class}, invocationHandler); } @Override @@ -71,7 +71,7 @@ public Connection wrapConnection(Connection connection, ConnectionInfo connectio CallbackHandler logic = new ConnectionCallbackHandler(connection, connectionInfo, this.proxyConfig); CallbackInvocationHandler invocationHandler = new CallbackInvocationHandler(logic); return (Connection) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), - new Class[]{Connection.class, Wrapped.class, ConnectionHolder.class}, invocationHandler); + new Class[]{Connection.class, Wrapped.class, ConnectionHolder.class, ProxyConfigHolder.class}, invocationHandler); } @Override @@ -82,7 +82,7 @@ public Batch wrapBatch(Batch batch, ConnectionInfo connectionInfo) { CallbackHandler logic = new BatchCallbackHandler(batch, connectionInfo, this.proxyConfig); CallbackInvocationHandler invocationHandler = new CallbackInvocationHandler(logic); return (Batch) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), - new Class[]{Batch.class, Wrapped.class, ConnectionHolder.class}, invocationHandler); + new Class[]{Batch.class, Wrapped.class, ConnectionHolder.class, ProxyConfigHolder.class}, invocationHandler); } @Override @@ -94,7 +94,7 @@ public Statement wrapStatement(Statement statement, StatementInfo statementInfo, CallbackHandler logic = new StatementCallbackHandler(statement, statementInfo, connectionInfo, this.proxyConfig); CallbackInvocationHandler invocationHandler = new CallbackInvocationHandler(logic); return (Statement) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), - new Class[]{Statement.class, Wrapped.class, ConnectionHolder.class}, invocationHandler); + new Class[]{Statement.class, Wrapped.class, ConnectionHolder.class, ProxyConfigHolder.class}, invocationHandler); } @Override @@ -105,7 +105,7 @@ public Result wrapResult(Result result, QueryExecutionInfo queryExecutionInfo) { CallbackHandler logic = new ResultCallbackHandler(result, queryExecutionInfo, this.proxyConfig); CallbackInvocationHandler invocationHandler = new CallbackInvocationHandler(logic); return (Result) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), - new Class[]{Result.class, Wrapped.class, ConnectionHolder.class}, invocationHandler); + new Class[]{Result.class, Wrapped.class, ConnectionHolder.class, ProxyConfigHolder.class}, invocationHandler); } @@ -114,7 +114,7 @@ public Result wrapResult(Result result, QueryExecutionInfo queryExecutionInfo) { */ static class CallbackInvocationHandler implements InvocationHandler { - private CallbackHandler delegate; + private final CallbackHandler delegate; public CallbackInvocationHandler(CallbackHandler delegate) { Assert.requireNonNull(delegate, "delegate must not be null"); diff --git a/src/main/java/io/r2dbc/proxy/callback/ProxyConfigHolder.java b/src/main/java/io/r2dbc/proxy/callback/ProxyConfigHolder.java new file mode 100644 index 00000000..9164e4eb --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/callback/ProxyConfigHolder.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.callback; + +/** + * Provide a method to retrieve {@link ProxyConfig} from proxy object. + * + * @author Tadaya Tsuyukubo + */ +public interface ProxyConfigHolder { + + /** + * Retrieve {@link ProxyConfig}. + * + * @return proxy config object + */ + ProxyConfig getProxyConfig(); + +} diff --git a/src/test/java/io/r2dbc/proxy/callback/BatchCallbackHandlerTest.java b/src/test/java/io/r2dbc/proxy/callback/BatchCallbackHandlerTest.java index 624d729d..dabe8fc6 100644 --- a/src/test/java/io/r2dbc/proxy/callback/BatchCallbackHandlerTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/BatchCallbackHandlerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,8 @@ import static org.mockito.Mockito.when; /** + * Test for {@link BatchCallbackHandler}. + * * @author Tadaya Tsuyukubo */ public class BatchCallbackHandlerTest { @@ -47,6 +49,8 @@ public class BatchCallbackHandlerTest { private static Method UNWRAP_METHOD = ReflectionUtils.findMethod(Wrapped.class, "unwrap"); + private static Method GET_PROXY_CONFIG_METHOD = ReflectionUtils.findMethod(ProxyConfigHolder.class, "getProxyConfig"); + @Test @SuppressWarnings("unchecked") void batchOperation() throws Throwable { @@ -129,4 +133,16 @@ void add() throws Throwable { assertThat(result).isSameAs(proxyBatch); } + @Test + void getProxyConfig() throws Throwable { + Batch batch = mock(Batch.class); + ConnectionInfo connectionInfo = mock(ConnectionInfo.class); + ProxyConfig proxyConfig = new ProxyConfig(); + + BatchCallbackHandler callback = new BatchCallbackHandler(batch, connectionInfo, proxyConfig); + + Object result = callback.invoke(batch, GET_PROXY_CONFIG_METHOD, null); + assertThat(result).isSameAs(proxyConfig); + } + } diff --git a/src/test/java/io/r2dbc/proxy/callback/ConnectionCallbackHandlerTest.java b/src/test/java/io/r2dbc/proxy/callback/ConnectionCallbackHandlerTest.java index 688bffc9..bb7929aa 100644 --- a/src/test/java/io/r2dbc/proxy/callback/ConnectionCallbackHandlerTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/ConnectionCallbackHandlerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,8 @@ import static org.mockito.Mockito.when; /** + * Test for {@link ConnectionCallbackHandler}. + * * @author Tadaya Tsuyukubo */ public class ConnectionCallbackHandlerTest { @@ -58,6 +60,8 @@ public class ConnectionCallbackHandlerTest { private static Method UNWRAP_METHOD = ReflectionUtils.findMethod(Wrapped.class, "unwrap"); + private static Method GET_PROXY_CONFIG_METHOD = ReflectionUtils.findMethod(ProxyConfigHolder.class, "getProxyConfig"); + @Test void createBatch() throws Throwable { LastExecutionAwareListener listener = new LastExecutionAwareListener(); @@ -255,4 +259,16 @@ void unwrap() throws Throwable { assertThat(result).isSameAs(connection); } + @Test + void getProxyConfig() throws Throwable { + Connection connection = mock(Connection.class); + DefaultConnectionInfo connectionInfo = new DefaultConnectionInfo(); + ProxyConfig proxyConfig = new ProxyConfig(); + + ConnectionCallbackHandler callback = new ConnectionCallbackHandler(connection, connectionInfo, proxyConfig); + + Object result = callback.invoke(connection, GET_PROXY_CONFIG_METHOD, null); + assertThat(result).isSameAs(proxyConfig); + } + } diff --git a/src/test/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandlerTest.java b/src/test/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandlerTest.java index bd55127e..44aedba4 100644 --- a/src/test/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandlerTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandlerTest.java @@ -46,6 +46,8 @@ import static org.mockito.Mockito.when; /** + * Test for {@link ConnectionFactoryCallbackHandler}. + * * @author Tadaya Tsuyukubo */ public class ConnectionFactoryCallbackHandlerTest { @@ -56,6 +58,8 @@ public class ConnectionFactoryCallbackHandlerTest { private static Method UNWRAP_METHOD = ReflectionUtils.findMethod(Wrapped.class, "unwrap"); + private static Method GET_PROXY_CONFIG_METHOD = ReflectionUtils.findMethod(ProxyConfigHolder.class, "getProxyConfig"); + @Test void createConnection() throws Throwable { @@ -210,4 +214,15 @@ public void afterCreateOnConnectionFactory(MethodExecutionInfo methodExecutionIn assertThat(createdConnectionHolder).hasValue(mockedConnection); } + @Test + void getProxyConfig() throws Throwable { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + ProxyConfig proxyConfig = new ProxyConfig(); + + ConnectionFactoryCallbackHandler callback = new ConnectionFactoryCallbackHandler(connectionFactory, proxyConfig); + + Object result = callback.invoke(connectionFactory, GET_PROXY_CONFIG_METHOD, null); + assertThat(result).isSameAs(proxyConfig); + } + } diff --git a/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java b/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java index 84af7530..421ca297 100644 --- a/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,17 +17,20 @@ package io.r2dbc.proxy.callback; import io.r2dbc.proxy.core.ConnectionInfo; +import io.r2dbc.proxy.core.QueryExecutionInfo; import io.r2dbc.proxy.core.StatementInfo; import io.r2dbc.proxy.test.MockConnectionInfo; import io.r2dbc.proxy.test.MockStatementInfo; import io.r2dbc.spi.Batch; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Result; import io.r2dbc.spi.Statement; import io.r2dbc.spi.Wrapped; import io.r2dbc.spi.test.MockBatch; import io.r2dbc.spi.test.MockConnection; import io.r2dbc.spi.test.MockConnectionFactory; +import io.r2dbc.spi.test.MockResult; import io.r2dbc.spi.test.MockStatement; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -39,6 +42,8 @@ import static org.mockito.Mockito.when; /** + * Test for {@link JdkProxyFactory}. + * * @author Tadaya Tsuyukubo */ public class JdkProxyFactoryTest { @@ -64,30 +69,42 @@ void isProxy() { Connection connection = MockConnection.empty(); Batch batch = MockBatch.empty(); Statement statement = MockStatement.empty(); + Result result = MockResult.empty(); ConnectionInfo connectionInfo = MockConnectionInfo.empty(); StatementInfo statementInfo = MockStatementInfo.empty(); - - Object result; - - result = this.proxyFactory.wrapConnectionFactory(connectionFactory); - assertThat(Proxy.isProxyClass(result.getClass())).isTrue(); - assertThat(result).isInstanceOf(Wrapped.class); - assertThat(result).isNotInstanceOf(ConnectionHolder.class); - - result = this.proxyFactory.wrapConnection(connection, connectionInfo); - assertThat(Proxy.isProxyClass(result.getClass())).isTrue(); - assertThat(result).isInstanceOf(Wrapped.class); - assertThat(result).isInstanceOf(ConnectionHolder.class); - - result = this.proxyFactory.wrapBatch(batch, connectionInfo); - assertThat(Proxy.isProxyClass(result.getClass())).isTrue(); - assertThat(result).isInstanceOf(Wrapped.class); - assertThat(result).isInstanceOf(ConnectionHolder.class); - - result = this.proxyFactory.wrapStatement(statement, statementInfo, connectionInfo); - assertThat(Proxy.isProxyClass(result.getClass())).isTrue(); - assertThat(result).isInstanceOf(Wrapped.class); - assertThat(result).isInstanceOf(ConnectionHolder.class); + QueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); // need to be mutable + + Object wrapped; + + wrapped = this.proxyFactory.wrapConnectionFactory(connectionFactory); + assertThat(Proxy.isProxyClass(wrapped.getClass())).isTrue(); + assertThat(wrapped).isInstanceOf(Wrapped.class); + assertThat(wrapped).isNotInstanceOf(ConnectionHolder.class); + assertThat(wrapped).isInstanceOf(ProxyConfigHolder.class); + + wrapped = this.proxyFactory.wrapConnection(connection, connectionInfo); + assertThat(Proxy.isProxyClass(wrapped.getClass())).isTrue(); + assertThat(wrapped).isInstanceOf(Wrapped.class); + assertThat(wrapped).isInstanceOf(ConnectionHolder.class); + assertThat(wrapped).isInstanceOf(ProxyConfigHolder.class); + + wrapped = this.proxyFactory.wrapBatch(batch, connectionInfo); + assertThat(Proxy.isProxyClass(wrapped.getClass())).isTrue(); + assertThat(wrapped).isInstanceOf(Wrapped.class); + assertThat(wrapped).isInstanceOf(ConnectionHolder.class); + assertThat(wrapped).isInstanceOf(ProxyConfigHolder.class); + + wrapped = this.proxyFactory.wrapStatement(statement, statementInfo, connectionInfo); + assertThat(Proxy.isProxyClass(wrapped.getClass())).isTrue(); + assertThat(wrapped).isInstanceOf(Wrapped.class); + assertThat(wrapped).isInstanceOf(ConnectionHolder.class); + assertThat(wrapped).isInstanceOf(ProxyConfigHolder.class); + + wrapped = this.proxyFactory.wrapResult(result, queryExecutionInfo); + assertThat(Proxy.isProxyClass(wrapped.getClass())).isTrue(); + assertThat(wrapped).isInstanceOf(Wrapped.class); + assertThat(wrapped).isInstanceOf(ConnectionHolder.class); + assertThat(wrapped).isInstanceOf(ProxyConfigHolder.class); } @Test diff --git a/src/test/java/io/r2dbc/proxy/callback/ResultCallbackHandlerTest.java b/src/test/java/io/r2dbc/proxy/callback/ResultCallbackHandlerTest.java index 0e10f1f3..43f397b1 100644 --- a/src/test/java/io/r2dbc/proxy/callback/ResultCallbackHandlerTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/ResultCallbackHandlerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,8 @@ import static org.mockito.Mockito.when; /** + * Test for {@link ResultCallbackHandler}. + * * @author Tadaya Tsuyukubo */ public class ResultCallbackHandlerTest { @@ -50,6 +52,8 @@ public class ResultCallbackHandlerTest { private static Method UNWRAP_METHOD = ReflectionUtils.findMethod(Wrapped.class, "unwrap"); + private static Method GET_PROXY_CONFIG_METHOD = ReflectionUtils.findMethod(ProxyConfigHolder.class, "getProxyConfig"); + @Test void map() throws Throwable { @@ -268,4 +272,16 @@ void unwrap() throws Throwable { assertThat(result).isSameAs(mockResult); } + @Test + void getProxyConfig() throws Throwable { + Result mockResult = MockResult.empty(); + MutableQueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); + ProxyConfig proxyConfig = new ProxyConfig(); + + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig); + + Object result = callback.invoke(mockResult, GET_PROXY_CONFIG_METHOD, null); + assertThat(result).isSameAs(proxyConfig); + } + } diff --git a/src/test/java/io/r2dbc/proxy/callback/StatementCallbackHandlerTest.java b/src/test/java/io/r2dbc/proxy/callback/StatementCallbackHandlerTest.java index e7a4922e..7200544d 100644 --- a/src/test/java/io/r2dbc/proxy/callback/StatementCallbackHandlerTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/StatementCallbackHandlerTest.java @@ -46,6 +46,8 @@ import static org.mockito.Mockito.when; /** + * Test for {@link StatementCallbackHandler}. + * * @author Tadaya Tsuyukubo */ public class StatementCallbackHandlerTest { @@ -64,6 +66,8 @@ public class StatementCallbackHandlerTest { private static Method UNWRAP_METHOD = ReflectionUtils.findMethod(Wrapped.class, "unwrap"); + private static Method GET_PROXY_CONFIG_METHOD = ReflectionUtils.findMethod(ProxyConfigHolder.class, "getProxyConfig"); + @Test void add() throws Throwable { LastExecutionAwareListener testListener = new LastExecutionAwareListener(); @@ -373,4 +377,17 @@ void unwrap() throws Throwable { assertThat(result).isSameAs(statement); } + @Test + void getProxyConfig() throws Throwable { + Statement statement = MockStatement.empty(); + ConnectionInfo connectionInfo = MockConnectionInfo.empty(); + ProxyConfig proxyConfig = new ProxyConfig(); + StatementInfo statementInfo = MockStatementInfo.empty(); + + StatementCallbackHandler callback = new StatementCallbackHandler(statement, statementInfo, connectionInfo, proxyConfig); + + Object result = callback.invoke(statement, GET_PROXY_CONFIG_METHOD, null); + assertThat(result).isSameAs(proxyConfig); + } + } From abaca50c04270344274419b335768691b9a4fa43 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Mon, 21 Sep 2020 17:33:43 -0700 Subject: [PATCH 26/74] Introduce "FluxQueryInvocation" Previously, simple Flux with `doFirst()` and `doFinally()` were used to invoke method before/after callbacks. Instead, create `FluxQueryInvocation`, a new FluxOperator` and its subscriber/subscription to perform callback logic. [issue #70] --- .../callback/CallbackHandlerSupport.java | 53 ++----- .../proxy/callback/FluxQueryInvocation.java | 150 ++++++++++++++++++ .../callback/CallbackHandlerSupportTest.java | 7 +- 3 files changed, 162 insertions(+), 48 deletions(-) create mode 100644 src/main/java/io/r2dbc/proxy/callback/FluxQueryInvocation.java diff --git a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java index 64f8ea83..03b46c55 100644 --- a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java +++ b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java @@ -96,14 +96,14 @@ public interface MethodInvocationStrategy { /** * Utility class to get duration of executions. */ - private static class StopWatch { + static class StopWatch { private final Clock clock; @Nullable private Instant startTime; - private StopWatch(Clock clock) { + StopWatch(Clock clock) { this.clock = clock; } @@ -267,59 +267,24 @@ protected Object proceedExecution(Method method, Object target, @Nullable Object /** * Augment query execution result to hook up listener lifecycle. * - * @param flux query invocation result publisher + * @param publisher query invocation result publisher * @param executionInfo query execution context info * @return query invocation result flux * @throws IllegalArgumentException if {@code flux} is {@code null} * @throws IllegalArgumentException if {@code executionInfo} is {@code null} */ - protected Flux interceptQueryExecution(Publisher flux, MutableQueryExecutionInfo executionInfo) { - Assert.requireNonNull(flux, "flux must not be null"); + protected Flux interceptQueryExecution(Publisher publisher, MutableQueryExecutionInfo executionInfo) { + Assert.requireNonNull(publisher, "flux must not be null"); Assert.requireNonNull(executionInfo, "executionInfo must not be null"); - ProxyExecutionListener listener = this.proxyConfig.getListeners(); - - StopWatch stopWatch = new StopWatch(this.proxyConfig.getClock()); - - Flux queryExecutionFlux = Flux.from(flux) - .doFirst(() -> { - executionInfo.setThreadName(Thread.currentThread().getName()); - executionInfo.setThreadId(Thread.currentThread().getId()); - executionInfo.setCurrentMappedResult(null); - executionInfo.setProxyEventType(ProxyEventType.BEFORE_QUERY); - - listener.beforeQuery(executionInfo); - }) - .doOnSubscribe(s -> { - stopWatch.start(); - }) - .doOnNext(result -> { - // When at least one element is emitted, consider query execution is success, even when - // the publisher is canceled. see https://github.com/r2dbc/r2dbc-proxy/issues/55 - executionInfo.setSuccess(true); - }).doOnComplete(() -> { - executionInfo.setSuccess(true); - }) - .doOnError(throwable -> { - executionInfo.setThrowable(throwable); - executionInfo.setSuccess(false); - }) - .doFinally(signalType -> { - executionInfo.setExecuteDuration(stopWatch.getElapsedDuration()); - executionInfo.setThreadName(Thread.currentThread().getName()); - executionInfo.setThreadId(Thread.currentThread().getId()); - executionInfo.setCurrentMappedResult(null); - executionInfo.setProxyEventType(ProxyEventType.AFTER_QUERY); - - listener.afterQuery(executionInfo); - }); - ProxyFactory proxyFactory = this.proxyConfig.getProxyFactory(); - // return a publisher that returns proxy Result - return Flux.from(queryExecutionFlux) + Flux flux = new FluxQueryInvocation(Flux.from(publisher), executionInfo, this.proxyConfig) + // return a publisher that returns proxy Result .flatMap(queryResult -> Mono.just(proxyFactory.wrapResult(queryResult, executionInfo))); + return flux; + } /** diff --git a/src/main/java/io/r2dbc/proxy/callback/FluxQueryInvocation.java b/src/main/java/io/r2dbc/proxy/callback/FluxQueryInvocation.java new file mode 100644 index 00000000..2347f17f --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/callback/FluxQueryInvocation.java @@ -0,0 +1,150 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.callback; + +import io.r2dbc.proxy.core.ProxyEventType; +import io.r2dbc.proxy.listener.ProxyExecutionListener; +import io.r2dbc.spi.Batch; +import io.r2dbc.spi.Result; +import io.r2dbc.spi.Statement; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Scannable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxOperator; +import reactor.util.annotation.Nullable; + +/** + * Call before/after query callbacks by wrapping the result of {@link Statement#execute()} or + * {@link Batch#execute()} operations. + * + * @author Tadaya Tsuyukubo + */ +public class FluxQueryInvocation extends FluxOperator { + + private final MutableQueryExecutionInfo executionInfo; + + private final ProxyConfig proxyConfig; + + public FluxQueryInvocation(Flux source, MutableQueryExecutionInfo executionInfo, ProxyConfig proxyConfig) { + super(source); + this.executionInfo = executionInfo; + this.proxyConfig = proxyConfig; + } + + @Override + public void subscribe(CoreSubscriber actual) { + this.source.subscribe(new QueryInvocationSubscriber(actual, this.executionInfo, this.proxyConfig)); + } + + static class QueryInvocationSubscriber implements CoreSubscriber, Subscription, Scannable { + + private final CoreSubscriber delegate; + + private final MutableQueryExecutionInfo executionInfo; + + private final ProxyExecutionListener listener; + + private final CallbackHandlerSupport.StopWatch stopWatch; + + private Subscription subscription; + + public QueryInvocationSubscriber(CoreSubscriber delegate, MutableQueryExecutionInfo executionInfo, ProxyConfig proxyConfig) { + this.delegate = delegate; + this.executionInfo = executionInfo; + this.listener = proxyConfig.getListeners(); + this.stopWatch = new CallbackHandlerSupport.StopWatch(proxyConfig.getClock()); + } + + @Override + public void onSubscribe(Subscription s) { + this.subscription = s; + beforeQuery(); + this.delegate.onSubscribe(this); + } + + @Override + public void onNext(Result result) { + // When at least one element is emitted, consider query execution is success, even when + // the publisher is canceled. see https://github.com/r2dbc/r2dbc-proxy/issues/55 + this.executionInfo.setSuccess(true); + this.delegate.onNext(result); + } + + @Override + public void onError(Throwable t) { + this.executionInfo.setThrowable(t); + this.executionInfo.setSuccess(false); + afterQuery(); + this.delegate.onError(t); + } + + @Override + public void onComplete() { + this.executionInfo.setSuccess(true); + afterQuery(); + this.delegate.onComplete(); + } + + @Override + public void request(long n) { + this.subscription.request(n); + } + + @Override + public void cancel() { + // do not determine success/failure by cancel + afterQuery(); + this.subscription.cancel(); + } + + @Override + @Nullable + @SuppressWarnings("rawtypes") + public Object scanUnsafe(Attr key) { + if (key == Attr.ACTUAL) { + return this.delegate; + } + if (key == Attr.PARENT) { + return this.subscription; + } + return null; + } + + private void beforeQuery() { + this.executionInfo.setThreadName(Thread.currentThread().getName()); + this.executionInfo.setThreadId(Thread.currentThread().getId()); + this.executionInfo.setCurrentMappedResult(null); + this.executionInfo.setProxyEventType(ProxyEventType.BEFORE_QUERY); + + this.stopWatch.start(); + + this.listener.beforeQuery(this.executionInfo); + } + + private void afterQuery() { + this.executionInfo.setExecuteDuration(this.stopWatch.getElapsedDuration()); + this.executionInfo.setThreadName(Thread.currentThread().getName()); + this.executionInfo.setThreadId(Thread.currentThread().getId()); + this.executionInfo.setCurrentMappedResult(null); + this.executionInfo.setProxyEventType(ProxyEventType.AFTER_QUERY); + + this.listener.afterQuery(this.executionInfo); + } + } + +} diff --git a/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java b/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java index 26f37de3..b89594fd 100644 --- a/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -243,7 +243,7 @@ void interceptQueryExecutionWithMultipleResult() { Result mockResult2 = mock(Result.class); Result mockResult3 = mock(Result.class); Flux publisher = Flux.just(mockResult1, mockResult2, mockResult3) - .doOnSubscribe(subscription -> { + .doOnRequest(subscription -> { // this will be called AFTER listener.beforeQuery() but BEFORE emitting query result from this publisher. // verify BEFORE_QUERY assertThat(executionInfo.getProxyEventType()).isEqualTo(ProxyEventType.BEFORE_QUERY); @@ -315,7 +315,7 @@ void interceptQueryExecutionWithEmptyResult() { // produce multiple results Flux publisher = Flux.empty() - .doOnSubscribe(subscription -> { + .doOnRequest(subscription -> { // this will be called AFTER listener.beforeQuery() but BEFORE emitting query result from this publisher. // verify BEFORE_QUERY assertThat(executionInfo.getProxyEventType()).isEqualTo(ProxyEventType.BEFORE_QUERY); @@ -324,7 +324,6 @@ void interceptQueryExecutionWithEmptyResult() { assertThat(executionInfo.getCurrentResultCount()).isEqualTo(0); assertThat(executionInfo.getCurrentMappedResult()).isNull(); }); - ; Flux result = this.callbackHandlerSupport.interceptQueryExecution(publisher, executionInfo); From 80bf30c0bf48634525043cf795a762fdbea874ab Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 23 Sep 2020 10:22:28 -0700 Subject: [PATCH 27/74] Use dedicated operator for method callback Previously, for methods that return publisher, method callback logic is implemented by wrapping the result publisher with new Flux using `doOnSubscribe`, `doFinally`, etc. In this commit, instead of wrap and augment the publisher, creates dedicated operators, `[Mono|Flux]MethodInvocation` and its subscriber/subscription, to handle method callback handling. Also, adds `MonoMethodInvocationConnectionFactoryCreate` to special handle `ConnectionFactory#create` method callbacks. [issue #70] --- .../proxy/callback/BatchCallbackHandler.java | 2 +- .../callback/CallbackHandlerSupport.java | 54 +------- .../callback/ConnectionCallbackHandler.java | 4 +- .../ConnectionFactoryCallbackHandler.java | 83 ++++++----- .../proxy/callback/FluxMethodInvocation.java | 54 ++++++++ .../callback/MethodInvocationSubscriber.java | 131 ++++++++++++++++++ .../proxy/callback/MonoMethodInvocation.java | 54 ++++++++ ...thodInvocationConnectionFactoryCreate.java | 63 +++++++++ .../proxy/callback/ResultCallbackHandler.java | 2 +- .../callback/StatementCallbackHandler.java | 4 +- .../callback/CallbackHandlerSupportTest.java | 23 +-- 11 files changed, 377 insertions(+), 97 deletions(-) create mode 100644 src/main/java/io/r2dbc/proxy/callback/FluxMethodInvocation.java create mode 100644 src/main/java/io/r2dbc/proxy/callback/MethodInvocationSubscriber.java create mode 100644 src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocation.java create mode 100644 src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocationConnectionFactoryCreate.java diff --git a/src/main/java/io/r2dbc/proxy/callback/BatchCallbackHandler.java b/src/main/java/io/r2dbc/proxy/callback/BatchCallbackHandler.java index 6df7e74e..ab99df42 100644 --- a/src/main/java/io/r2dbc/proxy/callback/BatchCallbackHandler.java +++ b/src/main/java/io/r2dbc/proxy/callback/BatchCallbackHandler.java @@ -63,7 +63,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl return this.connectionInfo.getOriginalConnection(); } - Object result = proceedExecution(method, this.batch, args, this.proxyConfig.getListeners(), this.connectionInfo, null, null); + Object result = proceedExecution(method, this.batch, args, this.proxyConfig.getListeners(), this.connectionInfo, null); if ("add".equals(methodName)) { this.queries.add((String) args[0]); diff --git a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java index 03b46c55..fb6746cd 100644 --- a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java +++ b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java @@ -35,7 +35,6 @@ import java.time.Instant; import java.util.Arrays; import java.util.Set; -import java.util.function.BiFunction; import java.util.function.Consumer; import static java.util.stream.Collectors.toSet; @@ -136,8 +135,7 @@ public CallbackHandlerSupport(ProxyConfig proxyConfig) { * @param args arguments for the method. {@code null} if the method doesn't take any arguments. * @param listener listener that before/after method callbacks will be called * @param connectionInfo current connection information. {@code null} when invoked operation is not associated to the {@link Connection}. - * @param onMap a callback that will be chained on "map()" right after the result of the method invocation - * @param onComplete a callback that will be chained as the first doOnComplete on the result of the method invocation + * @param onComplete a callback that will be invoked at successful termination(onComplete) of the result publisher. * @return result of invoking the original object * @throws Throwable thrown exception during the invocation * @throws IllegalArgumentException if {@code method} is {@code null} @@ -146,7 +144,6 @@ public CallbackHandlerSupport(ProxyConfig proxyConfig) { */ protected Object proceedExecution(Method method, Object target, @Nullable Object[] args, ProxyExecutionListener listener, @Nullable ConnectionInfo connectionInfo, - @Nullable BiFunction onMap, @Nullable Consumer onComplete) throws Throwable { Assert.requireNonNull(method, "method must not be null"); Assert.requireNonNull(target, "target must not be null"); @@ -187,51 +184,12 @@ protected Object proceedExecution(Method method, Object target, @Nullable Object Class returnType = method.getReturnType(); if (Publisher.class.isAssignableFrom(returnType)) { - Publisher result = (Publisher) this.methodInvocationStrategy.invoke(method, target, args); - - return Flux.from(result) - .doFirst(() -> { - executionInfo.setThreadName(Thread.currentThread().getName()); - executionInfo.setThreadId(Thread.currentThread().getId()); - executionInfo.setProxyEventType(ProxyEventType.BEFORE_METHOD); - - listener.beforeMethod(executionInfo); - }) - .doOnSubscribe(s -> { - stopWatch.start(); - }) - .map(resultObj -> { - - // set produced object as result - executionInfo.setResult(resultObj); - - // apply a function to flux-chain right after the original publisher operations - if (onMap != null) { - return onMap.apply(resultObj, executionInfo); - } - return resultObj; - }) - .doOnComplete(() -> { - // apply a consumer to flux-chain right after the original publisher operations - // this is the first chained doOnComplete on the result publisher - if (onComplete != null) { - onComplete.accept(executionInfo); - } - }) - .doOnError(throwable -> { - executionInfo.setThrown(throwable); - }) - .doFinally(signalType -> { - executionInfo.setExecuteDuration(stopWatch.getElapsedDuration()); - executionInfo.setThreadName(Thread.currentThread().getName()); - executionInfo.setThreadId(Thread.currentThread().getId()); - executionInfo.setProxyEventType(ProxyEventType.AFTER_METHOD); - - listener.afterMethod(executionInfo); - }); - - + if (result instanceof Mono) { + return new MonoMethodInvocation((Mono) result, executionInfo, proxyConfig, onComplete); + } else { + return new FluxMethodInvocation(Flux.from(result), executionInfo, proxyConfig, onComplete); + } } else { // for method that generates non-publisher, execution happens when it is invoked. diff --git a/src/main/java/io/r2dbc/proxy/callback/ConnectionCallbackHandler.java b/src/main/java/io/r2dbc/proxy/callback/ConnectionCallbackHandler.java index d893cd8d..9c007ec9 100644 --- a/src/main/java/io/r2dbc/proxy/callback/ConnectionCallbackHandler.java +++ b/src/main/java/io/r2dbc/proxy/callback/ConnectionCallbackHandler.java @@ -90,13 +90,13 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl // replace the query args[0] = updatedQuery; - Object result = proceedExecution(method, this.connection, args, this.proxyConfig.getListeners(), this.connectionInfo, null, onComplete); + Object result = proceedExecution(method, this.connection, args, this.proxyConfig.getListeners(), this.connectionInfo, null); return this.proxyConfig.getProxyFactory().wrapStatement((Statement) result, statementInfo, this.connectionInfo); } // TODO: createSavepoint, releaseSavepoint, rollbackTransactionToSavepoint - Object result = proceedExecution(method, this.connection, args, this.proxyConfig.getListeners(), this.connectionInfo, null, onComplete); + Object result = proceedExecution(method, this.connection, args, this.proxyConfig.getListeners(), this.connectionInfo, onComplete); if ("createBatch".equals(methodName)) { return this.proxyConfig.getProxyFactory().wrapBatch((Batch) result, this.connectionInfo); diff --git a/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java b/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java index bea7a2dc..7ae10f43 100644 --- a/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java +++ b/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java @@ -16,6 +16,7 @@ package io.r2dbc.proxy.callback; +import io.r2dbc.proxy.core.ProxyEventType; import io.r2dbc.proxy.util.Assert; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; @@ -23,7 +24,6 @@ import reactor.core.publisher.Mono; import java.lang.reflect.Method; -import java.util.function.BiFunction; /** * Proxy callback handler for {@link ConnectionFactory}. @@ -51,41 +51,58 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl return this.connectionFactory; } - BiFunction onMap = null; - - if ("create".equals(methodName)) { - // callback for creating connection proxy - onMap = (resultObj, executionInfo) -> { - executionInfo.setResult(resultObj); - - Connection connection = (Connection) resultObj; // original connection - String connectionId = this.proxyConfig.getConnectionIdManager().getId(connection); - - DefaultConnectionInfo connectionInfo = new DefaultConnectionInfo(); - connectionInfo.setConnectionId(connectionId); - connectionInfo.setClosed(false); - connectionInfo.setOriginalConnection(connection); - - executionInfo.setConnectionInfo(connectionInfo); - - Connection proxyConnection = this.proxyConfig.getProxyFactory().wrapConnection(connection, connectionInfo); - return proxyConnection; - }; - } - - Object result = proceedExecution(method, this.connectionFactory, args, this.proxyConfig.getListeners(), null, onMap, null); - if ("create".equals(methodName)) { - // gh-68: - // "proceedExecution" returns a Flux that has logic to performs before/after method callbacks. - // When "usingWhen" is used with "create" method (e.g.: "Mono.usingWhen(connectionFactory.create(), resourceClosure, ...)"), - // the calling order becomes "before-method", actual "create", "resource-closure", then "after-method". - // Instead, we want "before-method", actual "create", *"after-method"*, then "resource-closure" - // By wrapping the Flux with Mono, when a connection is emitted (onNext), the Mono sends "cancel" to the Flux which triggers - // "doFinally" on Flux and calls "after-method" callback before "resource-closure" is called. - return Mono.from((Publisher) result); + Object target = this.connectionFactory; + StopWatch stopWatch = new StopWatch(this.proxyConfig.getClock()); + + // Method execution info + // Since Connection is not yet created, do not set ConnectionInfo + MutableMethodExecutionInfo executionInfo = new MutableMethodExecutionInfo(); + executionInfo.setMethod(method); + executionInfo.setMethodArgs(args); + executionInfo.setTarget(target); + + Publisher result = (Publisher) this.methodInvocationStrategy.invoke(method, target, args); + + // gh-68: Use special operator dedicated to "ConnectionFactory#create" method. + // Normally, method that returns a Publisher uses "proceedExecution(...)" from parent class. This method returns + // a "[Mono|Flux]MethodInvocation" that have logic to performs before/after method callbacks. + // However, when "ConnectionFactory#create" is used with "usingWhen", + // (e.g.: "Mono.usingWhen(connectionFactory.create(), resourceClosure, ...)"), the calling order becomes + // ["before-method", actual "create", "resource-closure", "after-method"]. + // Instead, we want ["before-method", actual "create", *"after-method"*, "resource-closure"] + // Therefore, here uses special mono operator that does not invoke "afterMethod" in "onComplete". + // Then, use "doOnSuccess()" to call "afterMethod" callback. This way, "after-method" is performed + // before "resource-closure" + return new MonoMethodInvocationConnectionFactoryCreate(Mono.from(result), executionInfo, proxyConfig) + .map(resultObj -> { + // set produced object as result + executionInfo.setResult(resultObj); + + // construct ConnectionInfo and returns proxy Connection + Connection connection = (Connection) resultObj; // original connection + String connectionId = this.proxyConfig.getConnectionIdManager().getId(connection); + + DefaultConnectionInfo connectionInfo = new DefaultConnectionInfo(); + connectionInfo.setConnectionId(connectionId); + connectionInfo.setClosed(false); + connectionInfo.setOriginalConnection(connection); + executionInfo.setConnectionInfo(connectionInfo); + + Connection proxyConnection = this.proxyConfig.getProxyFactory().wrapConnection(connection, connectionInfo); + return proxyConnection; + }) + .doOnSuccess((o) -> { + // invoke "afterMethod" callback + executionInfo.setExecuteDuration(stopWatch.getElapsedDuration()); + executionInfo.setThreadName(Thread.currentThread().getName()); + executionInfo.setThreadId(Thread.currentThread().getId()); + executionInfo.setProxyEventType(ProxyEventType.AFTER_METHOD); + this.proxyConfig.getListeners().afterMethod(executionInfo); + }); } + Object result = proceedExecution(method, this.connectionFactory, args, this.proxyConfig.getListeners(), null, null); return result; } diff --git a/src/main/java/io/r2dbc/proxy/callback/FluxMethodInvocation.java b/src/main/java/io/r2dbc/proxy/callback/FluxMethodInvocation.java new file mode 100644 index 00000000..dcab648e --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/callback/FluxMethodInvocation.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.callback; + +import io.r2dbc.proxy.core.MethodExecutionInfo; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxOperator; +import reactor.util.annotation.Nullable; + +import java.util.function.Consumer; + +/** + * A flux operator that calls before/after method callbacks. + * + * @author Tadaya Tsuyukubo + * @see MethodInvocationSubscriber + */ +class FluxMethodInvocation extends FluxOperator { + + private final MutableMethodExecutionInfo executionInfo; + + private final ProxyConfig proxyConfig; + + @Nullable + private final Consumer onComplete; + + public FluxMethodInvocation(Flux source, MutableMethodExecutionInfo executionInfo, ProxyConfig proxyConfig, @Nullable Consumer onComplete) { + super(source); + this.executionInfo = executionInfo; + this.proxyConfig = proxyConfig; + this.onComplete = onComplete; + } + + @Override + public void subscribe(CoreSubscriber actual) { + this.source.subscribe(new MethodInvocationSubscriber(actual, this.executionInfo, this.proxyConfig, this.onComplete)); + } + +} diff --git a/src/main/java/io/r2dbc/proxy/callback/MethodInvocationSubscriber.java b/src/main/java/io/r2dbc/proxy/callback/MethodInvocationSubscriber.java new file mode 100644 index 00000000..45e84a20 --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/callback/MethodInvocationSubscriber.java @@ -0,0 +1,131 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.callback; + +import io.r2dbc.proxy.core.MethodExecutionInfo; +import io.r2dbc.proxy.core.ProxyEventType; +import io.r2dbc.proxy.listener.ProxyExecutionListener; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Scannable; +import reactor.util.annotation.Nullable; + +import java.util.function.Consumer; + +/** + * Custom subscriber/subscription to invoke method callback. + * + * @author Tadaya Tsuyukubo + * @see MonoMethodInvocation + * @see FluxMethodInvocation + */ +class MethodInvocationSubscriber implements CoreSubscriber, Subscription, Scannable { + + protected final CoreSubscriber delegate; + + protected final MutableMethodExecutionInfo executionInfo; + + protected final ProxyExecutionListener listener; + + protected final CallbackHandlerSupport.StopWatch stopWatch; + + protected Subscription subscription; + + @Nullable + protected Consumer onComplete; + + public MethodInvocationSubscriber(CoreSubscriber delegate, MutableMethodExecutionInfo executionInfo, ProxyConfig proxyConfig, @Nullable Consumer onComplete) { + this.delegate = delegate; + this.executionInfo = executionInfo; + this.listener = proxyConfig.getListeners(); + this.stopWatch = new CallbackHandlerSupport.StopWatch(proxyConfig.getClock()); + this.onComplete = onComplete; + } + + @Override + public void onSubscribe(Subscription s) { + this.subscription = s; + beforeMethod(); + this.delegate.onSubscribe(this); + } + + @Override + public void onNext(Object object) { + this.executionInfo.setResult(object); // set produced object as result + this.delegate.onNext(object); + } + + @Override + public void onError(Throwable t) { + this.executionInfo.setThrown(t); + afterMethod(); + this.delegate.onError(t); + } + + @Override + public void onComplete() { + if (this.onComplete != null) { + this.onComplete.accept(this.executionInfo); + } + afterMethod(); + this.delegate.onComplete(); + } + + @Override + public void request(long n) { + this.subscription.request(n); + } + + @Override + public void cancel() { + afterMethod(); + this.subscription.cancel(); + } + + @Override + @Nullable + @SuppressWarnings("rawtypes") + public Object scanUnsafe(Scannable.Attr key) { + if (key == Scannable.Attr.ACTUAL) { + return this.delegate; + } + if (key == Scannable.Attr.PARENT) { + return this.subscription; + } + return null; + } + + private void beforeMethod() { + this.executionInfo.setThreadName(Thread.currentThread().getName()); + this.executionInfo.setThreadId(Thread.currentThread().getId()); + this.executionInfo.setProxyEventType(ProxyEventType.BEFORE_METHOD); + + this.stopWatch.start(); + + this.listener.beforeMethod(this.executionInfo); + } + + private void afterMethod() { + this.executionInfo.setExecuteDuration(this.stopWatch.getElapsedDuration()); + this.executionInfo.setThreadName(Thread.currentThread().getName()); + this.executionInfo.setThreadId(Thread.currentThread().getId()); + this.executionInfo.setProxyEventType(ProxyEventType.AFTER_METHOD); + + this.listener.afterMethod(this.executionInfo); + } + +} diff --git a/src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocation.java b/src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocation.java new file mode 100644 index 00000000..37a25def --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocation.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.callback; + +import io.r2dbc.proxy.core.MethodExecutionInfo; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoOperator; +import reactor.util.annotation.Nullable; + +import java.util.function.Consumer; + +/** + * A mono operator that calls before/after method callbacks. + * + * @author Tadaya Tsuyukubo + * @see MethodInvocationSubscriber + */ +class MonoMethodInvocation extends MonoOperator { + + private final MutableMethodExecutionInfo executionInfo; + + private final ProxyConfig proxyConfig; + + @Nullable + private final Consumer onComplete; + + public MonoMethodInvocation(Mono source, MutableMethodExecutionInfo executionInfo, ProxyConfig proxyConfig, @Nullable Consumer onComplete) { + super(source); + this.executionInfo = executionInfo; + this.proxyConfig = proxyConfig; + this.onComplete = onComplete; + } + + @Override + public void subscribe(CoreSubscriber actual) { + this.source.subscribe(new MethodInvocationSubscriber(actual, this.executionInfo, this.proxyConfig, this.onComplete)); + } + +} diff --git a/src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocationConnectionFactoryCreate.java b/src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocationConnectionFactoryCreate.java new file mode 100644 index 00000000..33e83a91 --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocationConnectionFactoryCreate.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.callback; + +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoOperator; + +/** + * Special Mono operator for {@link ConnectionFactory#create()} to invoke before/after + * method callbacks. + * + * @author Tadaya Tsuyukubo + */ +class MonoMethodInvocationConnectionFactoryCreate extends MonoOperator { + + private final MutableMethodExecutionInfo executionInfo; + + private final ProxyConfig proxyConfig; + + public MonoMethodInvocationConnectionFactoryCreate(Mono source, MutableMethodExecutionInfo executionInfo, ProxyConfig proxyConfig) { + super(source); + this.executionInfo = executionInfo; + this.proxyConfig = proxyConfig; + } + + @Override + public void subscribe(CoreSubscriber actual) { + this.source.subscribe(new MonoMethodInvocationConnectionFactoryCreateSubscriber(actual, this.executionInfo, this.proxyConfig)); + } + + static class MonoMethodInvocationConnectionFactoryCreateSubscriber extends MethodInvocationSubscriber { + + public MonoMethodInvocationConnectionFactoryCreateSubscriber(CoreSubscriber delegate, MutableMethodExecutionInfo executionInfo, ProxyConfig proxyConfig) { + super(delegate, executionInfo, proxyConfig, null); + } + + @Override + public void onComplete() { + // "doOnSuccess()" chained to this operator calls "afterMethod()" callback. + // Therefore, do not call "afterMethod" on onComplete(). + // see "ConnectionFactoryCallbackHandler" and how it handles "create" method. + this.delegate.onComplete(); + } + + } + +} diff --git a/src/main/java/io/r2dbc/proxy/callback/ResultCallbackHandler.java b/src/main/java/io/r2dbc/proxy/callback/ResultCallbackHandler.java index af6ef89b..ef313b8e 100644 --- a/src/main/java/io/r2dbc/proxy/callback/ResultCallbackHandler.java +++ b/src/main/java/io/r2dbc/proxy/callback/ResultCallbackHandler.java @@ -73,7 +73,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl return connectionInfo.getOriginalConnection(); } - Object invocationResult = proceedExecution(method, this.result, args, this.proxyConfig.getListeners(), connectionInfo, null, null); + Object invocationResult = proceedExecution(method, this.result, args, this.proxyConfig.getListeners(), connectionInfo, null); if ("map".equals(methodName)) { diff --git a/src/main/java/io/r2dbc/proxy/callback/StatementCallbackHandler.java b/src/main/java/io/r2dbc/proxy/callback/StatementCallbackHandler.java index 92cc5b54..e521af5a 100644 --- a/src/main/java/io/r2dbc/proxy/callback/StatementCallbackHandler.java +++ b/src/main/java/io/r2dbc/proxy/callback/StatementCallbackHandler.java @@ -97,7 +97,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl BindParameterConverter.BindOperation onBind = () -> { try { - proceedExecution(method, this.statement, args, this.proxyConfig.getListeners(), this.connectionInfo, null, null); + proceedExecution(method, this.statement, args, this.proxyConfig.getListeners(), this.connectionInfo, null); } catch (Throwable throwable) { throw new R2dbcProxyException("Failed to perform " + methodName, throwable); } @@ -127,7 +127,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl return proxy; } - Object result = proceedExecution(method, this.statement, args, this.proxyConfig.getListeners(), this.connectionInfo, null, null); + Object result = proceedExecution(method, this.statement, args, this.proxyConfig.getListeners(), this.connectionInfo, null); // add, bind, bindNull, execute if ("add".equals(methodName)) { diff --git a/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java b/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java index b89594fd..8b8424a2 100644 --- a/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java @@ -370,6 +370,7 @@ void proceedExecutionWithPublisher() throws Throwable { Object[] args = new Object[]{}; LastExecutionAwareListener listener = new LastExecutionAwareListener(); ConnectionInfo connectionInfo = MockConnectionInfo.empty(); + when(this.proxyConfig.getListeners()).thenReturn(new CompositeProxyExecutionListener(listener)); // produce single result in order to trigger StepVerifier#consumeNextWith. Result mockResult = MockResult.empty(); @@ -377,7 +378,7 @@ void proceedExecutionWithPublisher() throws Throwable { doReturn(publisher).when(target).execute(); - Object result = this.callbackHandlerSupport.proceedExecution(executeMethod, target, args, listener, connectionInfo, null, null); + Object result = this.callbackHandlerSupport.proceedExecution(executeMethod, target, args, listener, connectionInfo, null); // verify method on target is invoked verify(target).execute(); @@ -433,6 +434,7 @@ void proceedExecutionWithPublisherThrowsException() throws Throwable { Batch target = mock(Batch.class); Object[] args = new Object[]{}; LastExecutionAwareListener listener = new LastExecutionAwareListener(); + when(this.proxyConfig.getListeners()).thenReturn(new CompositeProxyExecutionListener(listener)); ConnectionInfo connectionInfo = MockConnectionInfo.empty(); // publisher that throws exception @@ -441,7 +443,7 @@ void proceedExecutionWithPublisherThrowsException() throws Throwable { doReturn(publisher).when(target).execute(); - Object result = this.callbackHandlerSupport.proceedExecution(executeMethod, target, args, listener, connectionInfo, null, null); + Object result = this.callbackHandlerSupport.proceedExecution(executeMethod, target, args, listener, connectionInfo, null); // verify method on target is invoked verify(target).execute(); @@ -488,6 +490,7 @@ void proceedExecutionWithPublisherImmediateCancel() throws Throwable { Batch target = mock(Batch.class); Object[] args = new Object[]{}; LastExecutionAwareListener listener = new LastExecutionAwareListener(); + when(this.proxyConfig.getListeners()).thenReturn(new CompositeProxyExecutionListener(listener)); ConnectionInfo connectionInfo = MockConnectionInfo.empty(); // produce single result in order to trigger StepVerifier#consumeNextWith. @@ -496,7 +499,7 @@ void proceedExecutionWithPublisherImmediateCancel() throws Throwable { doReturn(publisher).when(target).execute(); - Object result = this.callbackHandlerSupport.proceedExecution(executeMethod, target, args, listener, connectionInfo, null, null); + Object result = this.callbackHandlerSupport.proceedExecution(executeMethod, target, args, listener, connectionInfo, null); // verify method on target is invoked verify(target).execute(); @@ -531,7 +534,7 @@ void proceedExecutionWithNonPublisher() throws Throwable { doReturn(mockBatch).when(target).add("QUERY"); - Object result = this.callbackHandlerSupport.proceedExecution(addMethod, target, args, listener, connectionInfo, null, null); + Object result = this.callbackHandlerSupport.proceedExecution(addMethod, target, args, listener, connectionInfo, null); // verify method on target is invoked verify(target).add("QUERY"); @@ -579,7 +582,7 @@ void proceedExecutionWithNonPublisherThrowsException() throws Throwable { when(target.add("QUERY")).thenThrow(exception); assertThatThrownBy(() -> { - this.callbackHandlerSupport.proceedExecution(addMethod, target, args, listener, connectionInfo, null, null); + this.callbackHandlerSupport.proceedExecution(addMethod, target, args, listener, connectionInfo, null); }).isInstanceOf(RuntimeException.class); verify(target).add("QUERY"); @@ -633,19 +636,19 @@ public String toString() { Object result; // verify toString() - result = this.callbackHandlerSupport.proceedExecution(toStringMethod, target, null, listener, null, null, null); + result = this.callbackHandlerSupport.proceedExecution(toStringMethod, target, null, listener, null, null); assertThat(result).isEqualTo("MyStub-proxy [FOO]"); // verify hashCode() - result = this.callbackHandlerSupport.proceedExecution(hashCodeMethod, target, null, listener, null, null, null); + result = this.callbackHandlerSupport.proceedExecution(hashCodeMethod, target, null, listener, null, null); assertThat(result).isEqualTo(target.hashCode()); // verify equals() with null - result = this.callbackHandlerSupport.proceedExecution(equalsMethod, target, new Object[]{null}, listener, null, null, null); + result = this.callbackHandlerSupport.proceedExecution(equalsMethod, target, new Object[]{null}, listener, null, null); assertThat(result).isEqualTo(false); // verify equals() with target - result = this.callbackHandlerSupport.proceedExecution(equalsMethod, target, new Object[]{target}, listener, null, null, null); + result = this.callbackHandlerSupport.proceedExecution(equalsMethod, target, new Object[]{target}, listener, null, null); assertThat(result).isEqualTo(true); } @@ -667,7 +670,7 @@ void methodInvocationStrategy() throws Throwable { return resultMock; }); - Object result = this.callbackHandlerSupport.proceedExecution(addMethod, target, args, listener, connectionInfo, null, null); + Object result = this.callbackHandlerSupport.proceedExecution(addMethod, target, args, listener, connectionInfo, null); assertThat(result).isSameAs(resultMock); From 19fd6ad22e0a136b099cc66b666ea8973390f419 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Thu, 24 Sep 2020 17:09:04 -0700 Subject: [PATCH 28/74] Use custom subscribers only Previously, custom operators(`[Mono|Flux]MethodInvocation`, etc) were simply delegating the calls to custom subscribers that perform callback logic. In this commit, instead of custom operators, just use custom subscribers and use `Operations#liftPublisher` and `transform()` methods to apply them. [issue #70] --- .../callback/CallbackHandlerSupport.java | 24 ++- .../ConnectionFactoryCallbackHandler.java | 17 +- ...ctoryCreateMethodInvocationSubscriber.java | 44 +++++ .../proxy/callback/FluxMethodInvocation.java | 54 ------ .../proxy/callback/FluxQueryInvocation.java | 150 ----------------- .../callback/MethodInvocationSubscriber.java | 34 +++- .../proxy/callback/MonoMethodInvocation.java | 54 ------ ...thodInvocationConnectionFactoryCreate.java | 63 ------- .../callback/QueryInvocationSubscriber.java | 157 ++++++++++++++++++ 9 files changed, 257 insertions(+), 340 deletions(-) create mode 100644 src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCreateMethodInvocationSubscriber.java delete mode 100644 src/main/java/io/r2dbc/proxy/callback/FluxMethodInvocation.java delete mode 100644 src/main/java/io/r2dbc/proxy/callback/FluxQueryInvocation.java delete mode 100644 src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocation.java delete mode 100644 src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocationConnectionFactoryCreate.java create mode 100644 src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java diff --git a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java index fb6746cd..cd96133a 100644 --- a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java +++ b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java @@ -26,6 +26,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; import reactor.util.annotation.Nullable; import java.lang.reflect.InvocationTargetException; @@ -36,6 +37,7 @@ import java.util.Arrays; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; import static java.util.stream.Collectors.toSet; @@ -185,10 +187,13 @@ protected Object proceedExecution(Method method, Object target, @Nullable Object if (Publisher.class.isAssignableFrom(returnType)) { Publisher result = (Publisher) this.methodInvocationStrategy.invoke(method, target, args); + Function, ? extends Publisher> transformer = + Operators.liftPublisher((publisher, subscriber) -> + new MethodInvocationSubscriber(subscriber, executionInfo, proxyConfig, onComplete)); if (result instanceof Mono) { - return new MonoMethodInvocation((Mono) result, executionInfo, proxyConfig, onComplete); + return ((Mono) result).cast(Object.class).transform(transformer); } else { - return new FluxMethodInvocation(Flux.from(result), executionInfo, proxyConfig, onComplete); + return Flux.from(result).cast(Object.class).transform(transformer); } } else { // for method that generates non-publisher, execution happens when it is invoked. @@ -236,13 +241,14 @@ protected Flux interceptQueryExecution(Publisher flux = new FluxQueryInvocation(Flux.from(publisher), executionInfo, this.proxyConfig) - // return a publisher that returns proxy Result - .flatMap(queryResult -> Mono.just(proxyFactory.wrapResult(queryResult, executionInfo))); - - return flux; - + Function, ? extends Publisher> transformer = + Operators.liftPublisher((pub, subscriber) -> + new QueryInvocationSubscriber(subscriber, executionInfo, proxyConfig)); + + return Flux.from(publisher) + .cast(Result.class) + .transform(transformer) + .map(queryResult -> proxyFactory.wrapResult(queryResult, executionInfo)); } /** diff --git a/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java b/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java index 7ae10f43..bff0f302 100644 --- a/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java +++ b/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java @@ -22,6 +22,7 @@ import io.r2dbc.spi.ConnectionFactory; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; import java.lang.reflect.Method; @@ -64,17 +65,19 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl Publisher result = (Publisher) this.methodInvocationStrategy.invoke(method, target, args); - // gh-68: Use special operator dedicated to "ConnectionFactory#create" method. - // Normally, method that returns a Publisher uses "proceedExecution(...)" from parent class. This method returns - // a "[Mono|Flux]MethodInvocation" that have logic to performs before/after method callbacks. + // gh-68: Use special subscriber/subscription dedicated to "ConnectionFactory#create" method. + // Normally, method that returns a Publisher uses "proceedExecution(...)" from parent class which uses + // "MethodInvocationSubscriber" to perform before/after callback logic. // However, when "ConnectionFactory#create" is used with "usingWhen", // (e.g.: "Mono.usingWhen(connectionFactory.create(), resourceClosure, ...)"), the calling order becomes // ["before-method", actual "create", "resource-closure", "after-method"]. // Instead, we want ["before-method", actual "create", *"after-method"*, "resource-closure"] - // Therefore, here uses special mono operator that does not invoke "afterMethod" in "onComplete". - // Then, use "doOnSuccess()" to call "afterMethod" callback. This way, "after-method" is performed - // before "resource-closure" - return new MonoMethodInvocationConnectionFactoryCreate(Mono.from(result), executionInfo, proxyConfig) + // Therefore, here uses special subscriber that does not invoke "afterMethod" in "onComplete". + // Then, instead, use "doOnSuccess()" chained to this mono to call the "afterMethod" callback. + // This way, "after-method" is performed before "resource-closure" + return Mono.from(result) + .transform(Operators.liftPublisher((publisher, subscriber) -> + new ConnectionFactoryCreateMethodInvocationSubscriber(subscriber, executionInfo, proxyConfig))) .map(resultObj -> { // set produced object as result executionInfo.setResult(resultObj); diff --git a/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCreateMethodInvocationSubscriber.java b/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCreateMethodInvocationSubscriber.java new file mode 100644 index 00000000..c49c1022 --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCreateMethodInvocationSubscriber.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.callback; + +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.CoreSubscriber; + +/** + * Special subscriber for {@link ConnectionFactory#create()} to invoke before/after + * method callbacks. + * + * @author Tadaya Tsuyukubo + * @see MethodInvocationSubscriber + * @see ConnectionFactoryCallbackHandler + */ +class ConnectionFactoryCreateMethodInvocationSubscriber extends MethodInvocationSubscriber { + + public ConnectionFactoryCreateMethodInvocationSubscriber(CoreSubscriber delegate, MutableMethodExecutionInfo executionInfo, ProxyConfig proxyConfig) { + super(delegate, executionInfo, proxyConfig, null); + } + + @Override + public void onComplete() { + // "doOnSuccess()" chained to this operator calls "afterMethod()" callback. + // Therefore, do not call "afterMethod()" on "onComplete()" here. + // see "ConnectionFactoryCallbackHandler" and how it handles "create" method. + this.delegate.onComplete(); + } + +} diff --git a/src/main/java/io/r2dbc/proxy/callback/FluxMethodInvocation.java b/src/main/java/io/r2dbc/proxy/callback/FluxMethodInvocation.java deleted file mode 100644 index dcab648e..00000000 --- a/src/main/java/io/r2dbc/proxy/callback/FluxMethodInvocation.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.r2dbc.proxy.callback; - -import io.r2dbc.proxy.core.MethodExecutionInfo; -import reactor.core.CoreSubscriber; -import reactor.core.publisher.Flux; -import reactor.core.publisher.FluxOperator; -import reactor.util.annotation.Nullable; - -import java.util.function.Consumer; - -/** - * A flux operator that calls before/after method callbacks. - * - * @author Tadaya Tsuyukubo - * @see MethodInvocationSubscriber - */ -class FluxMethodInvocation extends FluxOperator { - - private final MutableMethodExecutionInfo executionInfo; - - private final ProxyConfig proxyConfig; - - @Nullable - private final Consumer onComplete; - - public FluxMethodInvocation(Flux source, MutableMethodExecutionInfo executionInfo, ProxyConfig proxyConfig, @Nullable Consumer onComplete) { - super(source); - this.executionInfo = executionInfo; - this.proxyConfig = proxyConfig; - this.onComplete = onComplete; - } - - @Override - public void subscribe(CoreSubscriber actual) { - this.source.subscribe(new MethodInvocationSubscriber(actual, this.executionInfo, this.proxyConfig, this.onComplete)); - } - -} diff --git a/src/main/java/io/r2dbc/proxy/callback/FluxQueryInvocation.java b/src/main/java/io/r2dbc/proxy/callback/FluxQueryInvocation.java deleted file mode 100644 index 2347f17f..00000000 --- a/src/main/java/io/r2dbc/proxy/callback/FluxQueryInvocation.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.r2dbc.proxy.callback; - -import io.r2dbc.proxy.core.ProxyEventType; -import io.r2dbc.proxy.listener.ProxyExecutionListener; -import io.r2dbc.spi.Batch; -import io.r2dbc.spi.Result; -import io.r2dbc.spi.Statement; -import org.reactivestreams.Subscription; -import reactor.core.CoreSubscriber; -import reactor.core.Scannable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.FluxOperator; -import reactor.util.annotation.Nullable; - -/** - * Call before/after query callbacks by wrapping the result of {@link Statement#execute()} or - * {@link Batch#execute()} operations. - * - * @author Tadaya Tsuyukubo - */ -public class FluxQueryInvocation extends FluxOperator { - - private final MutableQueryExecutionInfo executionInfo; - - private final ProxyConfig proxyConfig; - - public FluxQueryInvocation(Flux source, MutableQueryExecutionInfo executionInfo, ProxyConfig proxyConfig) { - super(source); - this.executionInfo = executionInfo; - this.proxyConfig = proxyConfig; - } - - @Override - public void subscribe(CoreSubscriber actual) { - this.source.subscribe(new QueryInvocationSubscriber(actual, this.executionInfo, this.proxyConfig)); - } - - static class QueryInvocationSubscriber implements CoreSubscriber, Subscription, Scannable { - - private final CoreSubscriber delegate; - - private final MutableQueryExecutionInfo executionInfo; - - private final ProxyExecutionListener listener; - - private final CallbackHandlerSupport.StopWatch stopWatch; - - private Subscription subscription; - - public QueryInvocationSubscriber(CoreSubscriber delegate, MutableQueryExecutionInfo executionInfo, ProxyConfig proxyConfig) { - this.delegate = delegate; - this.executionInfo = executionInfo; - this.listener = proxyConfig.getListeners(); - this.stopWatch = new CallbackHandlerSupport.StopWatch(proxyConfig.getClock()); - } - - @Override - public void onSubscribe(Subscription s) { - this.subscription = s; - beforeQuery(); - this.delegate.onSubscribe(this); - } - - @Override - public void onNext(Result result) { - // When at least one element is emitted, consider query execution is success, even when - // the publisher is canceled. see https://github.com/r2dbc/r2dbc-proxy/issues/55 - this.executionInfo.setSuccess(true); - this.delegate.onNext(result); - } - - @Override - public void onError(Throwable t) { - this.executionInfo.setThrowable(t); - this.executionInfo.setSuccess(false); - afterQuery(); - this.delegate.onError(t); - } - - @Override - public void onComplete() { - this.executionInfo.setSuccess(true); - afterQuery(); - this.delegate.onComplete(); - } - - @Override - public void request(long n) { - this.subscription.request(n); - } - - @Override - public void cancel() { - // do not determine success/failure by cancel - afterQuery(); - this.subscription.cancel(); - } - - @Override - @Nullable - @SuppressWarnings("rawtypes") - public Object scanUnsafe(Attr key) { - if (key == Attr.ACTUAL) { - return this.delegate; - } - if (key == Attr.PARENT) { - return this.subscription; - } - return null; - } - - private void beforeQuery() { - this.executionInfo.setThreadName(Thread.currentThread().getName()); - this.executionInfo.setThreadId(Thread.currentThread().getId()); - this.executionInfo.setCurrentMappedResult(null); - this.executionInfo.setProxyEventType(ProxyEventType.BEFORE_QUERY); - - this.stopWatch.start(); - - this.listener.beforeQuery(this.executionInfo); - } - - private void afterQuery() { - this.executionInfo.setExecuteDuration(this.stopWatch.getElapsedDuration()); - this.executionInfo.setThreadName(Thread.currentThread().getName()); - this.executionInfo.setThreadId(Thread.currentThread().getId()); - this.executionInfo.setCurrentMappedResult(null); - this.executionInfo.setProxyEventType(ProxyEventType.AFTER_QUERY); - - this.listener.afterQuery(this.executionInfo); - } - } - -} diff --git a/src/main/java/io/r2dbc/proxy/callback/MethodInvocationSubscriber.java b/src/main/java/io/r2dbc/proxy/callback/MethodInvocationSubscriber.java index 45e84a20..359ea6cb 100644 --- a/src/main/java/io/r2dbc/proxy/callback/MethodInvocationSubscriber.java +++ b/src/main/java/io/r2dbc/proxy/callback/MethodInvocationSubscriber.java @@ -21,6 +21,7 @@ import io.r2dbc.proxy.listener.ProxyExecutionListener; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; import reactor.core.Scannable; import reactor.util.annotation.Nullable; @@ -29,11 +30,12 @@ /** * Custom subscriber/subscription to invoke method callback. * + * This also implements {@link Subscription} to handle cancel case, and + * {@link Fuseable.QueueSubscription} to disable fusion. + * * @author Tadaya Tsuyukubo - * @see MonoMethodInvocation - * @see FluxMethodInvocation */ -class MethodInvocationSubscriber implements CoreSubscriber, Subscription, Scannable { +class MethodInvocationSubscriber implements CoreSubscriber, Subscription, Scannable, Fuseable.QueueSubscription { protected final CoreSubscriber delegate; @@ -109,6 +111,32 @@ public Object scanUnsafe(Scannable.Attr key) { return null; } + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Nullable + @Override + public Object poll() { + return null; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + + } + private void beforeMethod() { this.executionInfo.setThreadName(Thread.currentThread().getName()); this.executionInfo.setThreadId(Thread.currentThread().getId()); diff --git a/src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocation.java b/src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocation.java deleted file mode 100644 index 37a25def..00000000 --- a/src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocation.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.r2dbc.proxy.callback; - -import io.r2dbc.proxy.core.MethodExecutionInfo; -import reactor.core.CoreSubscriber; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoOperator; -import reactor.util.annotation.Nullable; - -import java.util.function.Consumer; - -/** - * A mono operator that calls before/after method callbacks. - * - * @author Tadaya Tsuyukubo - * @see MethodInvocationSubscriber - */ -class MonoMethodInvocation extends MonoOperator { - - private final MutableMethodExecutionInfo executionInfo; - - private final ProxyConfig proxyConfig; - - @Nullable - private final Consumer onComplete; - - public MonoMethodInvocation(Mono source, MutableMethodExecutionInfo executionInfo, ProxyConfig proxyConfig, @Nullable Consumer onComplete) { - super(source); - this.executionInfo = executionInfo; - this.proxyConfig = proxyConfig; - this.onComplete = onComplete; - } - - @Override - public void subscribe(CoreSubscriber actual) { - this.source.subscribe(new MethodInvocationSubscriber(actual, this.executionInfo, this.proxyConfig, this.onComplete)); - } - -} diff --git a/src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocationConnectionFactoryCreate.java b/src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocationConnectionFactoryCreate.java deleted file mode 100644 index 33e83a91..00000000 --- a/src/main/java/io/r2dbc/proxy/callback/MonoMethodInvocationConnectionFactoryCreate.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.r2dbc.proxy.callback; - -import io.r2dbc.spi.ConnectionFactory; -import reactor.core.CoreSubscriber; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoOperator; - -/** - * Special Mono operator for {@link ConnectionFactory#create()} to invoke before/after - * method callbacks. - * - * @author Tadaya Tsuyukubo - */ -class MonoMethodInvocationConnectionFactoryCreate extends MonoOperator { - - private final MutableMethodExecutionInfo executionInfo; - - private final ProxyConfig proxyConfig; - - public MonoMethodInvocationConnectionFactoryCreate(Mono source, MutableMethodExecutionInfo executionInfo, ProxyConfig proxyConfig) { - super(source); - this.executionInfo = executionInfo; - this.proxyConfig = proxyConfig; - } - - @Override - public void subscribe(CoreSubscriber actual) { - this.source.subscribe(new MonoMethodInvocationConnectionFactoryCreateSubscriber(actual, this.executionInfo, this.proxyConfig)); - } - - static class MonoMethodInvocationConnectionFactoryCreateSubscriber extends MethodInvocationSubscriber { - - public MonoMethodInvocationConnectionFactoryCreateSubscriber(CoreSubscriber delegate, MutableMethodExecutionInfo executionInfo, ProxyConfig proxyConfig) { - super(delegate, executionInfo, proxyConfig, null); - } - - @Override - public void onComplete() { - // "doOnSuccess()" chained to this operator calls "afterMethod()" callback. - // Therefore, do not call "afterMethod" on onComplete(). - // see "ConnectionFactoryCallbackHandler" and how it handles "create" method. - this.delegate.onComplete(); - } - - } - -} diff --git a/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java b/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java new file mode 100644 index 00000000..af5c1c8c --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java @@ -0,0 +1,157 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.callback; + +import io.r2dbc.proxy.core.ProxyEventType; +import io.r2dbc.proxy.listener.ProxyExecutionListener; +import io.r2dbc.spi.Batch; +import io.r2dbc.spi.Result; +import io.r2dbc.spi.Statement; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.core.Scannable; +import reactor.util.annotation.Nullable; + +/** + * Custom subscriber/subscription to invoke query callback. + * + * @author Tadaya Tsuyukubo + * @see CallbackHandlerSupport#interceptQueryExecution(Publisher, MutableQueryExecutionInfo) + */ +class QueryInvocationSubscriber implements CoreSubscriber, Subscription, Scannable, Fuseable.QueueSubscription { + + private final CoreSubscriber delegate; + + private final MutableQueryExecutionInfo executionInfo; + + private final ProxyExecutionListener listener; + + private final CallbackHandlerSupport.StopWatch stopWatch; + + private Subscription subscription; + + public QueryInvocationSubscriber(CoreSubscriber delegate, MutableQueryExecutionInfo executionInfo, ProxyConfig proxyConfig) { + this.delegate = delegate; + this.executionInfo = executionInfo; + this.listener = proxyConfig.getListeners(); + this.stopWatch = new CallbackHandlerSupport.StopWatch(proxyConfig.getClock()); + } + + @Override + public void onSubscribe(Subscription s) { + this.subscription = s; + beforeQuery(); + this.delegate.onSubscribe(this); + } + + @Override + public void onNext(Result result) { + // When at least one element is emitted, consider query execution is success, even when + // the publisher is canceled. see https://github.com/r2dbc/r2dbc-proxy/issues/55 + this.executionInfo.setSuccess(true); + this.delegate.onNext(result); + } + + @Override + public void onError(Throwable t) { + this.executionInfo.setThrowable(t); + this.executionInfo.setSuccess(false); + afterQuery(); + this.delegate.onError(t); + } + + @Override + public void onComplete() { + this.executionInfo.setSuccess(true); + afterQuery(); + this.delegate.onComplete(); + } + + @Override + public void request(long n) { + this.subscription.request(n); + } + + @Override + public void cancel() { + // do not determine success/failure by cancel + afterQuery(); + this.subscription.cancel(); + } + + @Override + @Nullable + @SuppressWarnings("rawtypes") + public Object scanUnsafe(Attr key) { + if (key == Attr.ACTUAL) { + return this.delegate; + } + if (key == Attr.PARENT) { + return this.subscription; + } + return null; + } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public Result poll() { + return null; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + + } + + private void beforeQuery() { + this.executionInfo.setThreadName(Thread.currentThread().getName()); + this.executionInfo.setThreadId(Thread.currentThread().getId()); + this.executionInfo.setCurrentMappedResult(null); + this.executionInfo.setProxyEventType(ProxyEventType.BEFORE_QUERY); + + this.stopWatch.start(); + + this.listener.beforeQuery(this.executionInfo); + } + + private void afterQuery() { + this.executionInfo.setExecuteDuration(this.stopWatch.getElapsedDuration()); + this.executionInfo.setThreadName(Thread.currentThread().getName()); + this.executionInfo.setThreadId(Thread.currentThread().getId()); + this.executionInfo.setCurrentMappedResult(null); + this.executionInfo.setProxyEventType(ProxyEventType.AFTER_QUERY); + + this.listener.afterQuery(this.executionInfo); + } + +} From d92560f3046ca12983d7b2f72e34e2f18d28e9ea Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Fri, 25 Sep 2020 13:21:00 -0700 Subject: [PATCH 29/74] Extract StopWatch to an independent class --- .../callback/CallbackHandlerSupport.java | 27 ---------- .../callback/MethodInvocationSubscriber.java | 4 +- .../callback/QueryInvocationSubscriber.java | 6 +-- .../io/r2dbc/proxy/callback/StopWatch.java | 53 +++++++++++++++++++ 4 files changed, 57 insertions(+), 33 deletions(-) create mode 100644 src/main/java/io/r2dbc/proxy/callback/StopWatch.java diff --git a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java index cd96133a..348028b8 100644 --- a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java +++ b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java @@ -94,33 +94,6 @@ public interface MethodInvocationStrategy { } } - /** - * Utility class to get duration of executions. - */ - static class StopWatch { - - private final Clock clock; - - @Nullable - private Instant startTime; - - StopWatch(Clock clock) { - this.clock = clock; - } - - public StopWatch start() { - this.startTime = this.clock.instant(); - return this; - } - - public Duration getElapsedDuration() { - if (this.startTime == null) { - return Duration.ZERO; // when stopwatch has not started - } - return Duration.between(this.startTime, this.clock.instant()); - } - } - protected final ProxyConfig proxyConfig; protected MethodInvocationStrategy methodInvocationStrategy = DEFAULT_INVOCATION_STRATEGY; diff --git a/src/main/java/io/r2dbc/proxy/callback/MethodInvocationSubscriber.java b/src/main/java/io/r2dbc/proxy/callback/MethodInvocationSubscriber.java index 359ea6cb..64acee15 100644 --- a/src/main/java/io/r2dbc/proxy/callback/MethodInvocationSubscriber.java +++ b/src/main/java/io/r2dbc/proxy/callback/MethodInvocationSubscriber.java @@ -43,7 +43,7 @@ class MethodInvocationSubscriber implements CoreSubscriber, Subscription protected final ProxyExecutionListener listener; - protected final CallbackHandlerSupport.StopWatch stopWatch; + protected final StopWatch stopWatch; protected Subscription subscription; @@ -54,7 +54,7 @@ public MethodInvocationSubscriber(CoreSubscriber delegate, MutableMethod this.delegate = delegate; this.executionInfo = executionInfo; this.listener = proxyConfig.getListeners(); - this.stopWatch = new CallbackHandlerSupport.StopWatch(proxyConfig.getClock()); + this.stopWatch = new StopWatch(proxyConfig.getClock()); this.onComplete = onComplete; } diff --git a/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java b/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java index af5c1c8c..a947b968 100644 --- a/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java +++ b/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java @@ -18,9 +18,7 @@ import io.r2dbc.proxy.core.ProxyEventType; import io.r2dbc.proxy.listener.ProxyExecutionListener; -import io.r2dbc.spi.Batch; import io.r2dbc.spi.Result; -import io.r2dbc.spi.Statement; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; @@ -42,7 +40,7 @@ class QueryInvocationSubscriber implements CoreSubscriber, Subscription, private final ProxyExecutionListener listener; - private final CallbackHandlerSupport.StopWatch stopWatch; + private final StopWatch stopWatch; private Subscription subscription; @@ -50,7 +48,7 @@ public QueryInvocationSubscriber(CoreSubscriber delegate, Mutabl this.delegate = delegate; this.executionInfo = executionInfo; this.listener = proxyConfig.getListeners(); - this.stopWatch = new CallbackHandlerSupport.StopWatch(proxyConfig.getClock()); + this.stopWatch = new StopWatch(proxyConfig.getClock()); } @Override diff --git a/src/main/java/io/r2dbc/proxy/callback/StopWatch.java b/src/main/java/io/r2dbc/proxy/callback/StopWatch.java new file mode 100644 index 00000000..7c2e7c49 --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/callback/StopWatch.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.callback; + +import reactor.util.annotation.Nullable; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; + +/** + * Utility class to get duration of executions. + * + * @author Tadaya Tsuyukubo + */ +class StopWatch { + + private final Clock clock; + + @Nullable + private Instant startTime; + + StopWatch(Clock clock) { + this.clock = clock; + } + + public StopWatch start() { + this.startTime = this.clock.instant(); + return this; + } + + public Duration getElapsedDuration() { + if (this.startTime == null) { + return Duration.ZERO; // when stopwatch has not started + } + return Duration.between(this.startTime, this.clock.instant()); + } + +} From b118ed9b7dce9abe13c162a421f7bf78acfb2001 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Thu, 1 Oct 2020 14:49:00 -0700 Subject: [PATCH 30/74] Update CHANGELOG for 0.8.3 release --- CHANGELOG | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 5b8a0a08..95a09bbd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,14 @@ R2DBC Proxy Changelog ============================= +0.8.3.RELEASE +------------------ +* Use custom subscribers to manage callback #70 +* Add ProxyConfigHolder #69 +* Fix "ConnectionFactory#create" after-method callback #68 +* Rename master branch to main #67 +* Upgrade to Reactor Dysprosium-SR7 #66 + 0.8.2.RELEASE ------------------ * Upgrade build and test dependencies #65 From b1916d2777737cf2cd9e630f8dfd09c5ee568693 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Thu, 8 Oct 2020 16:49:14 -0700 Subject: [PATCH 31/74] Update javadoc on afterQuery callback [issue #71] --- .../r2dbc/proxy/core/QueryExecutionInfo.java | 3 +++ .../listener/ProxyExecutionListener.java | 27 ++++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/r2dbc/proxy/core/QueryExecutionInfo.java b/src/main/java/io/r2dbc/proxy/core/QueryExecutionInfo.java index 68c0d823..08fe1ebf 100644 --- a/src/main/java/io/r2dbc/proxy/core/QueryExecutionInfo.java +++ b/src/main/java/io/r2dbc/proxy/core/QueryExecutionInfo.java @@ -113,6 +113,9 @@ public interface QueryExecutionInfo { /** * Get the time that took queries to execute. + *

+ * Duration is only populated in appropriate phase. + * (e.g.: {@link ProxyExecutionListener#afterQuery(QueryExecutionInfo)}) * * @return query execution duration */ diff --git a/src/main/java/io/r2dbc/proxy/listener/ProxyExecutionListener.java b/src/main/java/io/r2dbc/proxy/listener/ProxyExecutionListener.java index f82f2888..cdc42046 100644 --- a/src/main/java/io/r2dbc/proxy/listener/ProxyExecutionListener.java +++ b/src/main/java/io/r2dbc/proxy/listener/ProxyExecutionListener.java @@ -47,10 +47,8 @@ default void afterMethod(MethodExecutionInfo executionInfo) { } /** - * Called before execution of query. - * - * Query execution is {@link Batch#execute()} or {@link Statement#execute()}. - * + * Called before executing a query ({@link Batch#execute()} or {@link Statement#execute()}). + *

* Note: this callback is called when the publisher, result of the {@code execute()}, is being * subscribed. Not at the time of {@code execute()} is called, * @@ -60,10 +58,20 @@ default void beforeQuery(QueryExecutionInfo execInfo) { } /** - * Called after execution of query. - * - * Query execution is {@link Batch#execute()} or {@link Statement#execute()}. - * + * Called after executing a query ({@link Batch#execute()} or {@link Statement#execute()}). + *

+ * The callback order is: + *

    + *
  • {@link #beforeQuery(QueryExecutionInfo)} + *
  • {@link #eachQueryResult(QueryExecutionInfo)} for 1st result + *
  • {@link #eachQueryResult(QueryExecutionInfo)} for 2nd result + *
  • ... + *
  • {@link #eachQueryResult(QueryExecutionInfo)} for Nth result + *
  • {@link #afterQuery(QueryExecutionInfo)} + *
+ * {@link QueryExecutionInfo#getExecuteDuration()} is available in this callback and it holds + * the duration since {@link #beforeQuery(QueryExecutionInfo)}. + *

* Note: this callback is called when the publisher, result of the {@code execute()}, is being * subscribed. Not at the time of {@code execute()} is called, * @@ -74,9 +82,10 @@ default void afterQuery(QueryExecutionInfo execInfo) { /** * Called on processing each query {@link io.r2dbc.spi.Result}. - * + *

* While processing query results with {@link io.r2dbc.spi.Result#map(BiFunction)}, this callback * is called per result. + *

* {@link QueryExecutionInfo#getCurrentMappedResult()} contains the mapped result. * * @param execInfo query execution context From 75208170c0b5c01873f71618d6bca837fa3de9fd Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Fri, 9 Oct 2020 13:03:44 -0700 Subject: [PATCH 32/74] Introduce ProxyMethodExecutionListener Previously, `LifeCycleListener` interface defines callbacks for each before/after methods on r2dbc proxy classes. (e.g. `beforeCreateOnConnectionFactory`) However, since this interface is independent from `ProxyExecutionListener`, in spring application, it ks not straightforward to determine order of beans between `ProxyExecutionListener` and `LifeCycleListener`. In this commit, introduces a new interface `ProxyMethodExecutionListener` which is a child interface of `ProxyExecutionListener`. Then, `ProxyMethodExecutionListenerAdapter` provides invoking corresponding method as a shape of `ProxyExecutionListener`. --- .../io/r2dbc/proxy/callback/ProxyConfig.java | 4 +- .../CompositeProxyExecutionListener.java | 12 +- .../ProxyMethodExecutionListener.java | 530 ++++++++++++++++++ .../ProxyMethodExecutionListenerAdapter.java | 279 +++++++++ .../r2dbc/proxy/callback/ProxyConfigTest.java | 46 +- .../CompositeProxyExecutionListenerTest.java | 45 +- ...oxyMethodExecutionListenerAdapterTest.java | 188 +++++++ .../ProxyMethodExecutionListenerTest.java | 56 ++ 8 files changed, 1146 insertions(+), 14 deletions(-) create mode 100644 src/main/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListener.java create mode 100644 src/main/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListenerAdapter.java create mode 100644 src/test/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListenerAdapterTest.java create mode 100644 src/test/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListenerTest.java diff --git a/src/main/java/io/r2dbc/proxy/callback/ProxyConfig.java b/src/main/java/io/r2dbc/proxy/callback/ProxyConfig.java index 4bb62728..5a60a9d5 100644 --- a/src/main/java/io/r2dbc/proxy/callback/ProxyConfig.java +++ b/src/main/java/io/r2dbc/proxy/callback/ProxyConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -112,7 +112,7 @@ public CompositeProxyExecutionListener getListeners() { /** * Register {@link ProxyExecutionListener}. * - * @param listener listner to register + * @param listener a listener to register * @throws IllegalArgumentException if {@code proxyFactoryFactory} is {@code null} */ public void addListener(ProxyExecutionListener listener) { diff --git a/src/main/java/io/r2dbc/proxy/listener/CompositeProxyExecutionListener.java b/src/main/java/io/r2dbc/proxy/listener/CompositeProxyExecutionListener.java index efe02d02..ee418b9f 100644 --- a/src/main/java/io/r2dbc/proxy/listener/CompositeProxyExecutionListener.java +++ b/src/main/java/io/r2dbc/proxy/listener/CompositeProxyExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,6 +73,9 @@ public void eachQueryResult(QueryExecutionInfo execInfo) { public boolean add(ProxyExecutionListener listener) { Assert.requireNonNull(listener, "listener must not be null"); + if (listener instanceof ProxyMethodExecutionListener) { + return this.listeners.add(new ProxyMethodExecutionListenerAdapter((ProxyMethodExecutionListener) listener)); + } return this.listeners.add(listener); } @@ -85,8 +88,11 @@ public boolean add(ProxyExecutionListener listener) { */ public boolean addAll(Collection listeners) { Assert.requireNonNull(listeners, "listeners must not be null"); - - return this.listeners.addAll(listeners); + boolean result = false; + for (ProxyExecutionListener listener : listeners) { + result |= add(listener); // perform "ProxyMethodExecutionListener" conversion + } + return result; } /** diff --git a/src/main/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListener.java b/src/main/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListener.java new file mode 100644 index 00000000..077fcb20 --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListener.java @@ -0,0 +1,530 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.listener; + +import io.r2dbc.proxy.core.MethodExecutionInfo; +import io.r2dbc.proxy.core.QueryExecutionInfo; +import io.r2dbc.spi.Batch; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.IsolationLevel; +import io.r2dbc.spi.Result; +import io.r2dbc.spi.Statement; +import io.r2dbc.spi.ValidationDepth; + +import java.util.function.BiFunction; + +/** + * Listener interface called back when corresponding method on proxy is invoked. + * + * This interface extends {@link ProxyExecutionListener} interface and provides + * explicit before/after callback on each method on proxy. + * + * @author Tadaya Tsuyukubo + * @since 0.8.3 + */ +public interface ProxyMethodExecutionListener extends ProxyExecutionListener { + + // + // for ConnectionFactory + // + + /** + * Callback that is invoked before {@link ConnectionFactory#create()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeCreateOnConnectionFactory(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link ConnectionFactory#create()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterCreateOnConnectionFactory(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link ConnectionFactory#getMetadata()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeGetMetadataOnConnectionFactory(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link ConnectionFactory#getMetadata()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterGetMetadataOnConnectionFactory(MethodExecutionInfo methodExecutionInfo) { + } + + // + // for Connection + // + + /** + * Callback that is invoked before {@link Connection#beginTransaction()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeBeginTransactionOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#beginTransaction()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterBeginTransactionOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Connection#close()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeCloseOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#close()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterCloseOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Connection#commitTransaction()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeCommitTransactionOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#commitTransaction()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterCommitTransactionOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Connection#createBatch()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeCreateBatchOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#createBatch()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterCreateBatchOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Connection#createSavepoint(String)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeCreateSavepointOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#createSavepoint(String)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterCreateSavepointOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Connection#createStatement(String)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeCreateStatementOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#createStatement(String)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterCreateStatementOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Connection#releaseSavepoint(String)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeReleaseSavepointOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#releaseSavepoint(String)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterReleaseSavepointOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Connection#rollbackTransaction()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeRollbackTransactionOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#rollbackTransaction()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterRollbackTransactionOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Connection#rollbackTransactionToSavepoint(String)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeRollbackTransactionToSavepointOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#rollbackTransactionToSavepoint(String)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterRollbackTransactionToSavepointOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Connection#setTransactionIsolationLevel(IsolationLevel)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeSetTransactionIsolationLevelOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#setTransactionIsolationLevel(IsolationLevel)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterSetTransactionIsolationLevelOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Connection#validate(ValidationDepth)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeValidateOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#validate(ValidationDepth)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterValidateOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Connection#isAutoCommit()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeIsAutoCommitOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#isAutoCommit()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterIsAutoCommitOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Connection#getTransactionIsolationLevel()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeGetTransactionIsolationLevelOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#getTransactionIsolationLevel()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterGetTransactionIsolationLevelOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Connection#setAutoCommit(boolean)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeSetAutoCommitOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#setAutoCommit(boolean)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterSetAutoCommitOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Connection#getMetadata()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeGetMetadataOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Connection#getMetadata()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterGetMetadataOnConnection(MethodExecutionInfo methodExecutionInfo) { + } + + // + // for Batch + // + + /** + * Callback that is invoked before {@link Batch#add(String)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeAddOnBatch(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Batch#add(String)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterAddOnBatch(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Batch#execute()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeExecuteOnBatch(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Batch#execute()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterExecuteOnBatch(MethodExecutionInfo methodExecutionInfo) { + } + + // + // for Statement + // + + /** + * Callback that is invoked before {@link Statement#add()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeAddOnStatement(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Statement#add()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterAddOnStatement(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Statement#bind(int, Object)} or {@link Statement#bind(String, Object)} are called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeBindOnStatement(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Statement#bind(int, Object)} or {@link Statement#bind(String, Object)} are called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterBindOnStatement(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Statement#bindNull(int, Class)} or {@link Statement#bindNull(String, Class)} are called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeBindNullOnStatement(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Statement#bindNull(int, Class)} or {@link Statement#bindNull(String, Class)} are called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterBindNullOnStatement(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Statement#execute()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeExecuteOnStatement(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Statement#execute()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterExecuteOnStatement(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Statement#fetchSize(int)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeFetchSizeOnStatement(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Statement#fetchSize(int)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterFetchSizeOnStatement(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Statement#returnGeneratedValues(String...)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeReturnGeneratedValuesOnStatement(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Statement#returnGeneratedValues(String...)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterReturnGeneratedValuesOnStatement(MethodExecutionInfo methodExecutionInfo) { + } + + // + // For Result + // + + /** + * Callback that is invoked before {@link Result#getRowsUpdated()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeGetRowsUpdatedOnResult(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Result#getRowsUpdated()} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterGetRowsUpdatedOnResult(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked before {@link Result#map(BiFunction)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void beforeMapOnResult(MethodExecutionInfo methodExecutionInfo) { + } + + /** + * Callback that is invoked after {@link Result#map(BiFunction)} is called. + * + * @param methodExecutionInfo the current method execution info; never {@code null}. + */ + default void afterMapOnResult(MethodExecutionInfo methodExecutionInfo) { + } + + // + // For query execution + // + + /** + * Query execution callback that is invoked before {@link Batch#execute()} is called. + * + * @param queryExecutionInfo the current query execution info; never {@code null}. + */ + default void beforeExecuteOnBatch(QueryExecutionInfo queryExecutionInfo) { + } + + /** + * Query execution callback that is invoked after {@link Batch#execute()} is called. + * + * @param queryExecutionInfo the current query execution info; never {@code null}. + */ + default void afterExecuteOnBatch(QueryExecutionInfo queryExecutionInfo) { + } + + /** + * Query execution callback that is invoked before {@link Statement#execute()} is called. + * + * @param queryExecutionInfo the current query execution info; never {@code null}. + */ + default void beforeExecuteOnStatement(QueryExecutionInfo queryExecutionInfo) { + } + + /** + * Query execution callback that is invoked after {@link Statement#execute()} is called. + * + * @param queryExecutionInfo the current query execution info; never {@code null}. + */ + default void afterExecuteOnStatement(QueryExecutionInfo queryExecutionInfo) { + } + +} diff --git a/src/main/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListenerAdapter.java b/src/main/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListenerAdapter.java new file mode 100644 index 00000000..9882fc29 --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListenerAdapter.java @@ -0,0 +1,279 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.listener; + +import io.r2dbc.proxy.core.ExecutionType; +import io.r2dbc.proxy.core.MethodExecutionInfo; +import io.r2dbc.proxy.core.QueryExecutionInfo; +import io.r2dbc.spi.Batch; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Result; +import io.r2dbc.spi.Statement; + +import java.lang.reflect.Method; + +/** + * Adapter to make {@link ProxyMethodExecutionListener} work as {@link ProxyExecutionListener}. + * + * @author Tadaya Tsuyukubo + * @since 0.8.3 + */ +public class ProxyMethodExecutionListenerAdapter implements ProxyExecutionListener { + + private final ProxyMethodExecutionListener delegate; + + public ProxyMethodExecutionListenerAdapter(ProxyMethodExecutionListener delegate) { + this.delegate = delegate; + } + + @Override + public void beforeMethod(MethodExecutionInfo executionInfo) { + this.delegate.beforeMethod(executionInfo); + invokeMethodCallback(executionInfo, true); + } + + @Override + public void afterMethod(MethodExecutionInfo executionInfo) { + invokeMethodCallback(executionInfo, false); + this.delegate.afterMethod(executionInfo); + } + + @Override + public void beforeQuery(QueryExecutionInfo execInfo) { + this.delegate.beforeQuery(execInfo); + invokeQueryCallback(execInfo, true); + } + + @Override + public void afterQuery(QueryExecutionInfo execInfo) { + invokeQueryCallback(execInfo, false); + this.delegate.afterQuery(execInfo); + } + + @Override + public void eachQueryResult(QueryExecutionInfo execInfo) { + this.delegate.eachQueryResult(execInfo); + } + + private void invokeMethodCallback(MethodExecutionInfo executionInfo, boolean isBefore) { + Method method = executionInfo.getMethod(); + String methodName = method.getName(); + Class methodDeclaringClass = method.getDeclaringClass(); + + if (ConnectionFactory.class.isAssignableFrom(methodDeclaringClass)) { + // ConnectionFactory methods + if ("create".equals(methodName)) { + if (isBefore) { + this.delegate.beforeCreateOnConnectionFactory(executionInfo); + } else { + this.delegate.afterCreateOnConnectionFactory(executionInfo); + } + } else if ("getMetadata".equals(methodName)) { + if (isBefore) { + this.delegate.beforeGetMetadataOnConnectionFactory(executionInfo); + } else { + this.delegate.afterGetMetadataOnConnectionFactory(executionInfo); + } + } + } else if (Connection.class.isAssignableFrom(methodDeclaringClass)) { + // Connection methods + if ("beginTransaction".equals(methodName)) { + if (isBefore) { + this.delegate.beforeBeginTransactionOnConnection(executionInfo); + } else { + this.delegate.afterBeginTransactionOnConnection(executionInfo); + } + } else if ("close".equals(methodName)) { + if (isBefore) { + this.delegate.beforeCloseOnConnection(executionInfo); + } else { + this.delegate.afterCloseOnConnection(executionInfo); + } + } else if ("commitTransaction".equals(methodName)) { + if (isBefore) { + this.delegate.beforeCommitTransactionOnConnection(executionInfo); + } else { + this.delegate.afterCommitTransactionOnConnection(executionInfo); + } + } else if ("createBatch".equals(methodName)) { + if (isBefore) { + this.delegate.beforeCreateBatchOnConnection(executionInfo); + } else { + this.delegate.afterCreateBatchOnConnection(executionInfo); + } + } else if ("createSavepoint".equals(methodName)) { + if (isBefore) { + this.delegate.beforeCreateSavepointOnConnection(executionInfo); + } else { + this.delegate.afterCreateSavepointOnConnection(executionInfo); + } + } else if ("createStatement".equals(methodName)) { + if (isBefore) { + this.delegate.beforeCreateStatementOnConnection(executionInfo); + } else { + this.delegate.afterCreateStatementOnConnection(executionInfo); + } + } else if ("releaseSavepoint".equals(methodName)) { + if (isBefore) { + this.delegate.beforeReleaseSavepointOnConnection(executionInfo); + } else { + this.delegate.afterReleaseSavepointOnConnection(executionInfo); + } + } else if ("rollbackTransaction".equals(methodName)) { + if (isBefore) { + this.delegate.beforeRollbackTransactionOnConnection(executionInfo); + } else { + this.delegate.afterRollbackTransactionOnConnection(executionInfo); + } + } else if ("rollbackTransactionToSavepoint".equals(methodName)) { + if (isBefore) { + this.delegate.beforeRollbackTransactionToSavepointOnConnection(executionInfo); + } else { + this.delegate.afterRollbackTransactionToSavepointOnConnection(executionInfo); + } + } else if ("setTransactionIsolationLevel".equals(methodName)) { + if (isBefore) { + this.delegate.beforeSetTransactionIsolationLevelOnConnection(executionInfo); + } else { + this.delegate.afterSetTransactionIsolationLevelOnConnection(executionInfo); + } + } else if ("validate".equals(methodName)) { + if (isBefore) { + this.delegate.beforeValidateOnConnection(executionInfo); + } else { + this.delegate.afterValidateOnConnection(executionInfo); + } + } else if ("isAutoCommit".equals(methodName)) { + if (isBefore) { + this.delegate.beforeIsAutoCommitOnConnection(executionInfo); + } else { + this.delegate.afterIsAutoCommitOnConnection(executionInfo); + } + } else if ("getTransactionIsolationLevel".equals(methodName)) { + if (isBefore) { + this.delegate.beforeGetTransactionIsolationLevelOnConnection(executionInfo); + } else { + this.delegate.afterGetTransactionIsolationLevelOnConnection(executionInfo); + } + } else if ("setAutoCommit".equals(methodName)) { + if (isBefore) { + this.delegate.beforeSetAutoCommitOnConnection(executionInfo); + } else { + this.delegate.afterSetAutoCommitOnConnection(executionInfo); + } + } else if ("getMetadata".equals(methodName)) { + if (isBefore) { + this.delegate.beforeGetMetadataOnConnection(executionInfo); + } else { + this.delegate.afterGetMetadataOnConnection(executionInfo); + } + } + } else if (Batch.class.isAssignableFrom(methodDeclaringClass)) { + // Batch methods + if ("add".equals(methodName)) { + if (isBefore) { + this.delegate.beforeAddOnBatch(executionInfo); + } else { + this.delegate.afterAddOnBatch(executionInfo); + } + } else if ("execute".equals(methodName)) { + if (isBefore) { + this.delegate.beforeExecuteOnBatch(executionInfo); + } else { + this.delegate.afterExecuteOnBatch(executionInfo); + } + } + } else if (Statement.class.isAssignableFrom(methodDeclaringClass)) { + // Statement methods + if ("add".equals(methodName)) { + if (isBefore) { + this.delegate.beforeAddOnStatement(executionInfo); + } else { + this.delegate.afterAddOnStatement(executionInfo); + } + } else if ("bind".equals(methodName)) { + if (isBefore) { + this.delegate.beforeBindOnStatement(executionInfo); + } else { + this.delegate.afterBindOnStatement(executionInfo); + } + } else if ("bindNull".equals(methodName)) { + if (isBefore) { + this.delegate.beforeBindNullOnStatement(executionInfo); + } else { + this.delegate.afterBindNullOnStatement(executionInfo); + } + } else if ("execute".equals(methodName)) { + if (isBefore) { + this.delegate.beforeExecuteOnStatement(executionInfo); + } else { + this.delegate.afterExecuteOnStatement(executionInfo); + } + } else if ("fetchSize".equals(methodName)) { + if (isBefore) { + this.delegate.beforeFetchSizeOnStatement(executionInfo); + } else { + this.delegate.afterFetchSizeOnStatement(executionInfo); + } + } else if ("returnGeneratedValues".equals(methodName)) { + if (isBefore) { + this.delegate.beforeReturnGeneratedValuesOnStatement(executionInfo); + } else { + this.delegate.afterReturnGeneratedValuesOnStatement(executionInfo); + } + } + } else if (Result.class.isAssignableFrom(methodDeclaringClass)) { + if ("getRowsUpdated".equals(methodName)) { + if (isBefore) { + this.delegate.beforeGetRowsUpdatedOnResult(executionInfo); + } else { + this.delegate.afterGetRowsUpdatedOnResult(executionInfo); + } + } else if ("map".equals(methodName)) { + if (isBefore) { + this.delegate.beforeMapOnResult(executionInfo); + } else { + this.delegate.afterMapOnResult(executionInfo); + } + } + } + } + + private void invokeQueryCallback(QueryExecutionInfo execInfo, boolean isBefore) { + ExecutionType executionType = execInfo.getType(); + + if (executionType == ExecutionType.BATCH) { + if (isBefore) { + this.delegate.beforeExecuteOnBatch(execInfo); + } else { + this.delegate.afterExecuteOnBatch(execInfo); + } + } else { + if (isBefore) { + this.delegate.beforeExecuteOnStatement(execInfo); + } else { + this.delegate.afterExecuteOnStatement(execInfo); + } + } + } + + public ProxyMethodExecutionListener getDelegate() { + return this.delegate; + } + +} diff --git a/src/test/java/io/r2dbc/proxy/callback/ProxyConfigTest.java b/src/test/java/io/r2dbc/proxy/callback/ProxyConfigTest.java index d2d41563..05e28059 100644 --- a/src/test/java/io/r2dbc/proxy/callback/ProxyConfigTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/ProxyConfigTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import io.r2dbc.proxy.listener.LifeCycleListener; import io.r2dbc.proxy.listener.ProxyExecutionListener; +import io.r2dbc.proxy.listener.ProxyMethodExecutionListener; +import io.r2dbc.proxy.listener.ProxyMethodExecutionListenerAdapter; import org.junit.jupiter.api.Test; import java.time.Clock; @@ -52,7 +54,6 @@ void setProxyFactoryFactory() { @Test void builder() { - ConnectionIdManager connectionIdManager = mock(ConnectionIdManager.class); Clock clock = mock(Clock.class); ProxyExecutionListener listener = mock(ProxyExecutionListener.class); @@ -87,4 +88,45 @@ void builderWithDefaultValues() { assertThat(proxyConfig.getProxyFactory()).isNotNull(); } + @Test + void builderWithProxyMethodExecutionListener() { + ProxyMethodExecutionListener methodListener = mock(ProxyMethodExecutionListener.class); + + ProxyConfig proxyConfig = ProxyConfig.builder().listener(methodListener).build(); + assertThat(proxyConfig.getListeners().getListeners()) + .hasSize(1) + .first() + .isInstanceOfSatisfying(ProxyMethodExecutionListenerAdapter.class, listener -> { + assertThat(listener.getDelegate()).isSameAs(methodListener); + }); + } + + @Test + void addListener() { + ProxyConfig proxyConfig = ProxyConfig.builder().build(); + + ProxyExecutionListener listener = mock(ProxyExecutionListener.class); + proxyConfig.addListener(listener); + + assertThat(proxyConfig.getListeners().getListeners()) + .hasSize(1) + .first() + .isSameAs(listener); + } + + @Test + void addListenerWithProxyMethodExecutionListener() { + ProxyConfig proxyConfig = ProxyConfig.builder().build(); + + ProxyMethodExecutionListener methodListener = mock(ProxyMethodExecutionListener.class); + proxyConfig.addListener(methodListener); + + assertThat(proxyConfig.getListeners().getListeners()) + .hasSize(1) + .first() + .isInstanceOfSatisfying(ProxyMethodExecutionListenerAdapter.class, listener -> { + assertThat(listener.getDelegate()).isSameAs(methodListener); + }); + } + } diff --git a/src/test/java/io/r2dbc/proxy/listener/CompositeProxyExecutionListenerTest.java b/src/test/java/io/r2dbc/proxy/listener/CompositeProxyExecutionListenerTest.java index cde62353..f2761c9f 100644 --- a/src/test/java/io/r2dbc/proxy/listener/CompositeProxyExecutionListenerTest.java +++ b/src/test/java/io/r2dbc/proxy/listener/CompositeProxyExecutionListenerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.Arrays; + import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * @author Tadaya Tsuyukubo @@ -47,7 +50,6 @@ void setUp() { @Test void beforeMethod() { - MethodExecutionInfo executionInfo = MockMethodExecutionInfo.builder() .proxyEventType(ProxyEventType.BEFORE_METHOD) .build(); @@ -61,7 +63,6 @@ void beforeMethod() { @Test void afterMethod() { - MethodExecutionInfo executionInfo = MockMethodExecutionInfo.builder() .proxyEventType(ProxyEventType.AFTER_METHOD) .build(); @@ -75,7 +76,6 @@ void afterMethod() { @Test void beforeQuery() { - QueryExecutionInfo executionInfo = MockQueryExecutionInfo.empty(); this.compositeListener.beforeQuery(executionInfo); @@ -86,27 +86,58 @@ void beforeQuery() { @Test void afterQuery() { - QueryExecutionInfo executionInfo = MockQueryExecutionInfo.empty(); this.compositeListener.afterQuery(executionInfo); assertThat(this.listener1.getAfterQueryExecutionInfo()).isSameAs(executionInfo); assertThat(this.listener2.getAfterQueryExecutionInfo()).isSameAs(executionInfo); - } @Test void eachQueryResult() { - QueryExecutionInfo executionInfo = MockQueryExecutionInfo.empty(); this.compositeListener.eachQueryResult(executionInfo); assertThat(this.listener1.getEachQueryResultExecutionInfo()).isSameAs(executionInfo); assertThat(this.listener2.getEachQueryResultExecutionInfo()).isSameAs(executionInfo); + } + + @Test + void add() { + ProxyExecutionListener proxyListener = mock(ProxyExecutionListener.class); + ProxyMethodExecutionListener proxyMethodListener = mock(ProxyMethodExecutionListener.class); + + CompositeProxyExecutionListener composite = new CompositeProxyExecutionListener(); + composite.add(proxyListener); + composite.add(proxyMethodListener); + + assertThat(composite.getListeners()).hasSize(2); + assertThat(composite.getListeners()).first().isSameAs(proxyListener); + + assertThat(composite.getListeners()).element(1) + .isInstanceOfSatisfying(ProxyMethodExecutionListenerAdapter.class, adapter -> { + assertThat(adapter.getDelegate()).isSameAs(proxyMethodListener); + }); } + @Test + void addAll() { + ProxyExecutionListener proxyListener = mock(ProxyExecutionListener.class); + ProxyMethodExecutionListener proxyMethodListener = mock(ProxyMethodExecutionListener.class); + + CompositeProxyExecutionListener composite = new CompositeProxyExecutionListener(); + composite.addAll(Arrays.asList(proxyListener, proxyMethodListener)); + + assertThat(composite.getListeners()).hasSize(2); + assertThat(composite.getListeners()).first().isSameAs(proxyListener); + + assertThat(composite.getListeners()).element(1) + .isInstanceOfSatisfying(ProxyMethodExecutionListenerAdapter.class, adapter -> { + assertThat(adapter.getDelegate()).isSameAs(proxyMethodListener); + }); + } } diff --git a/src/test/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListenerAdapterTest.java b/src/test/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListenerAdapterTest.java new file mode 100644 index 00000000..13b6b587 --- /dev/null +++ b/src/test/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListenerAdapterTest.java @@ -0,0 +1,188 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.listener; + +import io.r2dbc.proxy.core.ExecutionType; +import io.r2dbc.proxy.core.MethodExecutionInfo; +import io.r2dbc.proxy.core.QueryExecutionInfo; +import io.r2dbc.proxy.test.MockMethodExecutionInfo; +import io.r2dbc.proxy.test.MockQueryExecutionInfo; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryMetadata; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.mockito.invocation.Invocation; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Method; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockingDetails; + +/** + * Test for {@link ProxyMethodExecutionListenerAdapter}. + * + * @author Tadaya Tsuyukubo + */ +public class ProxyMethodExecutionListenerAdapterTest { + + /** + * Test to verify invocation on delegated {@link ProxyMethodExecutionListener} by calling + * {@link ProxyMethodExecutionListenerAdapter#beforeMethod(MethodExecutionInfo)} and + * {@link ProxyMethodExecutionListenerAdapter#afterMethod(MethodExecutionInfo)}. + * + * @param clazz class that r2dbc-proxy creates a proxy + */ + @ParameterizedTest + @ProxyClassesSource + void methodInvocations(Class clazz) { + String className = clazz.getSimpleName(); + Method[] declaredMethods = clazz.getDeclaredMethods(); + for (Method methodToInvoke : declaredMethods) { + String methodName = methodToInvoke.getName(); + + // beforeXxxOnYyy : Xxx is a capitalized method-name and Yyy is a capitalized class-name + String expectedBeforeMethodName = "before" + StringUtils.capitalize(methodName) + "On" + StringUtils.capitalize(className); + String expectedAfterMethodName = "after" + StringUtils.capitalize(methodName) + "On" + StringUtils.capitalize(className); + + // mock executing method + MethodExecutionInfo methodExecutionInfo = MockMethodExecutionInfo.builder() + .method(methodToInvoke) + .build(); + + ProxyMethodExecutionListener methodListener = mock(ProxyMethodExecutionListener.class); + ProxyExecutionListener listener = new ProxyMethodExecutionListenerAdapter(methodListener); + + // invoke beforeMethod() + listener.beforeMethod(methodExecutionInfo); + + // ProxyMethodExecutionListenerAdapter#beforeMethod calls its delegate in this order: + // - "beforeMethod" + // - "beforeXxxOnYyy" + assertThat(mockingDetails(methodListener).getInvocations()) + .hasSize(2) + .extracting(Invocation::getMethod) + .extracting(Method::getName) + .containsExactly("beforeMethod", expectedBeforeMethodName); + + // reset + clearInvocations(methodListener); + + listener.afterMethod(methodExecutionInfo); + + // ProxyMethodExecutionListenerAdapter#afterMethod calls its delegate in this order: + // - "afterXxxOnYyy" + // - "afterMethod" + assertThat(mockingDetails(methodListener).getInvocations()) + .hasSize(2) + .extracting(Invocation::getMethod) + .extracting(Method::getName) + .containsExactly(expectedAfterMethodName, "afterMethod"); + } + + } + + @Test + void queryExecution() { + ProxyMethodExecutionListener methodListener = mock(ProxyMethodExecutionListener.class); + ProxyExecutionListener listener = new ProxyMethodExecutionListenerAdapter(methodListener); + + QueryExecutionInfo queryExecutionInfo; + + // for Statement#execute + queryExecutionInfo = MockQueryExecutionInfo.builder() + .type(ExecutionType.STATEMENT) + .build(); + + // test beforeQuery + listener.beforeQuery(queryExecutionInfo); + verifyQueryExecutionInvocation(methodListener, "beforeQuery", "beforeExecuteOnStatement"); + + clearInvocations(methodListener); + + // test afterQuery + listener.afterQuery(queryExecutionInfo); + verifyQueryExecutionInvocation(methodListener, "afterExecuteOnStatement", "afterQuery"); + + clearInvocations(methodListener); + + + // for Batch#execute + queryExecutionInfo = MockQueryExecutionInfo.builder() + .type(ExecutionType.BATCH) + .build(); + + // test beforeQuery + listener.beforeQuery(queryExecutionInfo); + verifyQueryExecutionInvocation(methodListener, "beforeQuery", "beforeExecuteOnBatch"); + + clearInvocations(methodListener); + + // test afterQuery + listener.afterQuery(queryExecutionInfo); + verifyQueryExecutionInvocation(methodListener, "afterExecuteOnBatch", "afterQuery"); + } + + private void verifyQueryExecutionInvocation(ProxyMethodExecutionListener mockListener, String... expectedMethodNames) { + assertThat(mockingDetails(mockListener).getInvocations()) + .hasSize(2) + .extracting(Invocation::getMethod) + .extracting(Method::getName) + .containsExactly(expectedMethodNames); + } + + + @Test + void methodInvocationWithConcreteClass() { + // just declare an abstract class to get Method object + abstract class MyConnectionFactory implements ConnectionFactory { + + @Override + public ConnectionFactoryMetadata getMetadata() { + return null; + } + } + + ProxyMethodExecutionListener methodListener = mock(ProxyMethodExecutionListener.class); + ProxyExecutionListener listener = new ProxyMethodExecutionListenerAdapter(methodListener); + + Method getMetadataMethod = ReflectionUtils.findMethod(MyConnectionFactory.class, "getMetadata"); + + // just make sure that retrieved method is not the one defined on interface + Method getMetadataMethodFromInterface = ReflectionUtils.findMethod(ConnectionFactory.class, "getMetadata"); + assertThat(getMetadataMethod).isNotEqualTo(getMetadataMethodFromInterface); + + MethodExecutionInfo methodExecutionInfo = MockMethodExecutionInfo.builder() + .method(getMetadataMethod) + .build(); + + // test beforeQuery + listener.beforeMethod(methodExecutionInfo); + + assertThat(mockingDetails(methodListener).getInvocations()) + .extracting(Invocation::getMethod) + .extracting(Method::getName) + .contains("beforeGetMetadataOnConnectionFactory"); + } + + + // TODO: add test for onEachQueryResult + +} diff --git a/src/test/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListenerTest.java b/src/test/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListenerTest.java new file mode 100644 index 00000000..78fe5aa6 --- /dev/null +++ b/src/test/java/io/r2dbc/proxy/listener/ProxyMethodExecutionListenerTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.listener; + +import org.junit.jupiter.params.ParameterizedTest; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Method; +import java.util.Set; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toSet; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link ProxyMethodExecutionListener}. + * + * @author Tadaya Tsuyukubo + */ +public class ProxyMethodExecutionListenerTest { + + @ParameterizedTest + @ProxyClassesSource + void verifyMethodNames(Class clazz) { + + String className = clazz.getSimpleName(); + + Set expected = Stream.of(clazz.getDeclaredMethods()) + .map(Method::getName) + .flatMap(methodName -> { + // beforeXxxOnYyy / afterXxxOnYyy + String name = StringUtils.capitalize(methodName) + "On" + StringUtils.capitalize(className); + return Stream.of("before" + name, "after" + name); + }) + .collect(toSet()); + + assertThat(ProxyMethodExecutionListener.class.getDeclaredMethods()) + .extracting(Method::getName) + .containsAll(expected); + } + +} From 7cac99c5acb175d88d64c7f00ab3cb4d76b6f880 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Fri, 9 Oct 2020 13:30:30 -0700 Subject: [PATCH 33/74] Deprecate LifeCycleListener Deprecate `LifeCycleListener` and `LifeCycleExecutionListener` in favor of `ProxyMethodExecutionListener` and `ProxyMethodExecutionListenerAdapter`. --- .../java/io/r2dbc/proxy/ProxyConnectionFactory.java | 11 ++++++----- .../r2dbc/proxy/ProxyConnectionFactoryProvider.java | 6 +++--- .../java/io/r2dbc/proxy/callback/ProxyConfig.java | 11 ++++++----- .../proxy/listener/LifeCycleExecutionListener.java | 7 ++++--- .../io/r2dbc/proxy/listener/LifeCycleListener.java | 2 ++ .../ConnectionFactoryCallbackHandlerTest.java | 4 ++-- .../java/io/r2dbc/proxy/callback/ProxyConfigTest.java | 4 ++-- .../listener/LifeCycleExecutionListenerTest.java | 1 + .../r2dbc/proxy/listener/LifeCycleListenerTest.java | 1 + 9 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/main/java/io/r2dbc/proxy/ProxyConnectionFactory.java b/src/main/java/io/r2dbc/proxy/ProxyConnectionFactory.java index 3c14cc8c..d4db01ae 100644 --- a/src/main/java/io/r2dbc/proxy/ProxyConnectionFactory.java +++ b/src/main/java/io/r2dbc/proxy/ProxyConnectionFactory.java @@ -20,9 +20,8 @@ import io.r2dbc.proxy.callback.ProxyConfig; import io.r2dbc.proxy.core.MethodExecutionInfo; import io.r2dbc.proxy.core.QueryExecutionInfo; -import io.r2dbc.proxy.listener.LifeCycleExecutionListener; -import io.r2dbc.proxy.listener.LifeCycleListener; import io.r2dbc.proxy.listener.ProxyExecutionListener; +import io.r2dbc.proxy.listener.ProxyMethodExecutionListener; import io.r2dbc.proxy.util.Assert; import io.r2dbc.spi.ConnectionFactory; @@ -229,16 +228,18 @@ public Builder listener(ProxyExecutionListener listener) { } /** - * Register a {@link LifeCycleListener}. + * Register a {@link io.r2dbc.proxy.listener.LifeCycleListener}. * * @param lifeCycleListener a listener to register * @return builder * @throws IllegalArgumentException if {@code lifeCycleListener} is {@code null} + * @deprecated Use {@link #listener(ProxyExecutionListener)} with {@link ProxyMethodExecutionListener} */ - public Builder listener(LifeCycleListener lifeCycleListener) { + @Deprecated + public Builder listener(io.r2dbc.proxy.listener.LifeCycleListener lifeCycleListener) { Assert.requireNonNull(lifeCycleListener, "lifeCycleListener must not be null"); - this.listener(LifeCycleExecutionListener.of(lifeCycleListener)); + this.listener(io.r2dbc.proxy.listener.LifeCycleExecutionListener.of(lifeCycleListener)); return this; } diff --git a/src/main/java/io/r2dbc/proxy/ProxyConnectionFactoryProvider.java b/src/main/java/io/r2dbc/proxy/ProxyConnectionFactoryProvider.java index e4b8a433..72a1c134 100644 --- a/src/main/java/io/r2dbc/proxy/ProxyConnectionFactoryProvider.java +++ b/src/main/java/io/r2dbc/proxy/ProxyConnectionFactoryProvider.java @@ -17,7 +17,6 @@ package io.r2dbc.proxy; -import io.r2dbc.proxy.listener.LifeCycleListener; import io.r2dbc.proxy.listener.ProxyExecutionListener; import io.r2dbc.proxy.util.Assert; import io.r2dbc.spi.ConnectionFactories; @@ -119,6 +118,7 @@ public ConnectionFactory create(ConnectionFactoryOptions connectionFactoryOption return builder.build(); } + @SuppressWarnings("deprecation") private void registerProxyListeners(Object optionValue, ProxyConnectionFactory.Builder builder) { if (optionValue instanceof Collection) { ((Collection) optionValue).forEach(element -> registerProxyListeners(element, builder)); @@ -129,8 +129,8 @@ private void registerProxyListeners(Object optionValue, ProxyConnectionFactory.B registerProxyListenerClass((Class) optionValue, builder); } else if (optionValue instanceof ProxyExecutionListener) { builder.listener((ProxyExecutionListener) optionValue); - } else if (optionValue instanceof LifeCycleListener) { - builder.listener((LifeCycleListener) optionValue); + } else if (optionValue instanceof io.r2dbc.proxy.listener.LifeCycleListener) { + builder.listener((io.r2dbc.proxy.listener.LifeCycleListener) optionValue); } else { throw new IllegalArgumentException(optionValue + " is not a proxy listener instance"); } diff --git a/src/main/java/io/r2dbc/proxy/callback/ProxyConfig.java b/src/main/java/io/r2dbc/proxy/callback/ProxyConfig.java index 5a60a9d5..7119b483 100644 --- a/src/main/java/io/r2dbc/proxy/callback/ProxyConfig.java +++ b/src/main/java/io/r2dbc/proxy/callback/ProxyConfig.java @@ -18,9 +18,8 @@ import io.r2dbc.proxy.listener.BindParameterConverter; import io.r2dbc.proxy.listener.CompositeProxyExecutionListener; -import io.r2dbc.proxy.listener.LifeCycleExecutionListener; -import io.r2dbc.proxy.listener.LifeCycleListener; import io.r2dbc.proxy.listener.ProxyExecutionListener; +import io.r2dbc.proxy.listener.ProxyMethodExecutionListener; import io.r2dbc.proxy.util.Assert; import java.time.Clock; @@ -209,16 +208,18 @@ public Builder listener(ProxyExecutionListener listener) { } /** - * Add a {@link LifeCycleListener}. + * Add a {@link io.r2dbc.proxy.listener.LifeCycleListener}. * * @param lifeCycleListener a listener to add * @return builder * @throws IllegalArgumentException if {@code listener} is {@code null} + * @deprecated Use {@link #listener(ProxyExecutionListener)} with {@link ProxyMethodExecutionListener}. */ - public Builder listener(LifeCycleListener lifeCycleListener) { + @Deprecated + public Builder listener(io.r2dbc.proxy.listener.LifeCycleListener lifeCycleListener) { Assert.requireNonNull(lifeCycleListener, "lifeCycleListener must not be null"); - return listener(LifeCycleExecutionListener.of(lifeCycleListener)); + return listener(io.r2dbc.proxy.listener.LifeCycleExecutionListener.of(lifeCycleListener)); } /** diff --git a/src/main/java/io/r2dbc/proxy/listener/LifeCycleExecutionListener.java b/src/main/java/io/r2dbc/proxy/listener/LifeCycleExecutionListener.java index a1720d6c..575285fd 100644 --- a/src/main/java/io/r2dbc/proxy/listener/LifeCycleExecutionListener.java +++ b/src/main/java/io/r2dbc/proxy/listener/LifeCycleExecutionListener.java @@ -16,8 +16,6 @@ package io.r2dbc.proxy.listener; -import java.lang.reflect.Method; - import io.r2dbc.proxy.core.ExecutionType; import io.r2dbc.proxy.core.MethodExecutionInfo; import io.r2dbc.proxy.core.QueryExecutionInfo; @@ -28,12 +26,15 @@ import io.r2dbc.spi.Result; import io.r2dbc.spi.Statement; +import java.lang.reflect.Method; + /** * Provides explicit callbacks on all SPI invocations and query executions on given {@link LifeCycleListener}. * * @author Tadaya Tsuyukubo - * @see LifeCycleListener + * @deprecated Use {@link ProxyMethodExecutionListener} and {@link ProxyMethodExecutionListenerAdapter}. */ +@Deprecated public final class LifeCycleExecutionListener implements ProxyExecutionListener { private final LifeCycleListener delegate; diff --git a/src/main/java/io/r2dbc/proxy/listener/LifeCycleListener.java b/src/main/java/io/r2dbc/proxy/listener/LifeCycleListener.java index a98288bc..62a84224 100644 --- a/src/main/java/io/r2dbc/proxy/listener/LifeCycleListener.java +++ b/src/main/java/io/r2dbc/proxy/listener/LifeCycleListener.java @@ -33,7 +33,9 @@ * * @author Tadaya Tsuyukubo * @see LifeCycleExecutionListener + * @deprecated Use {@link ProxyMethodExecutionListener} */ +@Deprecated public interface LifeCycleListener { // diff --git a/src/test/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandlerTest.java b/src/test/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandlerTest.java index 44aedba4..0d0facf5 100644 --- a/src/test/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandlerTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandlerTest.java @@ -19,7 +19,6 @@ import io.r2dbc.proxy.core.ConnectionInfo; import io.r2dbc.proxy.core.MethodExecutionInfo; import io.r2dbc.proxy.listener.LastExecutionAwareListener; -import io.r2dbc.proxy.listener.LifeCycleListener; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryMetadata; @@ -144,6 +143,7 @@ void unwrap() throws Throwable { // gh-68 @Test + @SuppressWarnings("deprecation") void createConnectionWithUsingWhen() throws Throwable { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); Connection mockedConnection = mock(Connection.class); @@ -152,7 +152,7 @@ void createConnectionWithUsingWhen() throws Throwable { List list = Collections.synchronizedList(new ArrayList<>()); AtomicReference createdConnectionHolder = new AtomicReference<>(); - LifeCycleListener listener = new LifeCycleListener() { + io.r2dbc.proxy.listener.LifeCycleListener listener = new io.r2dbc.proxy.listener.LifeCycleListener() { @Override public void beforeCreateOnConnectionFactory(MethodExecutionInfo methodExecutionInfo) { diff --git a/src/test/java/io/r2dbc/proxy/callback/ProxyConfigTest.java b/src/test/java/io/r2dbc/proxy/callback/ProxyConfigTest.java index 05e28059..e4476960 100644 --- a/src/test/java/io/r2dbc/proxy/callback/ProxyConfigTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/ProxyConfigTest.java @@ -17,7 +17,6 @@ package io.r2dbc.proxy.callback; -import io.r2dbc.proxy.listener.LifeCycleListener; import io.r2dbc.proxy.listener.ProxyExecutionListener; import io.r2dbc.proxy.listener.ProxyMethodExecutionListener; import io.r2dbc.proxy.listener.ProxyMethodExecutionListenerAdapter; @@ -53,11 +52,12 @@ void setProxyFactoryFactory() { } @Test + @SuppressWarnings("deprecation") void builder() { ConnectionIdManager connectionIdManager = mock(ConnectionIdManager.class); Clock clock = mock(Clock.class); ProxyExecutionListener listener = mock(ProxyExecutionListener.class); - LifeCycleListener lifeCycleListener = mock(LifeCycleListener.class); + io.r2dbc.proxy.listener.LifeCycleListener lifeCycleListener = mock(io.r2dbc.proxy.listener.LifeCycleListener.class); ProxyFactory proxyFactory = mock(ProxyFactory.class); ProxyFactoryFactory proxyFactoryFactory = config -> proxyFactory; diff --git a/src/test/java/io/r2dbc/proxy/listener/LifeCycleExecutionListenerTest.java b/src/test/java/io/r2dbc/proxy/listener/LifeCycleExecutionListenerTest.java index 96cc4764..a8563e3c 100644 --- a/src/test/java/io/r2dbc/proxy/listener/LifeCycleExecutionListenerTest.java +++ b/src/test/java/io/r2dbc/proxy/listener/LifeCycleExecutionListenerTest.java @@ -41,6 +41,7 @@ /** * @author Tadaya Tsuyukubo */ +@SuppressWarnings("deprecation") public class LifeCycleExecutionListenerTest { /** diff --git a/src/test/java/io/r2dbc/proxy/listener/LifeCycleListenerTest.java b/src/test/java/io/r2dbc/proxy/listener/LifeCycleListenerTest.java index e601737b..e74dc07b 100644 --- a/src/test/java/io/r2dbc/proxy/listener/LifeCycleListenerTest.java +++ b/src/test/java/io/r2dbc/proxy/listener/LifeCycleListenerTest.java @@ -33,6 +33,7 @@ public class LifeCycleListenerTest { @ParameterizedTest @ProxyClassesSource + @SuppressWarnings("deprecation") void verifyMethodNames(Class clazz) { String className = clazz.getSimpleName(); From 3e57892203cc711c5ff99bc2ed3ad422e3a13b6f Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Fri, 9 Oct 2020 13:58:59 -0700 Subject: [PATCH 34/74] Update README --- README.md | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 069e48da..4a2148c0 100644 --- a/README.md +++ b/README.md @@ -247,9 +247,31 @@ Users can write own logic that performs any actions, such as audit logging, send notifications, calling external system, etc. +## Implementing custom listener + +In order to create a custom listener, simply implement `ProxyExecutionListener` or `ProxyMethodExecutionListener` +interface. + +```java +static class MyListener implements ProxyMethodExecutionListener { + @Override + public void afterCreateOnConnectionFactory(MethodExecutionInfo methodExecutionInfo) { + System.out.println("connection created"); + } +} +``` + +```java +ConnectionFactory proxyConnectionFactory = + ProxyConnectionFactory.builder(connectionFactory) + .listener(new MyListener()) + .build(); +``` + + ## API -Currently, there are two listener interfaces - `ProxyExecutionListener` and `LifeCycleListener`. +Currently, there are two listener interfaces - `ProxyExecutionListener` and `ProxyMethodExecutionListener`. These listeners define callback APIs for method and query executions. Formatters are used for converting execution information object to `String`. @@ -287,17 +309,17 @@ and `afterQuery()`.(Specifically, when returned result-publisher is subscribed.) `eachQueryResult()` is called on each mapped query result when `Result#map()` is subscribed. -### LifeCycleListener +### ProxyMethodExecutionListener -`LifeCycleListener` provides before/after methods for all methods defined on `ConnectionFactory`, -`Connection`, `Batch`, `Statement`, and `Result`, as well as method executions(`beforeMethod`, `afterMethod`), -query executions(`beforeQuery`, `afterQuery`) and result processing(`onEachQueryResult`). -This listener is built on top of method and query interceptor API on `ProxyExecutionListener`. +`ProxyMethodExecutionListener` is an extension of `ProxyExecutionListener`. +In addition to the methods defined in `ProxyExecutionListener`, `ProxyMethodExecutionListener` provides +before/after methods for all methods defined on `ConnectionFactory`, `Connection`, `Batch`, +`Statement`, and `Result`. For example, if you want know the creation of connection and close of it: ```java -public class ConnectionStartToEndListener implements LifeCycleListener { +public class ConnectionStartToEndListener implements ProxyMethodExecutionListener { @Override public void beforeCreateOnConnectionFactory(MethodExecutionInfo methodExecutionInfo) { From fb08b62849a91fab5552b8bf85ee2d30c2c220d4 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Fri, 9 Oct 2020 14:36:08 -0700 Subject: [PATCH 35/74] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 95a09bbd..be1a6056 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ R2DBC Proxy Changelog 0.8.3.RELEASE ------------------ +* Introduce "ProxyMethodExecutionListener" and deprecate "LifeCycleListener" #72 * Use custom subscribers to manage callback #70 * Add ProxyConfigHolder #69 * Fix "ConnectionFactory#create" after-method callback #68 From e269e75d7cc040859457b372f111b53509b58aec Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 15 Oct 2020 16:26:16 +0200 Subject: [PATCH 36/74] Use GitHub actions to deploy to OSS Sonatype/Maven Central [resolves #74] --- .github/workflows/ci.yml | 30 +++ .github/workflows/pullrequests.yml | 25 +++ .github/workflows/release.yml | 29 +++ CI.adoc | 23 -- Jenkinsfile | 102 --------- README.md | 35 ++- ci/build-and-deploy-to-artifactory.sh | 10 - ci/build-and-deploy-to-maven-central.sh | 49 +++-- ci/create-release.sh | 18 -- ci/test.sh | 5 - pom.xml | 269 ++++++------------------ settings.xml | 6 +- 12 files changed, 204 insertions(+), 397 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/pullrequests.yml create mode 100644 .github/workflows/release.yml delete mode 100644 CI.adoc delete mode 100644 Jenkinsfile delete mode 100755 ci/build-and-deploy-to-artifactory.sh delete mode 100755 ci/create-release.sh delete mode 100755 ci/test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..20e3ef2f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Java CI with Maven + +on: + push: + branches: [ main, 0.8.x ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Cache local Maven repository + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Build with Maven + env: + SONATYPE_USER: ${{ secrets.SONATYPE_USER }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + run: ./mvnw -B deploy -P snapshot -s settings.xml diff --git a/.github/workflows/pullrequests.yml b/.github/workflows/pullrequests.yml new file mode 100644 index 00000000..d21d7169 --- /dev/null +++ b/.github/workflows/pullrequests.yml @@ -0,0 +1,25 @@ +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Build Pull request with Maven + +on: [pull_request] + +jobs: + pr-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Cache local Maven repository + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-pr-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven-pr- + - name: Build with Maven + run: ./mvnw -B verify diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..d67df97e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Stage release to Maven Central + +on: + push: + branches: [ release-0.x ] + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Initialize Maven Version + run: ./mvnw -q org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version + - name: GPG Check + run: gpg -k + - name: Release with Maven + env: + SONATYPE_USER: ${{ secrets.SONATYPE_USER }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + GPG_KEY_BASE64: ${{ secrets.GPG_KEY_BASE64 }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: ci/build-and-deploy-to-maven-central.sh diff --git a/CI.adoc b/CI.adoc deleted file mode 100644 index de8cd6be..00000000 --- a/CI.adoc +++ /dev/null @@ -1,23 +0,0 @@ -= Continuous Integration - -== Running CI tasks locally - -Since this pipeline is purely Docker-based, it's easy to: - -* Debug what went wrong on your local machine. -* Test out a a tweak to your test routine before sending it out. -* Experiment against a new image before submitting your pull request. - -All of these use cases are great reasons to essentially run what the CI server does on your local machine. - -IMPORTANT: To do this you must have Docker installed on your machine. - -1. `docker run -it -u 1001:1001 --mount type=bind,source="$(pwd)",target=/r2dbc-h2-github springci/r2dbc-openjdk8-with-gpg:latest /bin/bash` -+ -This will launch the Docker image and mount your source code at `r2dbc-h2-github`. -+ -2. `cd r2dbc-h2-github - -You're all set! Since the container is binding to your source, you can make edits from your IDE and continue to run build jobs. - -NOTE: Docker containers can eat up disk space fast! From time to time, run `docker system prune` to clean out old images. \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 5deb66a6..00000000 --- a/Jenkinsfile +++ /dev/null @@ -1,102 +0,0 @@ -pipeline { - agent none - - triggers { - pollSCM 'H/10 * * * *' - upstream(upstreamProjects: "r2dbc-spi/0.8.x", threshold: hudson.model.Result.SUCCESS) - } - - options { - disableConcurrentBuilds() - buildDiscarder(logRotator(numToKeepStr: '14')) - } - - stages { - stage("test: baseline (jdk8)") { - agent { - docker { - image 'adoptopenjdk/openjdk8:latest' - args '-v $HOME/.m2:/tmp/jenkins-home/.m2' - } - } - options { timeout(time: 30, unit: 'MINUTES') } - steps { - sh 'rm -rf ?' - sh 'PROFILE=none ci/test.sh' - } - } - - stage('Deploy') { - when { - anyOf { - branch '0.8.x' - branch 'release-0.x' - } - not { triggeredBy 'UpstreamCause' } - } - agent { - docker { - image 'springci/r2dbc-openjdk8-with-gpg:latest' - args '-v $HOME/.m2:/tmp/jenkins-home/.m2' - } - } - options { timeout(time: 20, unit: 'MINUTES') } - - environment { - ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c') - SONATYPE = credentials('oss-token') - KEYRING = credentials('spring-signing-secring.gpg') - PASSPHRASE = credentials('spring-gpg-passphrase') - } - - steps { - script { - // Warm up this plugin quietly before using it. - sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -q org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version' - - // Extract project's version number - PROJECT_VERSION = sh( - script: 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version -o | grep -v INFO', - returnStdout: true - ).trim() - - RELEASE_TYPE = 'milestone' // .RC? or .M? - - if (PROJECT_VERSION.endsWith('SNAPSHOT')) { // .SNAPSHOT - RELEASE_TYPE = 'snapshot' - } else if (PROJECT_VERSION.endsWith('RELEASE') || PROJECT_VERSION ==~ /.*SR[0-9]+/) { // .RELEASE or .SR? - RELEASE_TYPE = 'release' - } - - if (RELEASE_TYPE == 'release') { - sh "PROFILE=central ci/build-and-deploy-to-maven-central.sh" - script { - slackSend( - color: 'warning', - channel: '#r2dbc-dev', - message: "WORKING ON: ${currentBuild.fullDisplayName} - `${currentBuild.currentResult}`\n${env.BUILD_URL} staged on Maven Central, awaiting final release.") - } - } else { - sh "PROFILE=${RELEASE_TYPE} ci/build-and-deploy-to-artifactory.sh" - } - } - } - } - } - - post { - changed { - script { - slackSend( - color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', - channel: '#r2dbc-dev', - message: "${currentBuild.fullDisplayName} - `${currentBuild.currentResult}`\n${env.BUILD_URL}") - emailext( - subject: "[${currentBuild.fullDisplayName}] ${currentBuild.currentResult}", - mimeType: 'text/html', - recipientProviders: [[$class: 'CulpritsRecipientProvider'], [$class: 'RequesterRecipientProvider']], - body: "${currentBuild.fullDisplayName} is reported as ${currentBuild.currentResult}") - } - } - } -} diff --git a/README.md b/README.md index 4a2148c0..14b46496 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Reactive Relational Database Connectivity Proxy Framework [![Build Status](https://travis-ci.org/r2dbc/r2dbc-postgresql.svg?branch=master)](https://travis-ci.org/r2dbc/r2dbc-proxy) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.r2dbc/r2dbc-proxy/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.r2dbc/r2dbc-proxy) +# Reactive Relational Database Connectivity Proxy Framework [![Java CI with Maven](https://github.com/r2dbc/r2dbc-proxy/workflows/Java%20CI%20with%20Maven/badge.svg?branch=0.8.x)](https://github.com/r2dbc/r2dbc-proxy/actions?query=workflow%3A%22Java+CI+with+Maven%22+branch%3A0.8.x) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.r2dbc/r2dbc-proxy/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.r2dbc/r2dbc-proxy) This project contains the proxy framework of the [R2DBC SPI][r]. @@ -21,30 +21,21 @@ Artifacts can be found on [Maven Central](https://search.maven.org/search?q=r2db ``` -Artifacts can be found at the following repositories. +If you'd rather like the latest snapshots of the upcoming major version, use our Maven snapshot repository and declare the appropriate dependency version. -### Repositories ```xml - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - true - - -``` + + io.r2dbc + r2dbc-proxy + ${version}.BUILD-SNAPSHOT + -```xml - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - + sonatype-nexus-snapshots + Sonatype OSS Snapshot Repository + https://oss.sonatype.org/content/repositories/snapshots -``` +``` ## Usage Configuration of the `ConnectionFactory` can be accomplished in two ways: @@ -492,6 +483,10 @@ If you want to build with the regular `mvn` command, you will need [Maven v3.5.0 _Also see [CONTRIBUTING.adoc](https://github.com/r2dbc/.github/blob/main/CONTRIBUTING.adoc) if you wish to submit pull requests, and in particular please sign the [Contributor's Agreement](https://cla.pivotal.io/sign/reactor) before your first change, however trivial._ +## Staging to Maven Central + +To stage a release to Maven Central, you need to create a release tag (release version) that contains the desired state and version numbers (`mvn versions:set versions:commit -q -o -DgenerateBackupPoms=false -DnewVersion=x.y.z.(RELEASE|Mnnn|RCnnn`) and force-push it to the `release-0.x` branch. This push will trigger a Maven staging build (see `build-and-deploy-to-maven-central.sh`). + ## License This project is released under version 2.0 of the [Apache License][l]. diff --git a/ci/build-and-deploy-to-artifactory.sh b/ci/build-and-deploy-to-artifactory.sh deleted file mode 100755 index 1c6b6d1e..00000000 --- a/ci/build-and-deploy-to-artifactory.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -# -# Deploy the artifactory -# -echo 'Deploying to Artifactory...' - -MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -P${PROFILE} -Dmaven.test.skip=true clean deploy -B diff --git a/ci/build-and-deploy-to-maven-central.sh b/ci/build-and-deploy-to-maven-central.sh index 6af5b16f..c808d2c6 100755 --- a/ci/build-and-deploy-to-maven-central.sh +++ b/ci/build-and-deploy-to-maven-central.sh @@ -1,22 +1,39 @@ -#!/bin/bash -x +#!/bin/bash set -euo pipefail -# -# Stage on Maven Central -# -echo 'Staging on Maven Central...' +VERSION=$(./mvnw org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version -o | grep -v INFO) -GNUPGHOME=/tmp/gpghome -export GNUPGHOME +if [[ $VERSION =~ [^.*-SNAPSHOT$] ]] ; then -mkdir $GNUPGHOME -cp $KEYRING $GNUPGHOME + echo "Cannot deploy a snapshot: $VERSION" + exit 1 +fi + +if [[ $VERSION =~ [^(\d+\.)+(RC(\d+)|M(\d+)|RELEASE)$] ]] ; then + + # + # Prepare GPG Key is expected to be in base64 + # Exported with gpg -a --export-secret-keys "your@email" | base64 > gpg.base64 + # + printf "$GPG_KEY_BASE64" | base64 --decode > gpg.asc + echo ${GPG_PASSPHRASE} | gpg --batch --yes --passphrase-fd 0 --import gpg.asc + gpg -k + + # + # Stage on Maven Central + # + echo "Staging $VERSION to Maven Central" + + ./mvnw \ + -s settings.xml \ + -Pcentral \ + -Dmaven.test.skip=true \ + -Dgpg.passphrase=${GPG_PASSPHRASE} \ + clean deploy -B +else + + echo "Not a release: $VERSION" + exit 1 +fi -MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw \ - -s settings.xml \ - -P${PROFILE} \ - -Dmaven.test.skip=true \ - -Dgpg.passphrase=${PASSPHRASE} \ - -Dgpg.secretKeyring=${GNUPGHOME}/secring.gpg \ - clean deploy -B diff --git a/ci/create-release.sh b/ci/create-release.sh deleted file mode 100755 index d0b26dd7..00000000 --- a/ci/create-release.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -RELEASE=$1 -SNAPSHOT=$2 - -./mvnw versions:set -DnewVersion=$RELEASE -DgenerateBackupPoms=false -git add . -git commit --message "v$RELEASE Release" - -# Tag the release -git tag -s v$RELEASE -m "v$RELEASE" - -# Bump up the version in pom.xml to the next snapshot -./mvnw versions:set -DnewVersion=$SNAPSHOT -DgenerateBackupPoms=false -git add . -git commit --message "v$SNAPSHOT Development" diff --git a/ci/test.sh b/ci/test.sh deleted file mode 100755 index 9effba2f..00000000 --- a/ci/test.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -P${PROFILE} clean dependency:list test -Dsort -B diff --git a/pom.xml b/pom.xml index 3a744d15..79515d62 100644 --- a/pom.xml +++ b/pom.xml @@ -316,226 +316,95 @@ snapshot - - - - - org.jfrog.buildinfo - artifactory-maven-plugin - 2.6.1 - false - - - build-info - - publish - - - - {{BUILD_URL}} - - - r2dbc-proxy - r2dbc-proxy - false - *:*:*:*@zip - - - https://repo.spring.io - {{ARTIFACTORY_USR}} - {{ARTIFACTORY_PSW}} - libs-snapshot-local - libs-snapshot-local - - - - - - - + + + sonatype + https://oss.sonatype.org/content/repositories/snapshots + + - milestone - + central + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + sonatype + https://oss.sonatype.org/ + false + true + true + + + + + + + - org.jfrog.buildinfo - artifactory-maven-plugin - 2.6.1 - false - - - build-info - - publish - - - - {{BUILD_URL}} - - - r2dbc-proxy - r2dbc-proxy - false - *:*:*:*@zip - - - https://repo.spring.io - {{ARTIFACTORY_USR}} - {{ARTIFACTORY_PSW}} - libs-milestone-local - libs-milestone-local - - - - + org.apache.maven.plugins + maven-gpg-plugin - - - - - release - - - - org.jfrog.buildinfo - artifactory-maven-plugin - 2.6.1 - false - - - build-info - - publish - - - - {{BUILD_URL}} - - - r2dbc-proxy - r2dbc-proxy - false - *:*:*:*@zip - - - https://repo.spring.io - {{ARTIFACTORY_USR}} - {{ARTIFACTORY_PSW}} - libs-release-local - libs-release-local - - - - + org.sonatype.plugins + nexus-staging-maven-plugin - - - - - - central - - - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - - --pinentry-mode - loopback - - - - - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.8 - true - - sonatype - https://oss.sonatype.org/ - false - true - true - - - - - - - - - org.apache.maven.plugins - maven-gpg-plugin - - - - org.sonatype.plugins - nexus-staging-maven-plugin - - - + - + - - - sonatype - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - + + + sonatype + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + - + - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - true - + sonatype-nexus-snapshots + Sonatype OSS Snapshot Repository + https://oss.sonatype.org/content/repositories/snapshots - - - spring-plugins-release - https://repo.spring.io/plugins-release - - - diff --git a/settings.xml b/settings.xml index 95700a15..2a04e7bd 100644 --- a/settings.xml +++ b/settings.xml @@ -5,8 +5,8 @@ sonatype - ${env.SONATYPE_USR} - ${env.SONATYPE_PSW} + ${env.SONATYPE_USER} + ${env.SONATYPE_PASSWORD} - \ No newline at end of file + From 74155cd45cfd4352cb8db7687fa869377969b254 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 15 Oct 2020 16:31:12 +0200 Subject: [PATCH 37/74] Polishing --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 79515d62..1e7e0cef 100644 --- a/pom.xml +++ b/pom.xml @@ -161,7 +161,7 @@ org.springframework.hateoas spring-hateoas - 1.0.0.RC2 + 1.0.0.RELEASE test From 66d141ccd31ff5520ca9e067460308882c270d75 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 21 Oct 2020 14:25:11 -0700 Subject: [PATCH 38/74] Update dependencies - R2DBC SPI 0.8.3 - Reactor Dysprosium SR12 - JUnit 5.7.0 - AssertJ 3.17.2 [closes #75] --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 1e7e0cef..76609485 100644 --- a/pom.xml +++ b/pom.xml @@ -32,15 +32,15 @@ https://github.com/r2dbc/r2dbc-proxy - 3.16.1 + 3.17.2 1.8 3.0.2 - 5.6.1 + 5.7.0 1.2.3 UTF-8 UTF-8 - 0.8.1.RELEASE - Dysprosium-SR7 + 0.8.3.RELEASE + Dysprosium-SR12 2.2.0.RELEASE 3.3.3 From 8c7431cf7564d07079003f2be9972f9ec6868468 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 21 Oct 2020 14:27:54 -0700 Subject: [PATCH 39/74] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index be1a6056..4754aba5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ R2DBC Proxy Changelog 0.8.3.RELEASE ------------------ +* Upgrade dependencies #75 * Introduce "ProxyMethodExecutionListener" and deprecate "LifeCycleListener" #72 * Use custom subscribers to manage callback #70 * Add ProxyConfigHolder #69 From 8f60c49763a0631b5047f58a7943a72451eb1028 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 22 Oct 2020 16:58:48 +0200 Subject: [PATCH 40/74] Upgrade to Mockito 3.5.15 [#75] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 76609485..ec9e1297 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ 0.8.3.RELEASE Dysprosium-SR12 2.2.0.RELEASE - 3.3.3 + 3.5.15 From 26596410c946bf1f079bc2d9133bcae0cc913286 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 22 Oct 2020 17:00:08 +0200 Subject: [PATCH 41/74] Release 0.8.3.RELEASE [resolves #73] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ec9e1297..91fa9016 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ io.r2dbc r2dbc-proxy - 0.8.3.BUILD-SNAPSHOT + 0.8.3.RELEASE jar Reactive Relational Database Connectivity - Proxy From 491017ec0f2e2bac5703bf7668c64d5a8998cca9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 22 Oct 2020 17:01:24 +0200 Subject: [PATCH 42/74] Prepare next development iteration [#73] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 91fa9016..db1f9ebe 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ io.r2dbc r2dbc-proxy - 0.8.3.RELEASE + 0.8.4.BUILD-SNAPSHOT jar Reactive Relational Database Connectivity - Proxy From b775b91d36fcc2f7a362ba3f67345a26cb5396e2 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Fri, 4 Dec 2020 17:40:45 -0800 Subject: [PATCH 43/74] Use caller's classloader when creating a proxy Previously, creating proxy used thread context classloader which caused a failure for classloader visibility to R2DBC SPI classes in some environment. Instead of using thread context classloader, use caller's classloader when creating a proxy. Also, updates `JdkProxyFactory` to easily change the proxy creation by extending the class. [resolves #78] --- .../r2dbc/proxy/callback/JdkProxyFactory.java | 19 ++++++------- .../proxy/callback/JdkProxyFactoryTest.java | 28 +++++++++++++++++++ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java b/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java index 660b7266..0c717676 100644 --- a/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java +++ b/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java @@ -59,8 +59,7 @@ public ConnectionFactory wrapConnectionFactory(ConnectionFactory connectionFacto CallbackHandler logic = new ConnectionFactoryCallbackHandler(connectionFactory, this.proxyConfig); CallbackInvocationHandler invocationHandler = new CallbackInvocationHandler(logic); - return (ConnectionFactory) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), - new Class[]{ConnectionFactory.class, Wrapped.class, ProxyConfigHolder.class}, invocationHandler); + return createProxy(invocationHandler, ConnectionFactory.class, Wrapped.class, ProxyConfigHolder.class); } @Override @@ -70,8 +69,7 @@ public Connection wrapConnection(Connection connection, ConnectionInfo connectio CallbackHandler logic = new ConnectionCallbackHandler(connection, connectionInfo, this.proxyConfig); CallbackInvocationHandler invocationHandler = new CallbackInvocationHandler(logic); - return (Connection) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), - new Class[]{Connection.class, Wrapped.class, ConnectionHolder.class, ProxyConfigHolder.class}, invocationHandler); + return createProxy(invocationHandler, Connection.class, Wrapped.class, ConnectionHolder.class, ProxyConfigHolder.class); } @Override @@ -81,8 +79,7 @@ public Batch wrapBatch(Batch batch, ConnectionInfo connectionInfo) { CallbackHandler logic = new BatchCallbackHandler(batch, connectionInfo, this.proxyConfig); CallbackInvocationHandler invocationHandler = new CallbackInvocationHandler(logic); - return (Batch) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), - new Class[]{Batch.class, Wrapped.class, ConnectionHolder.class, ProxyConfigHolder.class}, invocationHandler); + return createProxy(invocationHandler, Batch.class, Wrapped.class, ConnectionHolder.class, ProxyConfigHolder.class); } @Override @@ -93,8 +90,7 @@ public Statement wrapStatement(Statement statement, StatementInfo statementInfo, CallbackHandler logic = new StatementCallbackHandler(statement, statementInfo, connectionInfo, this.proxyConfig); CallbackInvocationHandler invocationHandler = new CallbackInvocationHandler(logic); - return (Statement) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), - new Class[]{Statement.class, Wrapped.class, ConnectionHolder.class, ProxyConfigHolder.class}, invocationHandler); + return createProxy(invocationHandler, Statement.class, Wrapped.class, ConnectionHolder.class, ProxyConfigHolder.class); } @Override @@ -104,10 +100,13 @@ public Result wrapResult(Result result, QueryExecutionInfo queryExecutionInfo) { CallbackHandler logic = new ResultCallbackHandler(result, queryExecutionInfo, this.proxyConfig); CallbackInvocationHandler invocationHandler = new CallbackInvocationHandler(logic); - return (Result) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), - new Class[]{Result.class, Wrapped.class, ConnectionHolder.class, ProxyConfigHolder.class}, invocationHandler); + return createProxy(invocationHandler, Result.class, Wrapped.class, ConnectionHolder.class, ProxyConfigHolder.class); } + @SuppressWarnings("unchecked") + protected T createProxy(InvocationHandler invocationHandler, Class... interfaces) { + return (T) Proxy.newProxyInstance(getClass().getClassLoader(), interfaces, invocationHandler); + } /** * {@link InvocationHandler} implementation that delegates to {@link CallbackHandler}. diff --git a/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java b/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java index 421ca297..17d7ec4b 100644 --- a/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java @@ -36,9 +36,12 @@ import org.junit.jupiter.api.Test; import java.lang.reflect.Proxy; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; /** @@ -137,6 +140,31 @@ void testToString() { } + @Test + void noThreadContextClassloader() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + + AtomicReference exHolder = new AtomicReference<>(); + Runnable logic = () -> { + try { + this.proxyFactory.wrapConnectionFactory(MockConnectionFactory.empty()); + } catch (Exception ex) { + exHolder.set(ex); + } + latch.countDown(); + }; + + ClassLoader classLoader = mock(ClassLoader.class); + Thread thread = new Thread(logic); + thread.setContextClassLoader(classLoader); + thread.start(); + + latch.await(); + + assertThat(exHolder.get()).isNull(); + verifyNoInteractions(classLoader); + } + private String getExpectedToString(Object target) { return target.getClass().getSimpleName() + "-proxy [" + target.toString() + "]"; } From c4bb3834a949c3be2e489829ef4791d3ecf5a3ee Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 9 Dec 2020 16:42:07 -0800 Subject: [PATCH 44/74] Update changelog --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 4754aba5..eaf47a2c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ R2DBC Proxy Changelog ============================= +0.8.4.RELEASE +------------------ +* Use caller's classloader when creating a proxy #78 + 0.8.3.RELEASE ------------------ * Upgrade dependencies #75 From f60f6367a3e4d8b975d1d15efdd0a36021b19cb6 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Thu, 10 Dec 2020 10:03:56 -0800 Subject: [PATCH 45/74] Release 0.8.4.RELEASE --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index db1f9ebe..cbb6b013 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ io.r2dbc r2dbc-proxy - 0.8.4.BUILD-SNAPSHOT + 0.8.4.RELEASE jar Reactive Relational Database Connectivity - Proxy From daf8fe719d7b643b74994e779a741ff8278e4a93 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Thu, 10 Dec 2020 10:08:30 -0800 Subject: [PATCH 46/74] Prepare next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cbb6b013..2c18947f 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ io.r2dbc r2dbc-proxy - 0.8.4.RELEASE + 0.8.5.BUILD-SNAPSHOT jar Reactive Relational Database Connectivity - Proxy From cccf2825d4f8cc636023a71d527387ed4b106e1d Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Tue, 23 Feb 2021 16:22:02 -0800 Subject: [PATCH 47/74] Upgrade R2DBC SPI to 0.8.4.RELEASE [resolves #83] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2c18947f..904b3035 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ 1.2.3 UTF-8 UTF-8 - 0.8.3.RELEASE + 0.8.4.RELEASE Dysprosium-SR12 2.2.0.RELEASE 3.5.15 From a6b70eb443522702bdf0a911e6ee90fb9182b15f Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Tue, 23 Feb 2021 16:25:57 -0800 Subject: [PATCH 48/74] Update CHAGELOG for 0.8.5.RELEASE [#83] --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index eaf47a2c..346272a4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ R2DBC Proxy Changelog ============================= +0.8.5.RELEASE +------------------ +* Upgrade to R2DBC SPI 0.8.4.RELEASE #83 + 0.8.4.RELEASE ------------------ * Use caller's classloader when creating a proxy #78 From 36955b922e4fe836214410253aeba7d67f57454a Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 24 Feb 2021 10:54:47 -0800 Subject: [PATCH 49/74] Upgrade to Reactor Dysprosium SR17 [resolves #84] --- CHANGELOG | 1 + pom.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 346272a4..43b83290 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ R2DBC Proxy Changelog 0.8.5.RELEASE ------------------ +* Upgrade to Reactor Dysprosium SR17 #84 * Upgrade to R2DBC SPI 0.8.4.RELEASE #83 0.8.4.RELEASE diff --git a/pom.xml b/pom.xml index 904b3035..d2d0af26 100644 --- a/pom.xml +++ b/pom.xml @@ -40,7 +40,7 @@ UTF-8 UTF-8 0.8.4.RELEASE - Dysprosium-SR12 + Dysprosium-SR17 2.2.0.RELEASE 3.5.15 From 195c720d453874d94b8d09871babee3c810aa7c9 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 24 Feb 2021 11:14:07 -0800 Subject: [PATCH 50/74] Copy "publish-docs.yml" from main [#82] --- .github/workflows/publish-docs.yml | 85 ++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 .github/workflows/publish-docs.yml diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml new file mode 100644 index 00000000..3d23986d --- /dev/null +++ b/.github/workflows/publish-docs.yml @@ -0,0 +1,85 @@ +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Publish documentation to the project page + +on: + push: + branches: [ main, release-0.x ] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + + - name: Cache Maven packages + uses: actions/cache@v2 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + - name: Get project version + run: | + VERSION=$( ./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout ) + echo "project_version=$VERSION" >> $GITHUB_ENV + + - name: Process asciidoc and javadoc + run: ./mvnw -q asciidoctor:process-asciidoc javadoc:javadoc + + # + # construct a directory to be copied to "gh-pages" branch + # target/deploy-documents/ -- map to "docs" dir in "gh-pages" + # `-- -- e.g. "0.9.0.BUILD-SNAPSHOT" + # `-- docs/html/ + # `-- api/ + # `-- CHANGELOG.txt + # `-- current-snapshot -- for latest snapshot from main + # `-- docs/html/ + # `-- api/ + # `-- CHANGELOG.txt + # `-- current -- for latest release version + # `-- docs/html/ + # `-- api/ + # `-- CHANGELOG.txt + + - name: Prepare "project-version" documents + run: | + mkdir -p target/deploy-documents/${{ env.project_version }}/docs/html + mkdir -p target/deploy-documents/${{ env.project_version }}/api + cp -Rf target/generated-docs/* target/deploy-documents/${{ env.project_version }}/docs/html/ + cp -Rf target/site/apidocs/* target/deploy-documents/${{ env.project_version }}/api/ + cp CHANGELOG target/deploy-documents/${{ env.project_version }}/CHANGELOG.txt + + - name: Prepare "current-snapshot" documents + if: "github.ref == 'refs/heads/main' && contains(env.project_version, 'snapshot')" + run: | + mkdir -p target/deploy-documents/current-snapshot/docs/html + mkdir -p target/deploy-documents/current-snapshot/api + cp -Rf target/generated-docs/* target/deploy-documents/current-snapshot/docs/html/ + cp -Rf target/site/apidocs/* target/deploy-documents/current-snapshot/api/ + cp CHANGELOG target/deploy-documents/current-snapshot/CHANGELOG.txt + + - name: Prepare "current" documents + if: "contains(env.project_version, 'release')" + run: | + mkdir -p target/deploy-documents/current/docs/html + mkdir -p target/deploy-documents/current/api + cp -Rf target/generated-docs/* target/deploy-documents/current/docs/html/ + cp -Rf target/site/apidocs/* target/deploy-documents/current/api/ + cp CHANGELOG target/deploy-documents/current/CHANGELOG.txt + + - name: Deploy documents + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_branch: gh-pages + publish_dir: target/deploy-documents + destination_dir: docs + keep_files: true + full_commit_message: "Deploying documents(${{ env.project_version}}) to ${{ github.ref }} from ${{ github.repository }}@${{ github.sha }}" From dd34a4eb6809127888bf84112d1005a6cbdb4571 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 24 Feb 2021 11:18:23 -0800 Subject: [PATCH 51/74] Revert "Copy "publish-docs.yml" from main" This reverts commit 195c720d453874d94b8d09871babee3c810aa7c9. Since this branch doesn't have asciidoc plugin, the action will fail at release. --- .github/workflows/publish-docs.yml | 85 ------------------------------ 1 file changed, 85 deletions(-) delete mode 100644 .github/workflows/publish-docs.yml diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml deleted file mode 100644 index 3d23986d..00000000 --- a/.github/workflows/publish-docs.yml +++ /dev/null @@ -1,85 +0,0 @@ -# This workflow will build a Java project with Maven -# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven - -name: Publish documentation to the project page - -on: - push: - branches: [ main, release-0.x ] - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 - with: - java-version: 1.8 - - - name: Cache Maven packages - uses: actions/cache@v2 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - - name: Get project version - run: | - VERSION=$( ./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout ) - echo "project_version=$VERSION" >> $GITHUB_ENV - - - name: Process asciidoc and javadoc - run: ./mvnw -q asciidoctor:process-asciidoc javadoc:javadoc - - # - # construct a directory to be copied to "gh-pages" branch - # target/deploy-documents/ -- map to "docs" dir in "gh-pages" - # `-- -- e.g. "0.9.0.BUILD-SNAPSHOT" - # `-- docs/html/ - # `-- api/ - # `-- CHANGELOG.txt - # `-- current-snapshot -- for latest snapshot from main - # `-- docs/html/ - # `-- api/ - # `-- CHANGELOG.txt - # `-- current -- for latest release version - # `-- docs/html/ - # `-- api/ - # `-- CHANGELOG.txt - - - name: Prepare "project-version" documents - run: | - mkdir -p target/deploy-documents/${{ env.project_version }}/docs/html - mkdir -p target/deploy-documents/${{ env.project_version }}/api - cp -Rf target/generated-docs/* target/deploy-documents/${{ env.project_version }}/docs/html/ - cp -Rf target/site/apidocs/* target/deploy-documents/${{ env.project_version }}/api/ - cp CHANGELOG target/deploy-documents/${{ env.project_version }}/CHANGELOG.txt - - - name: Prepare "current-snapshot" documents - if: "github.ref == 'refs/heads/main' && contains(env.project_version, 'snapshot')" - run: | - mkdir -p target/deploy-documents/current-snapshot/docs/html - mkdir -p target/deploy-documents/current-snapshot/api - cp -Rf target/generated-docs/* target/deploy-documents/current-snapshot/docs/html/ - cp -Rf target/site/apidocs/* target/deploy-documents/current-snapshot/api/ - cp CHANGELOG target/deploy-documents/current-snapshot/CHANGELOG.txt - - - name: Prepare "current" documents - if: "contains(env.project_version, 'release')" - run: | - mkdir -p target/deploy-documents/current/docs/html - mkdir -p target/deploy-documents/current/api - cp -Rf target/generated-docs/* target/deploy-documents/current/docs/html/ - cp -Rf target/site/apidocs/* target/deploy-documents/current/api/ - cp CHANGELOG target/deploy-documents/current/CHANGELOG.txt - - - name: Deploy documents - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_branch: gh-pages - publish_dir: target/deploy-documents - destination_dir: docs - keep_files: true - full_commit_message: "Deploying documents(${{ env.project_version}}) to ${{ github.ref }} from ${{ github.repository }}@${{ github.sha }}" From 74a22608d7a9ee0ac674eb508d64ccd9b4daf2f0 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 24 Feb 2021 11:22:44 -0800 Subject: [PATCH 52/74] [maven-release-plugin] prepare release v0.8.5.RELEASE --- pom.xml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index d2d0af26..f96fd160 100644 --- a/pom.xml +++ b/pom.xml @@ -14,17 +14,13 @@ ~ limitations under the License. --> - + 4.0.0 io.r2dbc r2dbc-proxy - 0.8.5.BUILD-SNAPSHOT + 0.8.5.RELEASE jar Reactive Relational Database Connectivity - Proxy @@ -56,7 +52,8 @@ scm:git:https://github.com/r2dbc/r2dbc-proxy https://github.com/r2dbc/r2dbc-proxy - + v0.8.5.RELEASE + From 4904f920235df4243a2c96224227072aeeb861e9 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 24 Feb 2021 11:22:51 -0800 Subject: [PATCH 53/74] [maven-release-plugin] prepare for next development iteration --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index f96fd160..7cd586f2 100644 --- a/pom.xml +++ b/pom.xml @@ -14,13 +14,13 @@ ~ limitations under the License. --> - + 4.0.0 io.r2dbc r2dbc-proxy - 0.8.5.RELEASE + 0.8.6.BUILD-SNAPSHOT jar Reactive Relational Database Connectivity - Proxy @@ -52,7 +52,7 @@ scm:git:https://github.com/r2dbc/r2dbc-proxy https://github.com/r2dbc/r2dbc-proxy - v0.8.5.RELEASE + HEAD From d2a87bb5827f7273ed6a9037c1433913dec2ff54 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 12 May 2021 11:50:12 -0700 Subject: [PATCH 54/74] Upgrade to Reactor Dysprosium SR20 [resolves #91] Signed-off-by: Tadaya Tsuyukubo --- pom.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7cd586f2..5ea355c4 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,7 @@ UTF-8 UTF-8 0.8.4.RELEASE - Dysprosium-SR17 + Dysprosium-SR20 2.2.0.RELEASE 3.5.15 @@ -401,6 +401,9 @@ sonatype-nexus-snapshots Sonatype OSS Snapshot Repository https://oss.sonatype.org/content/repositories/snapshots + + true + From 6ae86831d27c25b92076178b56978de7ae61c2ae Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 12 May 2021 11:52:10 -0700 Subject: [PATCH 55/74] Upgrade to SPI 0.8.5.RELEASE [resolves #92] Signed-off-by: Tadaya Tsuyukubo --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5ea355c4..0882a55f 100644 --- a/pom.xml +++ b/pom.xml @@ -35,7 +35,7 @@ 1.2.3 UTF-8 UTF-8 - 0.8.4.RELEASE + 0.8.5.RELEASE Dysprosium-SR20 2.2.0.RELEASE 3.5.15 From 07224b60fef2694ecb471e07de247a4533a786a4 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 12 May 2021 13:24:13 -0700 Subject: [PATCH 56/74] Update changelog [#93] Signed-off-by: Tadaya Tsuyukubo --- CHANGELOG | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 43b83290..692da2b3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ R2DBC Proxy Changelog ============================= +0.8.6.RELEASE +------------------ +* Upgrade to Reactor Dysprosium SR20 #91 +* Upgrade to R2DBC SPI 0.8.5.RELEASE #92 + 0.8.5.RELEASE ------------------ * Upgrade to Reactor Dysprosium SR17 #84 From 025bb437f13401ce337263412d82c5b73387b1ab Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 12 May 2021 13:18:58 -0700 Subject: [PATCH 57/74] Release 0.8.6.RELEASE [closes #93] Signed-off-by: Tadaya Tsuyukubo --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0882a55f..ac1acc12 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ io.r2dbc r2dbc-proxy - 0.8.6.BUILD-SNAPSHOT + 0.8.6.RELEASE jar Reactive Relational Database Connectivity - Proxy From 92d3e3ba3304a878c7ce0f352f6cbeb2e58feb97 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 12 May 2021 13:20:53 -0700 Subject: [PATCH 58/74] Prepare next development iteration [#93] Signed-off-by: Tadaya Tsuyukubo --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ac1acc12..c573bf45 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ io.r2dbc r2dbc-proxy - 0.8.6.RELEASE + 0.8.7.BUILD-SNAPSHOT jar Reactive Relational Database Connectivity - Proxy From 0fb0d297d7588b73b13d24e368063068ae807c2f Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Thu, 17 Jun 2021 17:12:56 -0700 Subject: [PATCH 59/74] Add failing tests to simulate the after-query callback failure When the publisher from `Statement#execute` completes while publisher from `Result#map` or `Result#getRowsUpdated`, the after query callback is called. The after callback should be invoked when the publisher from the `Result` operations have finished. This commit adds tests to simulate this situation and verifies it fails with current code. [#94] Signed-off-by: Tadaya Tsuyukubo --- .../callback/AfterQueryCallbackTest.java | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 src/test/java/io/r2dbc/proxy/callback/AfterQueryCallbackTest.java diff --git a/src/test/java/io/r2dbc/proxy/callback/AfterQueryCallbackTest.java b/src/test/java/io/r2dbc/proxy/callback/AfterQueryCallbackTest.java new file mode 100644 index 00000000..be99eb52 --- /dev/null +++ b/src/test/java/io/r2dbc/proxy/callback/AfterQueryCallbackTest.java @@ -0,0 +1,227 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.callback; + +import io.r2dbc.proxy.core.QueryExecutionInfo; +import io.r2dbc.proxy.core.StatementInfo; +import io.r2dbc.proxy.listener.ProxyExecutionListener; +import io.r2dbc.proxy.test.MockStatementInfo; +import io.r2dbc.spi.Result; +import io.r2dbc.spi.Statement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.test.publisher.TestPublisher; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Tadaya Tsuyukubo + */ + +class AfterQueryCallbackTest { + + private static final Logger logger = LoggerFactory.getLogger(AfterQueryCallbackTest.class); + + static class CountingListener implements ProxyExecutionListener { + + private final AtomicBoolean isBeforeQueryCalled = new AtomicBoolean(); + + private final AtomicBoolean isAfterQueryCalled = new AtomicBoolean(); + + private final AtomicInteger eachQueryResultCounter = new AtomicInteger(); + + @Override + public void eachQueryResult(QueryExecutionInfo execInfo) { + this.eachQueryResultCounter.incrementAndGet(); + logger.info("eachQueryResult " + execInfo.getCurrentMappedResult()); + } + + @Override + public void beforeQuery(QueryExecutionInfo execInfo) { + this.isBeforeQueryCalled.set(true); + logger.info("beforeQuery"); + } + + @Override + public void afterQuery(QueryExecutionInfo execInfo) { + this.isAfterQueryCalled.set(true); + logger.info("afterQuery"); + } + + public boolean isBeforeQueryCalled() { + return this.isBeforeQueryCalled.get(); + } + + public boolean isAfterQueryCalled() { + return this.isAfterQueryCalled.get(); + } + + public int getEachQueryResultCount() { + return this.eachQueryResultCounter.get(); + } + } + + CountingListener listener; + + // publisher for the return of "Statement#execute" + TestPublisher executePublisher; + + // publisher for the return of "Result#map" + TestPublisher resultPublisherForMap; + + // publisher for the return of "Result#getRowsUpdated" + TestPublisher resultPublisherForGetRowsUpdated; + + // a mock that returns above test-publishers for #map and #getRowsUpdated methods. + Result resultMock; + + + @BeforeEach + @SuppressWarnings("unchecked") + void beforeEach() { + this.listener = new CountingListener(); + this.executePublisher = TestPublisher.create(); + this.resultPublisherForMap = TestPublisher.create(); + this.resultPublisherForGetRowsUpdated = TestPublisher.create(); + + this.resultMock = mock(Result.class); + when(this.resultMock.map(any(BiFunction.class))).thenReturn(this.resultPublisherForMap); + when(this.resultMock.getRowsUpdated()).thenReturn(this.resultPublisherForGetRowsUpdated); + } + + // https://github.com/r2dbc/r2dbc-proxy/issues/94 + @Test + void completeExecutePublisherWhileProcessingResultWithMap() { + // This test performs the following scenario: + // - "Statement#execute" publishes a "Result" + // - "Result#map" publishes a String + // - "Statement#execute" publisher completes + // - "Result#map" publishes a String + // - "Result#map" publisher completes + + Flux flux = prepareFluxWithResultMap(); + flux.subscribe(); + + // received a result + this.executePublisher.next(this.resultMock); + verifyListener(false, 0); + + // process the first result + this.resultPublisherForMap.next("foo"); + verifyListener(false, 1); + + // now complete the publisher from Statement#execute + this.executePublisher.complete(); + verifyListener(false, 1); + + // process the second result + this.resultPublisherForMap.next("bar"); + verifyListener(false, 2); + + // complete the publisher for result + this.resultPublisherForMap.complete(); + verifyListener(true, 2); + } + + // https://github.com/r2dbc/r2dbc-proxy/issues/94 + @Test + void completeExecutePublisherWhileProcessingResultWithGetRowUpdated() { + // This test performs the following scenario: + // - "Statement#execute" publishes a Result + // - "Result#getRowsUpdated" publishes a number + // - "Statement#execute" publisher completes + // - "Result#getRowsUpdated" publisher completes + + Flux flux = prepareFluxWithResultGetRowsUpdated(); + flux.subscribe(); + + // received a result + this.executePublisher.next(this.resultMock); + verifyListener(false, 0); + + // process the "getRowsUpdated" result + // "listener#eachQueryResult" won't be called for "getRowsUpdated" + this.resultPublisherForGetRowsUpdated.next(100); + verifyListener(false, 0); + + this.executePublisher.complete(); + verifyListener(false, 0); + + this.resultPublisherForGetRowsUpdated.complete(); + verifyListener(true, 0); + } + + @SuppressWarnings("unchecked") + private Flux prepareFluxWithResultGetRowsUpdated() { + Statement mockStatement = mock(Statement.class); + when((Publisher) mockStatement.execute()).thenReturn(this.executePublisher); + + ProxyFactory proxyFactory = createProxyFactory(); + StatementInfo statementInfo = MockStatementInfo.builder().updatedQuery("SELECT * FROM foo").build(); + Statement proxyStatement = proxyFactory.wrapStatement(mockStatement, statementInfo, new DefaultConnectionInfo()); + + // perform "Statement#execute" and "Result#map" + Flux flux = Flux.from(proxyStatement.execute()).flatMap(result -> { + return result.getRowsUpdated(); + }); + + return flux; + } + + @SuppressWarnings("unchecked") + private Flux prepareFluxWithResultMap() { + Statement mockStatement = mock(Statement.class); + when((Publisher) mockStatement.execute()).thenReturn(this.executePublisher); + + ProxyFactory proxyFactory = createProxyFactory(); + StatementInfo statementInfo = MockStatementInfo.builder().updatedQuery("SELECT * FROM foo").build(); + Statement proxyStatement = proxyFactory.wrapStatement(mockStatement, statementInfo, new DefaultConnectionInfo()); + + // perform "Statement#execute" and "Result#map" + Flux flux = Flux.from(proxyStatement.execute()).flatMap(result -> + result.map((row, metadata) -> + // this lambda is skipped since the "resultMock" always return "resultPublisher" + row.get("name", String.class) + ) + ); + + return flux; + } + + private ProxyFactory createProxyFactory() { + ProxyConfig proxyConfig = ProxyConfig.builder().listener(this.listener).build(); + return new JdkProxyFactory(proxyConfig); + } + + private void verifyListener(boolean isAfterQueryCalled, int eachQueryResultCount) { + assertThat(this.listener.isBeforeQueryCalled()).as("isBeforeQueryCalled").isTrue(); + assertThat(this.listener.isAfterQueryCalled()).as("isAfterQueryCalled").isEqualTo(isAfterQueryCalled); + assertThat(this.listener.getEachQueryResultCount()).as("eachQueryResultCount").isEqualTo(eachQueryResultCount); + } + +} From 2c12a491d8b192150a753d99fbb2977a8f071f44 Mon Sep 17 00:00:00 2001 From: Deblock Thomas Date: Wed, 9 Jun 2021 08:59:18 +0200 Subject: [PATCH 60/74] feat: call afterQuery only when query is fully executed Signed-off-by: Deblock Thomas --- .../callback/CallbackHandlerSupport.java | 5 +- .../r2dbc/proxy/callback/JdkProxyFactory.java | 4 +- .../callback/MutableQueryExecutionInfo.java | 2 +- .../io/r2dbc/proxy/callback/ProxyFactory.java | 2 +- .../callback/QueriesExecutionCounter.java | 54 +++++ .../callback/QueryInvocationSubscriber.java | 28 ++- .../proxy/callback/ResultCallbackHandler.java | 34 ++- .../callback/CallbackHandlerSupportTest.java | 17 +- .../proxy/callback/JdkProxyFactoryTest.java | 4 +- .../r2dbc/proxy/callback/ProxyUtilsTest.java | 3 +- .../callback/ResultCallbackHandlerTest.java | 207 +++++++++++++++++- .../StatementCallbackHandlerTest.java | 43 +++- 12 files changed, 352 insertions(+), 51 deletions(-) create mode 100644 src/main/java/io/r2dbc/proxy/callback/QueriesExecutionCounter.java diff --git a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java index 348028b8..17d6a7ba 100644 --- a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java +++ b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java @@ -213,15 +213,16 @@ protected Flux interceptQueryExecution(Publisher, ? extends Publisher> transformer = Operators.liftPublisher((pub, subscriber) -> - new QueryInvocationSubscriber(subscriber, executionInfo, proxyConfig)); + new QueryInvocationSubscriber(subscriber, executionInfo, proxyConfig, queriesExecutionCounter)); return Flux.from(publisher) .cast(Result.class) .transform(transformer) - .map(queryResult -> proxyFactory.wrapResult(queryResult, executionInfo)); + .map(queryResult -> proxyFactory.wrapResult(queryResult, executionInfo, queriesExecutionCounter)); } /** diff --git a/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java b/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java index 0c717676..fe53bb7e 100644 --- a/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java +++ b/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java @@ -94,11 +94,11 @@ public Statement wrapStatement(Statement statement, StatementInfo statementInfo, } @Override - public Result wrapResult(Result result, QueryExecutionInfo queryExecutionInfo) { + public Result wrapResult(Result result, QueryExecutionInfo queryExecutionInfo, QueriesExecutionCounter queriesExecutionCounter) { Assert.requireNonNull(result, "result must not be null"); Assert.requireNonNull(queryExecutionInfo, "queryExecutionInfo must not be null"); - CallbackHandler logic = new ResultCallbackHandler(result, queryExecutionInfo, this.proxyConfig); + CallbackHandler logic = new ResultCallbackHandler(result, queryExecutionInfo, this.proxyConfig, queriesExecutionCounter); CallbackInvocationHandler invocationHandler = new CallbackInvocationHandler(logic); return createProxy(invocationHandler, Result.class, Wrapped.class, ConnectionHolder.class, ProxyConfigHolder.class); } diff --git a/src/main/java/io/r2dbc/proxy/callback/MutableQueryExecutionInfo.java b/src/main/java/io/r2dbc/proxy/callback/MutableQueryExecutionInfo.java index 4de35c1d..b92b41b7 100644 --- a/src/main/java/io/r2dbc/proxy/callback/MutableQueryExecutionInfo.java +++ b/src/main/java/io/r2dbc/proxy/callback/MutableQueryExecutionInfo.java @@ -23,6 +23,7 @@ import io.r2dbc.proxy.core.QueryExecutionInfo; import io.r2dbc.proxy.core.QueryInfo; import io.r2dbc.proxy.core.ValueStore; +import io.r2dbc.spi.Result; import reactor.util.annotation.Nullable; import java.lang.reflect.Method; @@ -209,5 +210,4 @@ public int getCurrentResultCount() { public Object getCurrentMappedResult() { return currentMappedResult; } - } diff --git a/src/main/java/io/r2dbc/proxy/callback/ProxyFactory.java b/src/main/java/io/r2dbc/proxy/callback/ProxyFactory.java index fb32ae43..9fa8b6cf 100644 --- a/src/main/java/io/r2dbc/proxy/callback/ProxyFactory.java +++ b/src/main/java/io/r2dbc/proxy/callback/ProxyFactory.java @@ -86,6 +86,6 @@ public interface ProxyFactory { * @throws IllegalArgumentException if {@code result} is {@code null} * @throws IllegalArgumentException if {@code executionInfo} is {@code null} */ - Result wrapResult(Result result, QueryExecutionInfo executionInfo); + Result wrapResult(Result result, QueryExecutionInfo executionInfo, QueriesExecutionCounter queriesExecutionCounter); } diff --git a/src/main/java/io/r2dbc/proxy/callback/QueriesExecutionCounter.java b/src/main/java/io/r2dbc/proxy/callback/QueriesExecutionCounter.java new file mode 100644 index 00000000..f43d1ead --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/callback/QueriesExecutionCounter.java @@ -0,0 +1,54 @@ +package io.r2dbc.proxy.callback; + +import java.time.Duration; + +/** + * Utility class to know how many result are processing and if all result has been processed. + * + * @author Thomas Deblock + */ +public class QueriesExecutionCounter { + private int numberOfGeneratedResult; + private int numberOfProcessedResult; + private boolean allResultHasBeenGenerated; + private final StopWatch stopWatch; + + public QueriesExecutionCounter(StopWatch stopWatch) { + this.numberOfGeneratedResult = 0; + this.numberOfProcessedResult = 0; + this.allResultHasBeenGenerated = false; + this.stopWatch = stopWatch; + } + + public void addGeneratedResult() { + this.numberOfGeneratedResult++; + } + + public Duration getElapsedDuration() { + return this.stopWatch.getElapsedDuration(); + } + + public void queryStarted() { + this.stopWatch.start(); + } + + public void resultProcessed() { + this.numberOfProcessedResult++; + } + + public boolean isQueryEnded() { + return this.areAllResultProcessed() && this.areAllResultGenerated(); + } + + public boolean areAllResultProcessed() { + return this.numberOfProcessedResult >= this.numberOfGeneratedResult; + } + + public boolean areAllResultGenerated() { + return this.allResultHasBeenGenerated; + } + + public void allResultHasBeenGenerated() { + this.allResultHasBeenGenerated = true; + } +} diff --git a/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java b/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java index a947b968..a615333c 100644 --- a/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java +++ b/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java @@ -40,29 +40,28 @@ class QueryInvocationSubscriber implements CoreSubscriber, Subscription, private final ProxyExecutionListener listener; - private final StopWatch stopWatch; + private final QueriesExecutionCounter queriesExecutionCounter; private Subscription subscription; - public QueryInvocationSubscriber(CoreSubscriber delegate, MutableQueryExecutionInfo executionInfo, ProxyConfig proxyConfig) { + public QueryInvocationSubscriber(CoreSubscriber delegate, MutableQueryExecutionInfo executionInfo, ProxyConfig proxyConfig, QueriesExecutionCounter queriesExecutionCounter) { this.delegate = delegate; this.executionInfo = executionInfo; this.listener = proxyConfig.getListeners(); - this.stopWatch = new StopWatch(proxyConfig.getClock()); + this.queriesExecutionCounter = queriesExecutionCounter; } @Override public void onSubscribe(Subscription s) { this.subscription = s; + this.queriesExecutionCounter.queryStarted(); beforeQuery(); this.delegate.onSubscribe(this); } @Override public void onNext(Result result) { - // When at least one element is emitted, consider query execution is success, even when - // the publisher is canceled. see https://github.com/r2dbc/r2dbc-proxy/issues/55 - this.executionInfo.setSuccess(true); + this.queriesExecutionCounter.addGeneratedResult(); this.delegate.onNext(result); } @@ -76,8 +75,12 @@ public void onError(Throwable t) { @Override public void onComplete() { - this.executionInfo.setSuccess(true); - afterQuery(); + this.queriesExecutionCounter.allResultHasBeenGenerated(); + if (this.queriesExecutionCounter.isQueryEnded()) { + this.executionInfo.setSuccess(true); + afterQuery(); + } + this.delegate.onComplete(); } @@ -89,7 +92,10 @@ public void request(long n) { @Override public void cancel() { // do not determine success/failure by cancel - afterQuery(); + this.queriesExecutionCounter.allResultHasBeenGenerated(); + if (this.queriesExecutionCounter.isQueryEnded()) { + afterQuery(); + } this.subscription.cancel(); } @@ -137,13 +143,11 @@ private void beforeQuery() { this.executionInfo.setCurrentMappedResult(null); this.executionInfo.setProxyEventType(ProxyEventType.BEFORE_QUERY); - this.stopWatch.start(); - this.listener.beforeQuery(this.executionInfo); } private void afterQuery() { - this.executionInfo.setExecuteDuration(this.stopWatch.getElapsedDuration()); + this.executionInfo.setExecuteDuration(this.queriesExecutionCounter.getElapsedDuration()); this.executionInfo.setThreadName(Thread.currentThread().getName()); this.executionInfo.setThreadId(Thread.currentThread().getId()); this.executionInfo.setCurrentMappedResult(null); diff --git a/src/main/java/io/r2dbc/proxy/callback/ResultCallbackHandler.java b/src/main/java/io/r2dbc/proxy/callback/ResultCallbackHandler.java index ef313b8e..48866e8f 100644 --- a/src/main/java/io/r2dbc/proxy/callback/ResultCallbackHandler.java +++ b/src/main/java/io/r2dbc/proxy/callback/ResultCallbackHandler.java @@ -38,6 +38,8 @@ public final class ResultCallbackHandler extends CallbackHandlerSupport { private final MutableQueryExecutionInfo queryExecutionInfo; + private final QueriesExecutionCounter queriesExecutionCounter; + /** * Callback handler logic for {@link Result}. * @@ -47,16 +49,20 @@ public final class ResultCallbackHandler extends CallbackHandlerSupport { * @param result query result * @param queryExecutionInfo query execution info * @param proxyConfig proxy config + * @param queriesExecutionCounter queries execution counter * @throws IllegalArgumentException if {@code result} is {@code null} * @throws IllegalArgumentException if {@code queryExecutionInfo} is {@code null} * @throws IllegalArgumentException if {@code proxyConfig} is {@code null} + * @throws IllegalArgumentException if {@code queriesExecutionCounter} is {@code null} * @throws IllegalArgumentException if {@code queryExecutionInfo} is not an instance of {@link MutableQueryExecutionInfo} */ - public ResultCallbackHandler(Result result, QueryExecutionInfo queryExecutionInfo, ProxyConfig proxyConfig) { + public ResultCallbackHandler(Result result, QueryExecutionInfo queryExecutionInfo, ProxyConfig proxyConfig, QueriesExecutionCounter queriesExecutionCounter) { super(proxyConfig); this.result = Assert.requireNonNull(result, "result must not be null"); Assert.requireNonNull(queryExecutionInfo, "queryExecutionInfo must not be null"); this.queryExecutionInfo = Assert.requireType(queryExecutionInfo, MutableQueryExecutionInfo.class, "queryExecutionInfo must be MutableQueryExecutionInfo"); + Assert.requireNonNull(queriesExecutionCounter, "queriesExecutionCounter must not be null"); + this.queriesExecutionCounter = queriesExecutionCounter; } @Override @@ -75,14 +81,11 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl Object invocationResult = proceedExecution(method, this.result, args, this.proxyConfig.getListeners(), connectionInfo, null); - if ("map".equals(methodName)) { - + if ("map".equals(methodName) || "getRowsUpdated".equals(methodName)) { AtomicInteger resultCount = new AtomicInteger(0); - // add logic to call "listener#eachQueryResult()" return Flux.from((Publisher) invocationResult) .doOnEach(signal -> { - boolean proceed = signal.isOnNext() || signal.isOnError(); if (!proceed) { return; @@ -93,16 +96,17 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl if (signal.isOnNext()) { Object mappedResult = signal.get(); + this.queryExecutionInfo.setSuccess(true); this.queryExecutionInfo.setCurrentResultCount(count); this.queryExecutionInfo.setCurrentMappedResult(mappedResult); this.queryExecutionInfo.setThrowable(null); } else { // onError Throwable thrown = signal.getThrowable(); + this.queryExecutionInfo.setSuccess(false); this.queryExecutionInfo.setCurrentResultCount(count); this.queryExecutionInfo.setCurrentMappedResult(null); this.queryExecutionInfo.setThrowable(thrown); - } this.queryExecutionInfo.setProxyEventType(ProxyEventType.EACH_QUERY_RESULT); @@ -114,13 +118,25 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl // callback this.proxyConfig.getListeners().eachQueryResult(this.queryExecutionInfo); + }) + .switchIfEmpty(Flux.defer(() -> { + this.queryExecutionInfo.setSuccess(true); + return Flux.empty(); + })) + .doOnTerminate(() -> { + this.queriesExecutionCounter.resultProcessed(); + if (this.queriesExecutionCounter.isQueryEnded()) { + this.queryExecutionInfo.setExecuteDuration(this.queriesExecutionCounter.getElapsedDuration()); + this.queryExecutionInfo.setThreadName(Thread.currentThread().getName()); + this.queryExecutionInfo.setThreadId(Thread.currentThread().getId()); + this.queryExecutionInfo.setCurrentMappedResult(null); + this.queryExecutionInfo.setProxyEventType(ProxyEventType.AFTER_QUERY); + this.proxyConfig.getListeners().afterQuery(this.queryExecutionInfo); + } }); - } return invocationResult; - } - } diff --git a/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java b/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java index 8b8424a2..16376857 100644 --- a/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java @@ -99,7 +99,10 @@ void interceptQueryExecution() { // when it creates a proxy for Result Result mockResultProxy = MockResult.empty(); - when(proxyFactory.wrapResult(any(), any())).thenReturn(mockResultProxy); + when(proxyFactory.wrapResult(any(), any(), any())).thenAnswer((args)-> { + ((QueriesExecutionCounter) args.getArgument(2)).resultProcessed(); + return mockResultProxy; + }); // produce single result Result mockResult = MockResult.empty(); @@ -111,6 +114,7 @@ void interceptQueryExecution() { StepVerifier.create(result) .expectSubscription() .consumeNextWith(c -> { + //executionInfo.resultProcessed(c); // verify produced result is the proxy result assertThat(c).isSameAs(mockResultProxy); }) @@ -143,7 +147,7 @@ void interceptQueryExecution() { // verify the call to create a proxy result ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(Result.class); - verify(proxyFactory).wrapResult(resultCaptor.capture(), eq(executionInfo)); + verify(proxyFactory).wrapResult(resultCaptor.capture(), eq(executionInfo), any()); Result captureResult = resultCaptor.getValue(); assertThat(captureResult).isSameAs(mockResult); @@ -236,7 +240,10 @@ void interceptQueryExecutionWithMultipleResult() { // when it creates a proxy for Result Result mockResultProxy = mock(Result.class); - when(proxyFactory.wrapResult(any(), any())).thenReturn(mockResultProxy); + when(proxyFactory.wrapResult(any(), any(), any())).thenAnswer((args)-> { + ((QueriesExecutionCounter) args.getArgument(2)).resultProcessed(); + return mockResultProxy; + }); // produce multiple results Result mockResult1 = mock(Result.class); @@ -267,7 +274,6 @@ void interceptQueryExecutionWithMultipleResult() { }) .assertNext(c -> { assertThat(c).as("third result").isSameAs(mockResultProxy); - }) .expectComplete() .verify(); @@ -277,7 +283,6 @@ void interceptQueryExecutionWithMultipleResult() { assertThat(listener.getAfterMethodExecutionInfo()).isNull(); assertThat(listener.getBeforeQueryExecutionInfo()).isSameAs(executionInfo); assertThat(listener.getAfterQueryExecutionInfo()).isSameAs(executionInfo); - assertThat(executionInfo.getProxyEventType()).isEqualTo(ProxyEventType.AFTER_QUERY); String threadName = Thread.currentThread().getName(); @@ -298,7 +303,7 @@ void interceptQueryExecutionWithMultipleResult() { // verify the call to create proxy result ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(Result.class); - verify(proxyFactory, times(3)).wrapResult(resultCaptor.capture(), eq(executionInfo)); + verify(proxyFactory, times(3)).wrapResult(resultCaptor.capture(), eq(executionInfo), any()); List captured = resultCaptor.getAllValues(); assertThat(captured).hasSize(3).containsExactly(mockResult1, mockResult2, mockResult3); diff --git a/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java b/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java index 17d7ec4b..f01a5774 100644 --- a/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java @@ -76,7 +76,7 @@ void isProxy() { ConnectionInfo connectionInfo = MockConnectionInfo.empty(); StatementInfo statementInfo = MockStatementInfo.empty(); QueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); // need to be mutable - + QueriesExecutionCounter queriesExecutionCounter = mock(QueriesExecutionCounter.class); Object wrapped; wrapped = this.proxyFactory.wrapConnectionFactory(connectionFactory); @@ -103,7 +103,7 @@ void isProxy() { assertThat(wrapped).isInstanceOf(ConnectionHolder.class); assertThat(wrapped).isInstanceOf(ProxyConfigHolder.class); - wrapped = this.proxyFactory.wrapResult(result, queryExecutionInfo); + wrapped = this.proxyFactory.wrapResult(result, queryExecutionInfo, queriesExecutionCounter); assertThat(Proxy.isProxyClass(wrapped.getClass())).isTrue(); assertThat(wrapped).isInstanceOf(Wrapped.class); assertThat(wrapped).isInstanceOf(ConnectionHolder.class); diff --git a/src/test/java/io/r2dbc/proxy/callback/ProxyUtilsTest.java b/src/test/java/io/r2dbc/proxy/callback/ProxyUtilsTest.java index 2ce2fb5e..0dfb0bf6 100644 --- a/src/test/java/io/r2dbc/proxy/callback/ProxyUtilsTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/ProxyUtilsTest.java @@ -56,10 +56,11 @@ void unwrapConnection() { MutableQueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); queryExecutionInfo.setConnectionInfo(connectionInfo); + QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(new StopWatch(proxyConfig.getClock())); Batch proxyBatch = proxyConfig.getProxyFactory().wrapBatch(originalBatch, connectionInfo); Statement proxyStatement = proxyConfig.getProxyFactory().wrapStatement(originalStatement, statementInfo, connectionInfo); - Result proxyResult = proxyConfig.getProxyFactory().wrapResult(originalResult, queryExecutionInfo); + Result proxyResult = proxyConfig.getProxyFactory().wrapResult(originalResult, queryExecutionInfo, queriesExecutionCounter); Optional result; diff --git a/src/test/java/io/r2dbc/proxy/callback/ResultCallbackHandlerTest.java b/src/test/java/io/r2dbc/proxy/callback/ResultCallbackHandlerTest.java index 43f397b1..23ebf46d 100644 --- a/src/test/java/io/r2dbc/proxy/callback/ResultCallbackHandlerTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/ResultCallbackHandlerTest.java @@ -56,7 +56,7 @@ public class ResultCallbackHandlerTest { @Test - void map() throws Throwable { + void mapWhenAllResultAreNotAlreadyGenerated() throws Throwable { LastExecutionAwareListener listener = new LastExecutionAwareListener(); MutableQueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); @@ -66,8 +66,10 @@ void map() throws Throwable { Row row2 = MockRow.builder().identified(0, String.class, "bar").build(); Row row3 = MockRow.builder().identified(0, String.class, "baz").build(); Result mockResult = MockResult.builder().row(row1, row2, row3).rowMetadata(MockRowMetadata.empty()).build(); + QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); + queriesExecutionCounter.addGeneratedResult(); - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig); + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); // map function to return the String value BiFunction mapBiFunction = (row, rowMetadata) -> row.get(0, String.class); @@ -94,6 +96,7 @@ void map() throws Throwable { assertThat(queryExecutionInfo.getThreadId()).isEqualTo(threadId); assertThat(queryExecutionInfo.getThreadName()).isEqualTo(threadName); assertThat(queryExecutionInfo.getThrowable()).isNull(); + assertThat(queriesExecutionCounter.areAllResultProcessed()).isFalse(); }) .assertNext(obj -> { // second assertThat(obj).isEqualTo("bar"); @@ -106,6 +109,7 @@ void map() throws Throwable { assertThat(queryExecutionInfo.getThreadId()).isEqualTo(threadId); assertThat(queryExecutionInfo.getThreadName()).isEqualTo(threadName); assertThat(queryExecutionInfo.getThrowable()).isNull(); + assertThat(queriesExecutionCounter.areAllResultProcessed()).isFalse(); }) .assertNext(obj -> { // third assertThat(obj).isEqualTo("baz"); @@ -118,13 +122,16 @@ void map() throws Throwable { assertThat(queryExecutionInfo.getThreadId()).isEqualTo(threadId); assertThat(queryExecutionInfo.getThreadName()).isEqualTo(threadName); assertThat(queryExecutionInfo.getThrowable()).isNull(); + assertThat(queriesExecutionCounter.areAllResultProcessed()).isFalse(); }) .verifyComplete(); + assertThat(queriesExecutionCounter.areAllResultProcessed()).isTrue(); + assertThat(queryExecutionInfo.getProxyEventType()).isEqualTo(ProxyEventType.EACH_QUERY_RESULT).as("alert query has not be called"); } @Test - void mapWithPublisherException() throws Throwable { + void mapWithPublisherExceptionWhenAllResultAreNotAlreadyGenerated() throws Throwable { LastExecutionAwareListener listener = new LastExecutionAwareListener(); MutableQueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); @@ -138,7 +145,9 @@ void mapWithPublisherException() throws Throwable { Result mockResult = mock(Result.class); when(mockResult.map(any())).thenReturn(publisher); - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig); + QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); + + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); // since "mockResult.map()" is mocked, args can be anything as long as num of args matches to signature. Object[] args = new Object[]{null}; @@ -150,6 +159,7 @@ void mapWithPublisherException() throws Throwable { long threadId = Thread.currentThread().getId(); String threadName = Thread.currentThread().getName(); + queriesExecutionCounter.addGeneratedResult(); StepVerifier.create((Publisher) result) .expectSubscription() .consumeErrorWith(thrown -> { @@ -159,7 +169,6 @@ void mapWithPublisherException() throws Throwable { assertThat(listener.getEachQueryResultExecutionInfo()).isSameAs(queryExecutionInfo); - // verify EACH_QUERY_RESULT assertThat(queryExecutionInfo.getProxyEventType()).isEqualTo(ProxyEventType.EACH_QUERY_RESULT); assertThat(queryExecutionInfo.getCurrentResultCount()).isEqualTo(1); assertThat(queryExecutionInfo.getCurrentMappedResult()).isNull(); @@ -168,7 +177,7 @@ void mapWithPublisherException() throws Throwable { } @Test - void mapWithEmptyPublisher() throws Throwable { + void mapWithEmptyPublisherWhenAllResultAreNotAlreadyGenerated() throws Throwable { LastExecutionAwareListener listener = new LastExecutionAwareListener(); MutableQueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); @@ -183,8 +192,9 @@ void mapWithEmptyPublisher() throws Throwable { return null; }; + QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig); + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); // since "mockResult.map()" is mocked, args can be anything as long as num of args matches to signature. Object[] args = new Object[]{mapBiFunction}; @@ -201,9 +211,178 @@ void mapWithEmptyPublisher() throws Throwable { .isNull(); } + @Test + void mapWhenAllResultHasBeenGenerated() throws Throwable { + LastExecutionAwareListener listener = new LastExecutionAwareListener(); + + MutableQueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); + ProxyConfig proxyConfig = ProxyConfig.builder().listener(listener).build(); + + Row row1 = MockRow.builder().identified(0, String.class, "foo").build(); + Row row2 = MockRow.builder().identified(0, String.class, "bar").build(); + Row row3 = MockRow.builder().identified(0, String.class, "baz").build(); + Result mockResult = MockResult.builder().row(row1, row2, row3).rowMetadata(MockRowMetadata.empty()).build(); + + QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); + queriesExecutionCounter.addGeneratedResult(); + queriesExecutionCounter.allResultHasBeenGenerated(); + + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); + + // map function to return the String value + BiFunction mapBiFunction = (row, rowMetadata) -> row.get(0, String.class); + + Object[] args = new Object[]{mapBiFunction}; + Object result = callback.invoke(mockResult, MAP_METHOD, args); + + assertThat(result) + .isInstanceOf(Publisher.class); + + long threadId = Thread.currentThread().getId(); + String threadName = Thread.currentThread().getName(); + + StepVerifier.create((Publisher) result) + .expectSubscription() + .assertNext(obj -> { // first + assertThat(obj).isEqualTo("foo"); + assertThat(listener.getEachQueryResultExecutionInfo()).isSameAs(queryExecutionInfo); + + // verify EACH_QUERY_RESULT + assertThat(queryExecutionInfo.getProxyEventType()).isEqualTo(ProxyEventType.EACH_QUERY_RESULT); + assertThat(queryExecutionInfo.getCurrentResultCount()).isEqualTo(1); + assertThat(queryExecutionInfo.getCurrentMappedResult()).isEqualTo("foo"); + assertThat(queryExecutionInfo.getThreadId()).isEqualTo(threadId); + assertThat(queryExecutionInfo.getThreadName()).isEqualTo(threadName); + assertThat(queryExecutionInfo.getThrowable()).isNull(); + assertThat(queriesExecutionCounter.areAllResultProcessed()).isFalse(); + }) + .assertNext(obj -> { // second + assertThat(obj).isEqualTo("bar"); + assertThat(listener.getEachQueryResultExecutionInfo()).isSameAs(queryExecutionInfo); + + // verify EACH_QUERY_RESULT + assertThat(queryExecutionInfo.getProxyEventType()).isEqualTo(ProxyEventType.EACH_QUERY_RESULT); + assertThat(queryExecutionInfo.getCurrentResultCount()).isEqualTo(2); + assertThat(queryExecutionInfo.getCurrentMappedResult()).isEqualTo("bar"); + assertThat(queryExecutionInfo.getThreadId()).isEqualTo(threadId); + assertThat(queryExecutionInfo.getThreadName()).isEqualTo(threadName); + assertThat(queryExecutionInfo.getThrowable()).isNull(); + assertThat(queriesExecutionCounter.areAllResultProcessed()).isFalse(); + }) + .assertNext(obj -> { // third + assertThat(obj).isEqualTo("baz"); + assertThat(listener.getEachQueryResultExecutionInfo()).isSameAs(queryExecutionInfo); + + // verify EACH_QUERY_RESULT + assertThat(queryExecutionInfo.getProxyEventType()).isEqualTo(ProxyEventType.EACH_QUERY_RESULT); + assertThat(queryExecutionInfo.getCurrentResultCount()).isEqualTo(3); + assertThat(queryExecutionInfo.getCurrentMappedResult()).isEqualTo("baz"); + assertThat(queryExecutionInfo.getThreadId()).isEqualTo(threadId); + assertThat(queryExecutionInfo.getThreadName()).isEqualTo(threadName); + assertThat(queryExecutionInfo.getThrowable()).isNull(); + assertThat(queriesExecutionCounter.areAllResultProcessed()).isFalse(); + }) + .verifyComplete(); + + assertThat(queriesExecutionCounter.areAllResultProcessed()).isTrue(); + assertThat(queryExecutionInfo.getProxyEventType()).isEqualTo(ProxyEventType.AFTER_QUERY); + assertThat(queryExecutionInfo.getCurrentMappedResult()).isEqualTo(null); + assertThat(queryExecutionInfo.getCurrentMappedResult()).isEqualTo(null); + assertThat(queryExecutionInfo.getThreadId()).isEqualTo(threadId); + assertThat(queryExecutionInfo.getThreadName()).isEqualTo(threadName); + assertThat(queryExecutionInfo.getThrowable()).isNull(); + } + + @Test + void mapWithPublisherExceptionWhenAllHasBeenGenerated() throws Throwable { + LastExecutionAwareListener listener = new LastExecutionAwareListener(); + + MutableQueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); + ProxyConfig proxyConfig = ProxyConfig.builder().listener(listener).build(); + + + // return a publisher that throws exception at execution + Exception exception = new RuntimeException("map exception"); + TestPublisher publisher = TestPublisher.create().error(exception); + + Result mockResult = mock(Result.class); + when(mockResult.map(any())).thenReturn(publisher); + + QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); + + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); + + // since "mockResult.map()" is mocked, args can be anything as long as num of args matches to signature. + Object[] args = new Object[]{null}; + Object result = callback.invoke(mockResult, MAP_METHOD, args); + + assertThat(result).isInstanceOf(Publisher.class); + assertThat(result).isNotSameAs(publisher); + + long threadId = Thread.currentThread().getId(); + String threadName = Thread.currentThread().getName(); + + queriesExecutionCounter.addGeneratedResult(); + queriesExecutionCounter.allResultHasBeenGenerated(); + + StepVerifier.create((Publisher) result) + .expectSubscription() + .consumeErrorWith(thrown -> { + assertThat(thrown).isSameAs(exception); + }) + .verify(); + + assertThat(listener.getEachQueryResultExecutionInfo()).isSameAs(queryExecutionInfo); + + assertThat(queryExecutionInfo.getProxyEventType()).isEqualTo(ProxyEventType.AFTER_QUERY); + assertThat(queryExecutionInfo.getCurrentMappedResult()).isEqualTo(null); + assertThat(queryExecutionInfo.getCurrentMappedResult()).isNull(); + assertThat(queryExecutionInfo.getThreadId()).isEqualTo(threadId); + assertThat(queryExecutionInfo.getThreadName()).isEqualTo(threadName); + } + + @Test + void mapWithEmptyPublisherWhenAllResultHasBeenGenerated() throws Throwable { + LastExecutionAwareListener listener = new LastExecutionAwareListener(); + + MutableQueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); + ProxyConfig proxyConfig = ProxyConfig.builder().listener(listener).build(); + + // return empty result + Result mockResult = MockResult.builder().build(); + + QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); + queriesExecutionCounter.addGeneratedResult(); + queriesExecutionCounter.allResultHasBeenGenerated(); + + AtomicBoolean isCalled = new AtomicBoolean(); + BiFunction mapBiFunction = (row, rowMetadata) -> { + isCalled.set(true); + return null; + }; + + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); + + // since "mockResult.map()" is mocked, args can be anything as long as num of args matches to signature. + Object[] args = new Object[]{mapBiFunction}; + Object result = callback.invoke(mockResult, MAP_METHOD, args); + + assertThat(result).isInstanceOf(Publisher.class); + assertThat(isCalled).as("map function should not be called").isFalse(); + + StepVerifier.create((Publisher) result) + .expectSubscription() + .verifyComplete(); + + assertThat(listener.getAfterQueryExecutionInfo()).isSameAs(queryExecutionInfo); + assertThat(queryExecutionInfo.getProxyEventType()).isEqualTo(ProxyEventType.AFTER_QUERY); + assertThat(queryExecutionInfo.getCurrentMappedResult()).isEqualTo(null); + assertThat(queryExecutionInfo.getCurrentMappedResult()).isNull(); + } + @Test @SuppressWarnings("unchecked") - void mapWithResultThatErrorsAtExecutionTime() throws Throwable { + void mapWithResultThatErrorsAtExecutionTimeWhenAllResultAreNotAlreadyGenerated() throws Throwable { // call to the "map()" method returns a publisher that fails(errors) at execution time @@ -226,13 +405,16 @@ void mapWithResultThatErrorsAtExecutionTime() throws Throwable { throw exception; }; + QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig); + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); // since "mockResult.map()" is mocked, args can be anything as long as num of args matches to signature. Object[] args = new Object[]{mapBiFunction}; Object result = callback.invoke(mockResult, MAP_METHOD, args); + queriesExecutionCounter.addGeneratedResult(); + assertThat(result) .isInstanceOf(Publisher.class); @@ -251,6 +433,7 @@ void mapWithResultThatErrorsAtExecutionTime() throws Throwable { // verify callback assertThat(listener.getEachQueryResultExecutionInfo()).isSameAs(queryExecutionInfo).as( "listener should be called even consuming throws exception"); + assertThat(queriesExecutionCounter.areAllResultProcessed()).isTrue().as("there are only one result processing, so after .map all result are processed"); assertThat(queryExecutionInfo.getProxyEventType()).isEqualTo(ProxyEventType.EACH_QUERY_RESULT); assertThat(queryExecutionInfo.getCurrentResultCount()).isEqualTo(1); assertThat(queryExecutionInfo.getCurrentMappedResult()).isNull(); @@ -265,8 +448,9 @@ void unwrap() throws Throwable { Result mockResult = MockResult.empty(); MutableQueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); ProxyConfig proxyConfig = new ProxyConfig(); + QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig); + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); Object result = callback.invoke(mockResult, UNWRAP_METHOD, null); assertThat(result).isSameAs(mockResult); @@ -277,8 +461,9 @@ void getProxyConfig() throws Throwable { Result mockResult = MockResult.empty(); MutableQueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); ProxyConfig proxyConfig = new ProxyConfig(); + QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig); + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); Object result = callback.invoke(mockResult, GET_PROXY_CONFIG_METHOD, null); assertThat(result).isSameAs(proxyConfig); diff --git a/src/test/java/io/r2dbc/proxy/callback/StatementCallbackHandlerTest.java b/src/test/java/io/r2dbc/proxy/callback/StatementCallbackHandlerTest.java index 7200544d..9907aa2c 100644 --- a/src/test/java/io/r2dbc/proxy/callback/StatementCallbackHandlerTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/StatementCallbackHandlerTest.java @@ -26,9 +26,11 @@ import io.r2dbc.proxy.listener.LastExecutionAwareListener; import io.r2dbc.proxy.test.MockConnectionInfo; import io.r2dbc.proxy.test.MockStatementInfo; +import io.r2dbc.spi.Result; import io.r2dbc.spi.Statement; import io.r2dbc.spi.Wrapped; import io.r2dbc.spi.test.MockResult; +import io.r2dbc.spi.test.MockRow; import io.r2dbc.spi.test.MockStatement; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; @@ -280,6 +282,7 @@ void executeOperationWithBindByName() throws Throwable { } + @SuppressWarnings("unchecked") @Test void executeThenCancel() throws Throwable { LastExecutionAwareListener testListener = new LastExecutionAwareListener(); @@ -291,9 +294,10 @@ void executeThenCancel() throws Throwable { Statement statement = MockStatement.builder().result(MockResult.empty()).build(); StatementCallbackHandler callback = new StatementCallbackHandler(statement, statementInfo, connectionInfo, proxyConfig); - Object result = callback.invoke(statement, EXECUTE_METHOD, new Object[]{}); + Flux result = Flux.from((Publisher) callback.invoke(statement, EXECUTE_METHOD, new Object[]{})) + .flatMap(r -> Flux.from(r.map((row, metadata) -> row)).then(Mono.just(r))); - StepVerifier.create((Publisher) result) + StepVerifier.create(result) .expectSubscription() .expectNextCount(1) .thenCancel()// cancel after consuming one result @@ -305,7 +309,6 @@ void executeThenCancel() throws Throwable { assertThat(afterQueryInfo.isSuccess()) .as("Consuming at least one result is considered to query execution success") .isTrue(); - } @Test @@ -335,6 +338,7 @@ void executeThenImmediatelyCancel() throws Throwable { } + @SuppressWarnings("unchecked") @Test void executeThenNext() throws Throwable { LastExecutionAwareListener testListener = new LastExecutionAwareListener(); @@ -349,7 +353,7 @@ void executeThenNext() throws Throwable { Object result = callback.invoke(statement, EXECUTE_METHOD, new Object[]{}); // Flux.next() cancels upstream publisher - Mono mono = ((Flux) result).next(); + Mono mono = ((Flux) result).next(); StepVerifier.create(mono) .expectSubscription() @@ -358,6 +362,37 @@ void executeThenNext() throws Throwable { QueryExecutionInfo afterQueryInfo = testListener.getAfterQueryExecutionInfo(); + assertThat(afterQueryInfo) + .as("Consuming one result without call .map or .getRowUpdated don't execute the query, so afterQuery is not called") + .isNull(); + } + + @SuppressWarnings("unchecked") + @Test + void executeThenNextWithMapCallOnResult() throws Throwable { + LastExecutionAwareListener testListener = new LastExecutionAwareListener(); + + ConnectionInfo connectionInfo = mock(ConnectionInfo.class); + StatementInfo statementInfo = MockStatementInfo.builder().updatedQuery("QUERY").build(); + ProxyConfig proxyConfig = ProxyConfig.builder().listener(testListener).build(); + + Statement statement = MockStatement.builder().result(MockResult.empty()).build(); + StatementCallbackHandler callback = new StatementCallbackHandler(statement, statementInfo, connectionInfo, proxyConfig); + + Object result = callback.invoke(statement, EXECUTE_METHOD, new Object[]{}); + + // Flux.next() cancels upstream publisher + Mono mono = ((Flux) result) + .flatMap(r -> r.map((row, it) -> row)) + .next(); + + StepVerifier.create(mono) + .expectSubscription() + .expectNextCount(0).as("nothing on the flux since the result is empty. r.map return an empty publisher") + .verifyComplete(); + + QueryExecutionInfo afterQueryInfo = testListener.getAfterQueryExecutionInfo(); + assertThat(afterQueryInfo).isNotNull(); assertThat(afterQueryInfo.isSuccess()) .as("Consuming at least one result is considered to query execution success") From 1e0dfea5b636c3ca3f753034aa1b2e0124a315ff Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 30 Jun 2021 14:15:38 -0700 Subject: [PATCH 61/74] Polish and update contribution Rename `QueriesExecutionCounter` to `QueriesExecutionContext` and its methods. Move the logic calling `eachQueryResult` to a custom subscriber, `ResultInvocationSubscriber`. [#94] Signed-off-by: Tadaya Tsuyukubo --- .../callback/CallbackHandlerSupport.java | 9 +- .../r2dbc/proxy/callback/JdkProxyFactory.java | 5 +- .../io/r2dbc/proxy/callback/ProxyFactory.java | 10 +- .../callback/QueriesExecutionContext.java | 105 ++++++++++ .../callback/QueriesExecutionCounter.java | 54 ------ .../callback/QueryInvocationSubscriber.java | 39 ++-- .../proxy/callback/ResultCallbackHandler.java | 77 ++------ .../callback/ResultInvocationSubscriber.java | 179 ++++++++++++++++++ .../listener/ProxyExecutionListener.java | 4 +- .../callback/AfterQueryCallbackTest.java | 9 +- .../callback/CallbackHandlerSupportTest.java | 12 +- .../proxy/callback/JdkProxyFactoryTest.java | 4 +- .../r2dbc/proxy/callback/ProxyUtilsTest.java | 4 +- .../callback/ResultCallbackHandlerTest.java | 73 +++---- 14 files changed, 392 insertions(+), 192 deletions(-) create mode 100644 src/main/java/io/r2dbc/proxy/callback/QueriesExecutionContext.java delete mode 100644 src/main/java/io/r2dbc/proxy/callback/QueriesExecutionCounter.java create mode 100644 src/main/java/io/r2dbc/proxy/callback/ResultInvocationSubscriber.java diff --git a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java index 17d6a7ba..5c5b47fb 100644 --- a/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java +++ b/src/main/java/io/r2dbc/proxy/callback/CallbackHandlerSupport.java @@ -31,9 +31,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.time.Clock; -import java.time.Duration; -import java.time.Instant; import java.util.Arrays; import java.util.Set; import java.util.function.Consumer; @@ -213,16 +210,16 @@ protected Flux interceptQueryExecution(Publisher, ? extends Publisher> transformer = Operators.liftPublisher((pub, subscriber) -> - new QueryInvocationSubscriber(subscriber, executionInfo, proxyConfig, queriesExecutionCounter)); + new QueryInvocationSubscriber(subscriber, executionInfo, proxyConfig, queriesExecutionContext)); return Flux.from(publisher) .cast(Result.class) .transform(transformer) - .map(queryResult -> proxyFactory.wrapResult(queryResult, executionInfo, queriesExecutionCounter)); + .map(queryResult -> proxyFactory.wrapResult(queryResult, executionInfo, queriesExecutionContext)); } /** diff --git a/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java b/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java index fe53bb7e..5a43af4e 100644 --- a/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java +++ b/src/main/java/io/r2dbc/proxy/callback/JdkProxyFactory.java @@ -94,11 +94,12 @@ public Statement wrapStatement(Statement statement, StatementInfo statementInfo, } @Override - public Result wrapResult(Result result, QueryExecutionInfo queryExecutionInfo, QueriesExecutionCounter queriesExecutionCounter) { + public Result wrapResult(Result result, QueryExecutionInfo queryExecutionInfo, QueriesExecutionContext queriesExecutionContext) { Assert.requireNonNull(result, "result must not be null"); Assert.requireNonNull(queryExecutionInfo, "queryExecutionInfo must not be null"); + Assert.requireNonNull(queriesExecutionContext, "queriesExecutionContext must not be null"); - CallbackHandler logic = new ResultCallbackHandler(result, queryExecutionInfo, this.proxyConfig, queriesExecutionCounter); + CallbackHandler logic = new ResultCallbackHandler(result, queryExecutionInfo, this.proxyConfig, queriesExecutionContext); CallbackInvocationHandler invocationHandler = new CallbackInvocationHandler(logic); return createProxy(invocationHandler, Result.class, Wrapped.class, ConnectionHolder.class, ProxyConfigHolder.class); } diff --git a/src/main/java/io/r2dbc/proxy/callback/ProxyFactory.java b/src/main/java/io/r2dbc/proxy/callback/ProxyFactory.java index 9fa8b6cf..ec05eee3 100644 --- a/src/main/java/io/r2dbc/proxy/callback/ProxyFactory.java +++ b/src/main/java/io/r2dbc/proxy/callback/ProxyFactory.java @@ -17,8 +17,8 @@ package io.r2dbc.proxy.callback; import io.r2dbc.proxy.core.ConnectionInfo; -import io.r2dbc.proxy.core.StatementInfo; import io.r2dbc.proxy.core.QueryExecutionInfo; +import io.r2dbc.proxy.core.StatementInfo; import io.r2dbc.spi.Batch; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; @@ -80,12 +80,14 @@ public interface ProxyFactory { /** * Create a proxy {@link Result}. * - * @param result original result - * @param executionInfo executionInfo + * @param result original result + * @param executionInfo executionInfo + * @param queriesExecutionContext queries execution context * @return proxy result * @throws IllegalArgumentException if {@code result} is {@code null} * @throws IllegalArgumentException if {@code executionInfo} is {@code null} + * @throws IllegalArgumentException if {@code queriesExecutionContext} is {@code null} */ - Result wrapResult(Result result, QueryExecutionInfo executionInfo, QueriesExecutionCounter queriesExecutionCounter); + Result wrapResult(Result result, QueryExecutionInfo executionInfo, QueriesExecutionContext queriesExecutionContext); } diff --git a/src/main/java/io/r2dbc/proxy/callback/QueriesExecutionContext.java b/src/main/java/io/r2dbc/proxy/callback/QueriesExecutionContext.java new file mode 100644 index 00000000..d01e6952 --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/callback/QueriesExecutionContext.java @@ -0,0 +1,105 @@ +package io.r2dbc.proxy.callback; + +import io.r2dbc.spi.Result; + +import java.time.Clock; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +/** + * The context of queries execution. + *

+ * Holds the count of {@link Result} produced by {@code Statement#execute} and count that has + * consumed {@code Result#[map|getRowsUpdated]}. + * + * @author Thomas Deblock + * @author Tadaya Tsuyukubo + */ +public class QueriesExecutionContext { + + private static final AtomicIntegerFieldUpdater PRODUCED_COUNT_INCREMENTER = + AtomicIntegerFieldUpdater.newUpdater(QueriesExecutionContext.class, "resultProducedCount"); + + private static final AtomicIntegerFieldUpdater CONSUMED_COUNT_INCREMENTER = + AtomicIntegerFieldUpdater.newUpdater(QueriesExecutionContext.class, "resultConsumedCount"); + + /** + * Increment this count when a publisher from {@code Statement#execute} produced a {@link Result}. + * Accessed via {@link #PRODUCED_COUNT_INCREMENTER}. + */ + private volatile int resultProducedCount; + + /** + * Increment this count when a publisher from {@code Result#[map|getRowsUpdated]} is consumed. + * Accessed via {@link #CONSUMED_COUNT_INCREMENTER}. + */ + private volatile int resultConsumedCount; + + private final StopWatch stopWatch; + + private boolean allProduced; + + public QueriesExecutionContext(Clock clock) { + this.stopWatch = new StopWatch(clock); + } + + /** + * Increment the count of produced {@link Result} from {@code Statement#execute}. + */ + public void incrementProducedCount() { + PRODUCED_COUNT_INCREMENTER.incrementAndGet(this); + } + + /** + * Increment the count of consumptions from {@code Result#[map|getRowsUpdated]}. + */ + public void incrementConsumedCount() { + CONSUMED_COUNT_INCREMENTER.incrementAndGet(this); + } + + /** + * Retrieve the elapsed time from the stopwatch that has started by {@link #startStopwatch()}. + * + * @return duration from start + */ + public Duration getElapsedDuration() { + return this.stopWatch.getElapsedDuration(); + } + + /** + * Start the stopwatch. + */ + public void startStopwatch() { + this.stopWatch.start(); + } + + /** + * Whether the executed queries have finished and results are consumed. + *

+ * The query is considered finished when the publisher from {@code Statement#execute()} have produced + * {@link Result}s and those are consumed via {@code Result#getRowsUpdated} or {@code Result#map}. + * + * @return {@code true} if all {@code Result} are produced and consumed. + */ + public boolean isQueryFinished() { + return this.allProduced && isAllConsumed(); + } + + /** + * Whether currently all produced {@link Result}s are consumed. + * + * @return {@code true} if all produced {@link Result}s are consumed. + */ + public boolean isAllConsumed() { + return this.resultConsumedCount >= this.resultProducedCount; + } + + + /** + * When {@link QueryInvocationSubscriber} produced all {@link Result} objects. + */ + public void markAllProduced() { + this.allProduced = true; + } + +} diff --git a/src/main/java/io/r2dbc/proxy/callback/QueriesExecutionCounter.java b/src/main/java/io/r2dbc/proxy/callback/QueriesExecutionCounter.java deleted file mode 100644 index f43d1ead..00000000 --- a/src/main/java/io/r2dbc/proxy/callback/QueriesExecutionCounter.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.r2dbc.proxy.callback; - -import java.time.Duration; - -/** - * Utility class to know how many result are processing and if all result has been processed. - * - * @author Thomas Deblock - */ -public class QueriesExecutionCounter { - private int numberOfGeneratedResult; - private int numberOfProcessedResult; - private boolean allResultHasBeenGenerated; - private final StopWatch stopWatch; - - public QueriesExecutionCounter(StopWatch stopWatch) { - this.numberOfGeneratedResult = 0; - this.numberOfProcessedResult = 0; - this.allResultHasBeenGenerated = false; - this.stopWatch = stopWatch; - } - - public void addGeneratedResult() { - this.numberOfGeneratedResult++; - } - - public Duration getElapsedDuration() { - return this.stopWatch.getElapsedDuration(); - } - - public void queryStarted() { - this.stopWatch.start(); - } - - public void resultProcessed() { - this.numberOfProcessedResult++; - } - - public boolean isQueryEnded() { - return this.areAllResultProcessed() && this.areAllResultGenerated(); - } - - public boolean areAllResultProcessed() { - return this.numberOfProcessedResult >= this.numberOfGeneratedResult; - } - - public boolean areAllResultGenerated() { - return this.allResultHasBeenGenerated; - } - - public void allResultHasBeenGenerated() { - this.allResultHasBeenGenerated = true; - } -} diff --git a/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java b/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java index a615333c..27a66574 100644 --- a/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java +++ b/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java @@ -40,28 +40,31 @@ class QueryInvocationSubscriber implements CoreSubscriber, Subscription, private final ProxyExecutionListener listener; - private final QueriesExecutionCounter queriesExecutionCounter; + private final QueriesExecutionContext queriesExecutionContext; private Subscription subscription; - public QueryInvocationSubscriber(CoreSubscriber delegate, MutableQueryExecutionInfo executionInfo, ProxyConfig proxyConfig, QueriesExecutionCounter queriesExecutionCounter) { + private boolean resultProduced; + + public QueryInvocationSubscriber(CoreSubscriber delegate, MutableQueryExecutionInfo executionInfo, ProxyConfig proxyConfig, QueriesExecutionContext queriesExecutionContext) { this.delegate = delegate; this.executionInfo = executionInfo; this.listener = proxyConfig.getListeners(); - this.queriesExecutionCounter = queriesExecutionCounter; + this.queriesExecutionContext = queriesExecutionContext; } @Override public void onSubscribe(Subscription s) { this.subscription = s; - this.queriesExecutionCounter.queryStarted(); beforeQuery(); this.delegate.onSubscribe(this); } @Override public void onNext(Result result) { - this.queriesExecutionCounter.addGeneratedResult(); + this.queriesExecutionContext.incrementProducedCount(); + this.resultProduced = true; + this.executionInfo.setSuccess(true); this.delegate.onNext(result); } @@ -69,14 +72,21 @@ public void onNext(Result result) { public void onError(Throwable t) { this.executionInfo.setThrowable(t); this.executionInfo.setSuccess(false); - afterQuery(); + + // mark this publisher produced all Results + this.queriesExecutionContext.markAllProduced(); + if (this.queriesExecutionContext.isQueryFinished()) { + afterQuery(); + } + this.delegate.onError(t); } @Override public void onComplete() { - this.queriesExecutionCounter.allResultHasBeenGenerated(); - if (this.queriesExecutionCounter.isQueryEnded()) { + // mark this publisher produced all Results + this.queriesExecutionContext.markAllProduced(); + if (this.queriesExecutionContext.isQueryFinished()) { this.executionInfo.setSuccess(true); afterQuery(); } @@ -92,8 +102,13 @@ public void request(long n) { @Override public void cancel() { // do not determine success/failure by cancel - this.queriesExecutionCounter.allResultHasBeenGenerated(); - if (this.queriesExecutionCounter.isQueryEnded()) { + this.queriesExecutionContext.markAllProduced(); + if (this.queriesExecutionContext.isQueryFinished()) { + // When at least one element is emitted, consider query execution is success, even when + // the publisher is canceled. see https://github.com/r2dbc/r2dbc-proxy/issues/55 + if (this.resultProduced) { + this.executionInfo.setSuccess(true); + } afterQuery(); } this.subscription.cancel(); @@ -143,11 +158,13 @@ private void beforeQuery() { this.executionInfo.setCurrentMappedResult(null); this.executionInfo.setProxyEventType(ProxyEventType.BEFORE_QUERY); + this.queriesExecutionContext.startStopwatch(); + this.listener.beforeQuery(this.executionInfo); } private void afterQuery() { - this.executionInfo.setExecuteDuration(this.queriesExecutionCounter.getElapsedDuration()); + this.executionInfo.setExecuteDuration(this.queriesExecutionContext.getElapsedDuration()); this.executionInfo.setThreadName(Thread.currentThread().getName()); this.executionInfo.setThreadId(Thread.currentThread().getId()); this.executionInfo.setCurrentMappedResult(null); diff --git a/src/main/java/io/r2dbc/proxy/callback/ResultCallbackHandler.java b/src/main/java/io/r2dbc/proxy/callback/ResultCallbackHandler.java index 48866e8f..2c6e59ea 100644 --- a/src/main/java/io/r2dbc/proxy/callback/ResultCallbackHandler.java +++ b/src/main/java/io/r2dbc/proxy/callback/ResultCallbackHandler.java @@ -17,15 +17,15 @@ package io.r2dbc.proxy.callback; import io.r2dbc.proxy.core.ConnectionInfo; -import io.r2dbc.proxy.core.ProxyEventType; import io.r2dbc.proxy.core.QueryExecutionInfo; import io.r2dbc.proxy.util.Assert; import io.r2dbc.spi.Result; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import reactor.core.publisher.Operators; import java.lang.reflect.Method; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; /** * Proxy callback handler for {@link Result}. @@ -38,7 +38,7 @@ public final class ResultCallbackHandler extends CallbackHandlerSupport { private final MutableQueryExecutionInfo queryExecutionInfo; - private final QueriesExecutionCounter queriesExecutionCounter; + private final QueriesExecutionContext queriesExecutionContext; /** * Callback handler logic for {@link Result}. @@ -46,26 +46,27 @@ public final class ResultCallbackHandler extends CallbackHandlerSupport { * This constructor purposely uses {@link QueryExecutionInfo} interface for arguments instead of {@link MutableQueryExecutionInfo} implementation. * This way, creator of this callback handler ({@link ProxyFactory}) does not depend on {@link MutableQueryExecutionInfo} implementation. * - * @param result query result - * @param queryExecutionInfo query execution info - * @param proxyConfig proxy config - * @param queriesExecutionCounter queries execution counter + * @param result query result + * @param queryExecutionInfo query execution info + * @param proxyConfig proxy config + * @param queriesExecutionContext queries execution counter * @throws IllegalArgumentException if {@code result} is {@code null} * @throws IllegalArgumentException if {@code queryExecutionInfo} is {@code null} * @throws IllegalArgumentException if {@code proxyConfig} is {@code null} * @throws IllegalArgumentException if {@code queriesExecutionCounter} is {@code null} * @throws IllegalArgumentException if {@code queryExecutionInfo} is not an instance of {@link MutableQueryExecutionInfo} */ - public ResultCallbackHandler(Result result, QueryExecutionInfo queryExecutionInfo, ProxyConfig proxyConfig, QueriesExecutionCounter queriesExecutionCounter) { + public ResultCallbackHandler(Result result, QueryExecutionInfo queryExecutionInfo, ProxyConfig proxyConfig, QueriesExecutionContext queriesExecutionContext) { super(proxyConfig); this.result = Assert.requireNonNull(result, "result must not be null"); Assert.requireNonNull(queryExecutionInfo, "queryExecutionInfo must not be null"); this.queryExecutionInfo = Assert.requireType(queryExecutionInfo, MutableQueryExecutionInfo.class, "queryExecutionInfo must be MutableQueryExecutionInfo"); - Assert.requireNonNull(queriesExecutionCounter, "queriesExecutionCounter must not be null"); - this.queriesExecutionCounter = queriesExecutionCounter; + Assert.requireNonNull(queriesExecutionContext, "queriesExecutionContext must not be null"); + this.queriesExecutionContext = queriesExecutionContext; } @Override + @SuppressWarnings("unchecked") public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Assert.requireNonNull(proxy, "proxy must not be null"); Assert.requireNonNull(method, "method must not be null"); @@ -82,59 +83,11 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl Object invocationResult = proceedExecution(method, this.result, args, this.proxyConfig.getListeners(), connectionInfo, null); if ("map".equals(methodName) || "getRowsUpdated".equals(methodName)) { - AtomicInteger resultCount = new AtomicInteger(0); + Function, ? extends Publisher> transformer = + Operators.liftPublisher((pub, subscriber) -> + new ResultInvocationSubscriber(subscriber, this.queryExecutionInfo, this.proxyConfig, this.queriesExecutionContext)); - return Flux.from((Publisher) invocationResult) - .doOnEach(signal -> { - boolean proceed = signal.isOnNext() || signal.isOnError(); - if (!proceed) { - return; - } - - int count = resultCount.incrementAndGet(); - - if (signal.isOnNext()) { - Object mappedResult = signal.get(); - - this.queryExecutionInfo.setSuccess(true); - this.queryExecutionInfo.setCurrentResultCount(count); - this.queryExecutionInfo.setCurrentMappedResult(mappedResult); - this.queryExecutionInfo.setThrowable(null); - } else { - // onError - Throwable thrown = signal.getThrowable(); - this.queryExecutionInfo.setSuccess(false); - this.queryExecutionInfo.setCurrentResultCount(count); - this.queryExecutionInfo.setCurrentMappedResult(null); - this.queryExecutionInfo.setThrowable(thrown); - } - - this.queryExecutionInfo.setProxyEventType(ProxyEventType.EACH_QUERY_RESULT); - - String threadName = Thread.currentThread().getName(); - long threadId = Thread.currentThread().getId(); - this.queryExecutionInfo.setThreadName(threadName); - this.queryExecutionInfo.setThreadId(threadId); - - // callback - this.proxyConfig.getListeners().eachQueryResult(this.queryExecutionInfo); - }) - .switchIfEmpty(Flux.defer(() -> { - this.queryExecutionInfo.setSuccess(true); - return Flux.empty(); - })) - .doOnTerminate(() -> { - this.queriesExecutionCounter.resultProcessed(); - if (this.queriesExecutionCounter.isQueryEnded()) { - this.queryExecutionInfo.setExecuteDuration(this.queriesExecutionCounter.getElapsedDuration()); - this.queryExecutionInfo.setThreadName(Thread.currentThread().getName()); - this.queryExecutionInfo.setThreadId(Thread.currentThread().getId()); - this.queryExecutionInfo.setCurrentMappedResult(null); - this.queryExecutionInfo.setProxyEventType(ProxyEventType.AFTER_QUERY); - - this.proxyConfig.getListeners().afterQuery(this.queryExecutionInfo); - } - }); + return Flux.from((Publisher) invocationResult).transform(transformer); } return invocationResult; diff --git a/src/main/java/io/r2dbc/proxy/callback/ResultInvocationSubscriber.java b/src/main/java/io/r2dbc/proxy/callback/ResultInvocationSubscriber.java new file mode 100644 index 00000000..543308bb --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/callback/ResultInvocationSubscriber.java @@ -0,0 +1,179 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.callback; + +import io.r2dbc.proxy.core.ProxyEventType; +import io.r2dbc.proxy.listener.ProxyExecutionListener; +import io.r2dbc.spi.Result; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.core.Scannable; +import reactor.util.annotation.Nullable; + +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +/** + * Custom subscriber/subscription to on {@code Result#[map|getRowsUpdated]}.. + * + * @author Tadaya Tsuyukubo + * @see CallbackHandlerSupport#interceptQueryExecution(Publisher, MutableQueryExecutionInfo) + */ +class ResultInvocationSubscriber implements CoreSubscriber, Subscription, Scannable, Fuseable.QueueSubscription { + + private static final AtomicIntegerFieldUpdater RESULT_COUNT_INCREMENTER = + AtomicIntegerFieldUpdater.newUpdater(ResultInvocationSubscriber.class, "resultCount"); + + private final CoreSubscriber delegate; + + private final MutableQueryExecutionInfo executionInfo; + + private final ProxyExecutionListener listener; + + private final QueriesExecutionContext queriesExecutionContext; + + /** + * Accessed via {@link #RESULT_COUNT_INCREMENTER}. + */ + private volatile int resultCount; + + private Subscription subscription; + + public ResultInvocationSubscriber(CoreSubscriber delegate, MutableQueryExecutionInfo executionInfo, ProxyConfig proxyConfig, QueriesExecutionContext queriesExecutionContext) { + this.delegate = delegate; + this.executionInfo = executionInfo; + this.listener = proxyConfig.getListeners(); + this.queriesExecutionContext = queriesExecutionContext; + } + + @Override + public void onSubscribe(Subscription s) { + this.subscription = s; + this.delegate.onSubscribe(this); + } + + @Override + public void onNext(Object mappedResult) { + eachQueryResult(mappedResult, null); + this.delegate.onNext(mappedResult); + } + + @Override + public void onError(Throwable t) { + eachQueryResult(null, t); + + this.queriesExecutionContext.incrementConsumedCount(); + if (this.queriesExecutionContext.isQueryFinished()) { + afterQuery(); + } + + this.delegate.onError(t); + } + + @Override + public void onComplete() { + this.queriesExecutionContext.incrementConsumedCount(); + if (this.queriesExecutionContext.isQueryFinished()) { + afterQuery(); + } + + this.delegate.onComplete(); + } + + @Override + public void request(long n) { + this.subscription.request(n); + } + + @Override + public void cancel() { + // do not determine success/failure by cancel + this.queriesExecutionContext.incrementConsumedCount(); + if (this.queriesExecutionContext.isQueryFinished()) { + afterQuery(); + } + + this.subscription.cancel(); + } + + @Override + @Nullable + @SuppressWarnings("rawtypes") + public Object scanUnsafe(Attr key) { + if (key == Attr.ACTUAL) { + return this.delegate; + } + if (key == Attr.PARENT) { + return this.subscription; + } + return null; + } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public Result poll() { + return null; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + + } + + private void afterQuery() { + this.executionInfo.setExecuteDuration(this.queriesExecutionContext.getElapsedDuration()); + this.executionInfo.setThreadName(Thread.currentThread().getName()); + this.executionInfo.setThreadId(Thread.currentThread().getId()); + this.executionInfo.setCurrentMappedResult(null); + this.executionInfo.setProxyEventType(ProxyEventType.AFTER_QUERY); + + this.listener.afterQuery(this.executionInfo); + } + + private void eachQueryResult(@Nullable Object mappedResult, @Nullable Throwable throwable) { + this.executionInfo.setProxyEventType(ProxyEventType.EACH_QUERY_RESULT); + this.executionInfo.setThreadName(Thread.currentThread().getName()); + this.executionInfo.setThreadId(Thread.currentThread().getId()); + + this.executionInfo.setCurrentResultCount(RESULT_COUNT_INCREMENTER.incrementAndGet(this)); + this.executionInfo.setCurrentMappedResult(mappedResult); + if (throwable != null) { + this.executionInfo.setThrowable(throwable); + this.executionInfo.setSuccess(false); + } else { + this.executionInfo.setThrowable(null); + this.executionInfo.setSuccess(true); + } + + this.listener.eachQueryResult(this.executionInfo); + } +} diff --git a/src/main/java/io/r2dbc/proxy/listener/ProxyExecutionListener.java b/src/main/java/io/r2dbc/proxy/listener/ProxyExecutionListener.java index cdc42046..33cd03be 100644 --- a/src/main/java/io/r2dbc/proxy/listener/ProxyExecutionListener.java +++ b/src/main/java/io/r2dbc/proxy/listener/ProxyExecutionListener.java @@ -21,8 +21,6 @@ import io.r2dbc.spi.Batch; import io.r2dbc.spi.Statement; -import java.util.function.BiFunction; - /** * Listener interface that is called when proxy is invoked. * @@ -83,7 +81,7 @@ default void afterQuery(QueryExecutionInfo execInfo) { /** * Called on processing each query {@link io.r2dbc.spi.Result}. *

- * While processing query results with {@link io.r2dbc.spi.Result#map(BiFunction)}, this callback + * While processing query results {@link io.r2dbc.spi.Result}, this callback * is called per result. *

* {@link QueryExecutionInfo#getCurrentMappedResult()} contains the mapped result. diff --git a/src/test/java/io/r2dbc/proxy/callback/AfterQueryCallbackTest.java b/src/test/java/io/r2dbc/proxy/callback/AfterQueryCallbackTest.java index be99eb52..10161b1e 100644 --- a/src/test/java/io/r2dbc/proxy/callback/AfterQueryCallbackTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/AfterQueryCallbackTest.java @@ -42,7 +42,6 @@ /** * @author Tadaya Tsuyukubo */ - class AfterQueryCallbackTest { private static final Logger logger = LoggerFactory.getLogger(AfterQueryCallbackTest.class); @@ -165,15 +164,14 @@ void completeExecutePublisherWhileProcessingResultWithGetRowUpdated() { verifyListener(false, 0); // process the "getRowsUpdated" result - // "listener#eachQueryResult" won't be called for "getRowsUpdated" this.resultPublisherForGetRowsUpdated.next(100); - verifyListener(false, 0); + verifyListener(false, 1); this.executePublisher.complete(); - verifyListener(false, 0); + verifyListener(false, 1); this.resultPublisherForGetRowsUpdated.complete(); - verifyListener(true, 0); + verifyListener(true, 1); } @SuppressWarnings("unchecked") @@ -219,7 +217,6 @@ private ProxyFactory createProxyFactory() { } private void verifyListener(boolean isAfterQueryCalled, int eachQueryResultCount) { - assertThat(this.listener.isBeforeQueryCalled()).as("isBeforeQueryCalled").isTrue(); assertThat(this.listener.isAfterQueryCalled()).as("isAfterQueryCalled").isEqualTo(isAfterQueryCalled); assertThat(this.listener.getEachQueryResultCount()).as("eachQueryResultCount").isEqualTo(eachQueryResultCount); } diff --git a/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java b/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java index 16376857..3d4c93c1 100644 --- a/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/CallbackHandlerSupportTest.java @@ -100,7 +100,7 @@ void interceptQueryExecution() { // when it creates a proxy for Result Result mockResultProxy = MockResult.empty(); when(proxyFactory.wrapResult(any(), any(), any())).thenAnswer((args)-> { - ((QueriesExecutionCounter) args.getArgument(2)).resultProcessed(); + ((QueriesExecutionContext) args.getArgument(2)).incrementConsumedCount(); return mockResultProxy; }); @@ -213,8 +213,11 @@ void interceptQueryExecutionWithImmediateCancel() { Flux result = this.callbackHandlerSupport.interceptQueryExecution(resultPublisher, executionInfo); + + // Cancels immediately - StepVerifier.create(result) + StepVerifier.create(result.log()) + .expectSubscription() .thenCancel() .verify(); @@ -241,7 +244,7 @@ void interceptQueryExecutionWithMultipleResult() { // when it creates a proxy for Result Result mockResultProxy = mock(Result.class); when(proxyFactory.wrapResult(any(), any(), any())).thenAnswer((args)-> { - ((QueriesExecutionCounter) args.getArgument(2)).resultProcessed(); + ((QueriesExecutionContext) args.getArgument(2)).incrementConsumedCount(); return mockResultProxy; }); @@ -319,7 +322,8 @@ void interceptQueryExecutionWithEmptyResult() { when(this.proxyConfig.getListeners()).thenReturn(compositeListener); // produce multiple results - Flux publisher = Flux.empty() + Flux publisher = Flux.empty() + .ofType(Result.class) .doOnRequest(subscription -> { // this will be called AFTER listener.beforeQuery() but BEFORE emitting query result from this publisher. // verify BEFORE_QUERY diff --git a/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java b/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java index f01a5774..6e412eac 100644 --- a/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/JdkProxyFactoryTest.java @@ -76,7 +76,7 @@ void isProxy() { ConnectionInfo connectionInfo = MockConnectionInfo.empty(); StatementInfo statementInfo = MockStatementInfo.empty(); QueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); // need to be mutable - QueriesExecutionCounter queriesExecutionCounter = mock(QueriesExecutionCounter.class); + QueriesExecutionContext queriesExecutionContext = mock(QueriesExecutionContext.class); Object wrapped; wrapped = this.proxyFactory.wrapConnectionFactory(connectionFactory); @@ -103,7 +103,7 @@ void isProxy() { assertThat(wrapped).isInstanceOf(ConnectionHolder.class); assertThat(wrapped).isInstanceOf(ProxyConfigHolder.class); - wrapped = this.proxyFactory.wrapResult(result, queryExecutionInfo, queriesExecutionCounter); + wrapped = this.proxyFactory.wrapResult(result, queryExecutionInfo, queriesExecutionContext); assertThat(Proxy.isProxyClass(wrapped.getClass())).isTrue(); assertThat(wrapped).isInstanceOf(Wrapped.class); assertThat(wrapped).isInstanceOf(ConnectionHolder.class); diff --git a/src/test/java/io/r2dbc/proxy/callback/ProxyUtilsTest.java b/src/test/java/io/r2dbc/proxy/callback/ProxyUtilsTest.java index 0dfb0bf6..d1cf1bf3 100644 --- a/src/test/java/io/r2dbc/proxy/callback/ProxyUtilsTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/ProxyUtilsTest.java @@ -56,11 +56,11 @@ void unwrapConnection() { MutableQueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); queryExecutionInfo.setConnectionInfo(connectionInfo); - QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(new StopWatch(proxyConfig.getClock())); + QueriesExecutionContext queriesExecutionContext = new QueriesExecutionContext(proxyConfig.getClock()); Batch proxyBatch = proxyConfig.getProxyFactory().wrapBatch(originalBatch, connectionInfo); Statement proxyStatement = proxyConfig.getProxyFactory().wrapStatement(originalStatement, statementInfo, connectionInfo); - Result proxyResult = proxyConfig.getProxyFactory().wrapResult(originalResult, queryExecutionInfo, queriesExecutionCounter); + Result proxyResult = proxyConfig.getProxyFactory().wrapResult(originalResult, queryExecutionInfo, queriesExecutionContext); Optional result; diff --git a/src/test/java/io/r2dbc/proxy/callback/ResultCallbackHandlerTest.java b/src/test/java/io/r2dbc/proxy/callback/ResultCallbackHandlerTest.java index 23ebf46d..4c410c65 100644 --- a/src/test/java/io/r2dbc/proxy/callback/ResultCallbackHandlerTest.java +++ b/src/test/java/io/r2dbc/proxy/callback/ResultCallbackHandlerTest.java @@ -33,6 +33,7 @@ import reactor.test.publisher.TestPublisher; import java.lang.reflect.Method; +import java.time.Clock; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; @@ -66,10 +67,10 @@ void mapWhenAllResultAreNotAlreadyGenerated() throws Throwable { Row row2 = MockRow.builder().identified(0, String.class, "bar").build(); Row row3 = MockRow.builder().identified(0, String.class, "baz").build(); Result mockResult = MockResult.builder().row(row1, row2, row3).rowMetadata(MockRowMetadata.empty()).build(); - QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); - queriesExecutionCounter.addGeneratedResult(); + QueriesExecutionContext queriesExecutionContext = new QueriesExecutionContext(mock(Clock.class)); + queriesExecutionContext.incrementProducedCount(); - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionContext); // map function to return the String value BiFunction mapBiFunction = (row, rowMetadata) -> row.get(0, String.class); @@ -96,7 +97,7 @@ void mapWhenAllResultAreNotAlreadyGenerated() throws Throwable { assertThat(queryExecutionInfo.getThreadId()).isEqualTo(threadId); assertThat(queryExecutionInfo.getThreadName()).isEqualTo(threadName); assertThat(queryExecutionInfo.getThrowable()).isNull(); - assertThat(queriesExecutionCounter.areAllResultProcessed()).isFalse(); + assertThat(queriesExecutionContext.isAllConsumed()).isFalse(); }) .assertNext(obj -> { // second assertThat(obj).isEqualTo("bar"); @@ -109,7 +110,7 @@ void mapWhenAllResultAreNotAlreadyGenerated() throws Throwable { assertThat(queryExecutionInfo.getThreadId()).isEqualTo(threadId); assertThat(queryExecutionInfo.getThreadName()).isEqualTo(threadName); assertThat(queryExecutionInfo.getThrowable()).isNull(); - assertThat(queriesExecutionCounter.areAllResultProcessed()).isFalse(); + assertThat(queriesExecutionContext.isAllConsumed()).isFalse(); }) .assertNext(obj -> { // third assertThat(obj).isEqualTo("baz"); @@ -122,11 +123,11 @@ void mapWhenAllResultAreNotAlreadyGenerated() throws Throwable { assertThat(queryExecutionInfo.getThreadId()).isEqualTo(threadId); assertThat(queryExecutionInfo.getThreadName()).isEqualTo(threadName); assertThat(queryExecutionInfo.getThrowable()).isNull(); - assertThat(queriesExecutionCounter.areAllResultProcessed()).isFalse(); + assertThat(queriesExecutionContext.isAllConsumed()).isFalse(); }) .verifyComplete(); - assertThat(queriesExecutionCounter.areAllResultProcessed()).isTrue(); + assertThat(queriesExecutionContext.isAllConsumed()).isTrue(); assertThat(queryExecutionInfo.getProxyEventType()).isEqualTo(ProxyEventType.EACH_QUERY_RESULT).as("alert query has not be called"); } @@ -145,9 +146,9 @@ void mapWithPublisherExceptionWhenAllResultAreNotAlreadyGenerated() throws Throw Result mockResult = mock(Result.class); when(mockResult.map(any())).thenReturn(publisher); - QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); + QueriesExecutionContext queriesExecutionContext = new QueriesExecutionContext(mock(Clock.class)); - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionContext); // since "mockResult.map()" is mocked, args can be anything as long as num of args matches to signature. Object[] args = new Object[]{null}; @@ -159,7 +160,7 @@ void mapWithPublisherExceptionWhenAllResultAreNotAlreadyGenerated() throws Throw long threadId = Thread.currentThread().getId(); String threadName = Thread.currentThread().getName(); - queriesExecutionCounter.addGeneratedResult(); + queriesExecutionContext.incrementProducedCount(); StepVerifier.create((Publisher) result) .expectSubscription() .consumeErrorWith(thrown -> { @@ -192,9 +193,9 @@ void mapWithEmptyPublisherWhenAllResultAreNotAlreadyGenerated() throws Throwable return null; }; - QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); + QueriesExecutionContext queriesExecutionContext = new QueriesExecutionContext(mock(Clock.class)); - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionContext); // since "mockResult.map()" is mocked, args can be anything as long as num of args matches to signature. Object[] args = new Object[]{mapBiFunction}; @@ -223,11 +224,11 @@ void mapWhenAllResultHasBeenGenerated() throws Throwable { Row row3 = MockRow.builder().identified(0, String.class, "baz").build(); Result mockResult = MockResult.builder().row(row1, row2, row3).rowMetadata(MockRowMetadata.empty()).build(); - QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); - queriesExecutionCounter.addGeneratedResult(); - queriesExecutionCounter.allResultHasBeenGenerated(); + QueriesExecutionContext queriesExecutionContext = new QueriesExecutionContext(mock(Clock.class)); + queriesExecutionContext.incrementProducedCount(); + queriesExecutionContext.markAllProduced(); - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionContext); // map function to return the String value BiFunction mapBiFunction = (row, rowMetadata) -> row.get(0, String.class); @@ -254,7 +255,7 @@ void mapWhenAllResultHasBeenGenerated() throws Throwable { assertThat(queryExecutionInfo.getThreadId()).isEqualTo(threadId); assertThat(queryExecutionInfo.getThreadName()).isEqualTo(threadName); assertThat(queryExecutionInfo.getThrowable()).isNull(); - assertThat(queriesExecutionCounter.areAllResultProcessed()).isFalse(); + assertThat(queriesExecutionContext.isAllConsumed()).isFalse(); }) .assertNext(obj -> { // second assertThat(obj).isEqualTo("bar"); @@ -267,7 +268,7 @@ void mapWhenAllResultHasBeenGenerated() throws Throwable { assertThat(queryExecutionInfo.getThreadId()).isEqualTo(threadId); assertThat(queryExecutionInfo.getThreadName()).isEqualTo(threadName); assertThat(queryExecutionInfo.getThrowable()).isNull(); - assertThat(queriesExecutionCounter.areAllResultProcessed()).isFalse(); + assertThat(queriesExecutionContext.isAllConsumed()).isFalse(); }) .assertNext(obj -> { // third assertThat(obj).isEqualTo("baz"); @@ -280,11 +281,11 @@ void mapWhenAllResultHasBeenGenerated() throws Throwable { assertThat(queryExecutionInfo.getThreadId()).isEqualTo(threadId); assertThat(queryExecutionInfo.getThreadName()).isEqualTo(threadName); assertThat(queryExecutionInfo.getThrowable()).isNull(); - assertThat(queriesExecutionCounter.areAllResultProcessed()).isFalse(); + assertThat(queriesExecutionContext.isAllConsumed()).isFalse(); }) .verifyComplete(); - assertThat(queriesExecutionCounter.areAllResultProcessed()).isTrue(); + assertThat(queriesExecutionContext.isAllConsumed()).isTrue(); assertThat(queryExecutionInfo.getProxyEventType()).isEqualTo(ProxyEventType.AFTER_QUERY); assertThat(queryExecutionInfo.getCurrentMappedResult()).isEqualTo(null); assertThat(queryExecutionInfo.getCurrentMappedResult()).isEqualTo(null); @@ -308,9 +309,9 @@ void mapWithPublisherExceptionWhenAllHasBeenGenerated() throws Throwable { Result mockResult = mock(Result.class); when(mockResult.map(any())).thenReturn(publisher); - QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); + QueriesExecutionContext queriesExecutionContext = new QueriesExecutionContext(mock(Clock.class)); - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionContext); // since "mockResult.map()" is mocked, args can be anything as long as num of args matches to signature. Object[] args = new Object[]{null}; @@ -322,8 +323,8 @@ void mapWithPublisherExceptionWhenAllHasBeenGenerated() throws Throwable { long threadId = Thread.currentThread().getId(); String threadName = Thread.currentThread().getName(); - queriesExecutionCounter.addGeneratedResult(); - queriesExecutionCounter.allResultHasBeenGenerated(); + queriesExecutionContext.incrementProducedCount(); + queriesExecutionContext.markAllProduced(); StepVerifier.create((Publisher) result) .expectSubscription() @@ -351,9 +352,9 @@ void mapWithEmptyPublisherWhenAllResultHasBeenGenerated() throws Throwable { // return empty result Result mockResult = MockResult.builder().build(); - QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); - queriesExecutionCounter.addGeneratedResult(); - queriesExecutionCounter.allResultHasBeenGenerated(); + QueriesExecutionContext queriesExecutionContext = new QueriesExecutionContext(mock(Clock.class)); + queriesExecutionContext.incrementProducedCount(); + queriesExecutionContext.markAllProduced(); AtomicBoolean isCalled = new AtomicBoolean(); BiFunction mapBiFunction = (row, rowMetadata) -> { @@ -361,7 +362,7 @@ void mapWithEmptyPublisherWhenAllResultHasBeenGenerated() throws Throwable { return null; }; - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionContext); // since "mockResult.map()" is mocked, args can be anything as long as num of args matches to signature. Object[] args = new Object[]{mapBiFunction}; @@ -405,15 +406,15 @@ void mapWithResultThatErrorsAtExecutionTimeWhenAllResultAreNotAlreadyGenerated() throw exception; }; - QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); + QueriesExecutionContext queriesExecutionContext = new QueriesExecutionContext(mock(Clock.class)); - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionContext); // since "mockResult.map()" is mocked, args can be anything as long as num of args matches to signature. Object[] args = new Object[]{mapBiFunction}; Object result = callback.invoke(mockResult, MAP_METHOD, args); - queriesExecutionCounter.addGeneratedResult(); + queriesExecutionContext.incrementProducedCount(); assertThat(result) .isInstanceOf(Publisher.class); @@ -433,7 +434,7 @@ void mapWithResultThatErrorsAtExecutionTimeWhenAllResultAreNotAlreadyGenerated() // verify callback assertThat(listener.getEachQueryResultExecutionInfo()).isSameAs(queryExecutionInfo).as( "listener should be called even consuming throws exception"); - assertThat(queriesExecutionCounter.areAllResultProcessed()).isTrue().as("there are only one result processing, so after .map all result are processed"); + assertThat(queriesExecutionContext.isAllConsumed()).isTrue().as("there are only one result processing, so after .map all result are processed"); assertThat(queryExecutionInfo.getProxyEventType()).isEqualTo(ProxyEventType.EACH_QUERY_RESULT); assertThat(queryExecutionInfo.getCurrentResultCount()).isEqualTo(1); assertThat(queryExecutionInfo.getCurrentMappedResult()).isNull(); @@ -448,9 +449,9 @@ void unwrap() throws Throwable { Result mockResult = MockResult.empty(); MutableQueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); ProxyConfig proxyConfig = new ProxyConfig(); - QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); + QueriesExecutionContext queriesExecutionContext = new QueriesExecutionContext(mock(Clock.class)); - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionContext); Object result = callback.invoke(mockResult, UNWRAP_METHOD, null); assertThat(result).isSameAs(mockResult); @@ -461,9 +462,9 @@ void getProxyConfig() throws Throwable { Result mockResult = MockResult.empty(); MutableQueryExecutionInfo queryExecutionInfo = new MutableQueryExecutionInfo(); ProxyConfig proxyConfig = new ProxyConfig(); - QueriesExecutionCounter queriesExecutionCounter = new QueriesExecutionCounter(mock(StopWatch.class)); + QueriesExecutionContext queriesExecutionContext = new QueriesExecutionContext(mock(Clock.class)); - ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionCounter); + ResultCallbackHandler callback = new ResultCallbackHandler(mockResult, queryExecutionInfo, proxyConfig, queriesExecutionContext); Object result = callback.invoke(mockResult, GET_PROXY_CONFIG_METHOD, null); assertThat(result).isSameAs(proxyConfig); From 2c89e61a645013f9a6dac7bc5daacba7ef6e3060 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 30 Jun 2021 15:29:21 -0700 Subject: [PATCH 62/74] Extract afterQuery invocation Since `afterQuery` callback is invoked by `QueryInvocationSubscriber` and `ResultInvocationSubscriber`, extract the duplicated logic to the `AfterQueryCallbackInvoker` class. [#94] Signed-off-by: Tadaya Tsuyukubo --- .../callback/AfterQueryCallbackInvoker.java | 58 +++++++++++++++++++ .../callback/QueryInvocationSubscriber.java | 17 +++--- .../callback/ResultInvocationSubscriber.java | 11 ++-- 3 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 src/main/java/io/r2dbc/proxy/callback/AfterQueryCallbackInvoker.java diff --git a/src/main/java/io/r2dbc/proxy/callback/AfterQueryCallbackInvoker.java b/src/main/java/io/r2dbc/proxy/callback/AfterQueryCallbackInvoker.java new file mode 100644 index 00000000..66fe020d --- /dev/null +++ b/src/main/java/io/r2dbc/proxy/callback/AfterQueryCallbackInvoker.java @@ -0,0 +1,58 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.r2dbc.proxy.callback; + +import io.r2dbc.proxy.core.ProxyEventType; +import io.r2dbc.proxy.core.QueryExecutionInfo; +import io.r2dbc.proxy.listener.ProxyExecutionListener; + +/** + * Invoke {@link ProxyExecutionListener#afterQuery(QueryExecutionInfo)} callback. + *

+ * Extracted the logic to call "afterQuery" callback method. + * This is because gh-94 exhibits the need to put afterQuery callback invocation + * in both {@link QueryInvocationSubscriber} and {@link ResultInvocationSubscriber}. + * + * @author Tadaya Tsuyukubo + * @see QueryInvocationSubscriber + * @see ResultCallbackHandler + */ +class AfterQueryCallbackInvoker { + + private final MutableQueryExecutionInfo executionInfo; + + private final QueriesExecutionContext queriesExecutionContext; + + private final ProxyExecutionListener listener; + + public AfterQueryCallbackInvoker(MutableQueryExecutionInfo executionInfo, QueriesExecutionContext queriesExecutionContext, ProxyExecutionListener listener) { + this.executionInfo = executionInfo; + this.queriesExecutionContext = queriesExecutionContext; + this.listener = listener; + } + + public void afterQuery() { + this.executionInfo.setExecuteDuration(this.queriesExecutionContext.getElapsedDuration()); + this.executionInfo.setThreadName(Thread.currentThread().getName()); + this.executionInfo.setThreadId(Thread.currentThread().getId()); + this.executionInfo.setCurrentMappedResult(null); + this.executionInfo.setProxyEventType(ProxyEventType.AFTER_QUERY); + + this.listener.afterQuery(this.executionInfo); + } + +} diff --git a/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java b/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java index 27a66574..7668bf7e 100644 --- a/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java +++ b/src/main/java/io/r2dbc/proxy/callback/QueryInvocationSubscriber.java @@ -42,6 +42,8 @@ class QueryInvocationSubscriber implements CoreSubscriber, Subscription, private final QueriesExecutionContext queriesExecutionContext; + private final AfterQueryCallbackInvoker afterQueryCallbackInvoker; + private Subscription subscription; private boolean resultProduced; @@ -51,6 +53,7 @@ public QueryInvocationSubscriber(CoreSubscriber delegate, Mutabl this.executionInfo = executionInfo; this.listener = proxyConfig.getListeners(); this.queriesExecutionContext = queriesExecutionContext; + this.afterQueryCallbackInvoker = new AfterQueryCallbackInvoker(this.executionInfo, this.queriesExecutionContext, this.listener); } @Override @@ -152,6 +155,10 @@ public void clear() { } + private void afterQuery() { + this.afterQueryCallbackInvoker.afterQuery(); + } + private void beforeQuery() { this.executionInfo.setThreadName(Thread.currentThread().getName()); this.executionInfo.setThreadId(Thread.currentThread().getId()); @@ -163,14 +170,4 @@ private void beforeQuery() { this.listener.beforeQuery(this.executionInfo); } - private void afterQuery() { - this.executionInfo.setExecuteDuration(this.queriesExecutionContext.getElapsedDuration()); - this.executionInfo.setThreadName(Thread.currentThread().getName()); - this.executionInfo.setThreadId(Thread.currentThread().getId()); - this.executionInfo.setCurrentMappedResult(null); - this.executionInfo.setProxyEventType(ProxyEventType.AFTER_QUERY); - - this.listener.afterQuery(this.executionInfo); - } - } diff --git a/src/main/java/io/r2dbc/proxy/callback/ResultInvocationSubscriber.java b/src/main/java/io/r2dbc/proxy/callback/ResultInvocationSubscriber.java index 543308bb..477c927e 100644 --- a/src/main/java/io/r2dbc/proxy/callback/ResultInvocationSubscriber.java +++ b/src/main/java/io/r2dbc/proxy/callback/ResultInvocationSubscriber.java @@ -47,6 +47,8 @@ class ResultInvocationSubscriber implements CoreSubscriber, Subscription private final QueriesExecutionContext queriesExecutionContext; + private final AfterQueryCallbackInvoker afterQueryCallbackInvoker; + /** * Accessed via {@link #RESULT_COUNT_INCREMENTER}. */ @@ -59,6 +61,7 @@ public ResultInvocationSubscriber(CoreSubscriber delegate, MutableQueryE this.executionInfo = executionInfo; this.listener = proxyConfig.getListeners(); this.queriesExecutionContext = queriesExecutionContext; + this.afterQueryCallbackInvoker = new AfterQueryCallbackInvoker(this.executionInfo, this.queriesExecutionContext, this.listener); } @Override @@ -150,13 +153,7 @@ public void clear() { } private void afterQuery() { - this.executionInfo.setExecuteDuration(this.queriesExecutionContext.getElapsedDuration()); - this.executionInfo.setThreadName(Thread.currentThread().getName()); - this.executionInfo.setThreadId(Thread.currentThread().getId()); - this.executionInfo.setCurrentMappedResult(null); - this.executionInfo.setProxyEventType(ProxyEventType.AFTER_QUERY); - - this.listener.afterQuery(this.executionInfo); + this.afterQueryCallbackInvoker.afterQuery(); } private void eachQueryResult(@Nullable Object mappedResult, @Nullable Throwable throwable) { From 170ac759032fe4ba9561e218f977eb1dd94651cb Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Sun, 4 Jul 2021 14:43:15 -0700 Subject: [PATCH 63/74] Update changelog for 0.8.7.RELEASE Signed-off-by: Tadaya Tsuyukubo --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 692da2b3..579d08d8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ R2DBC Proxy Changelog ============================= +0.8.7.RELEASE +------------------ +* Fix "afterQuery" callback order #94 + 0.8.6.RELEASE ------------------ * Upgrade to Reactor Dysprosium SR20 #91 From 6834d7991ccb4b7d41fcd9e069245585e67e45f9 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Sun, 4 Jul 2021 14:51:23 -0700 Subject: [PATCH 64/74] Release 0.8.7.RELEASE [closes #98] Signed-off-by: Tadaya Tsuyukubo --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c573bf45..6f90f59b 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ io.r2dbc r2dbc-proxy - 0.8.7.BUILD-SNAPSHOT + 0.8.7.RELEASE jar Reactive Relational Database Connectivity - Proxy From 74839bb87b010d378942dcb49b1e5713221eb526 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Sun, 4 Jul 2021 14:52:35 -0700 Subject: [PATCH 65/74] Prepare next development iteration Signed-off-by: Tadaya Tsuyukubo --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6f90f59b..faec5471 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ io.r2dbc r2dbc-proxy - 0.8.7.RELEASE + 0.8.8.BUILD-SNAPSHOT jar Reactive Relational Database Connectivity - Proxy From 59c2b28c31a1f1e4470a2d44d6f8db76cc23cecd Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Mon, 5 Jul 2021 15:56:56 -0700 Subject: [PATCH 66/74] Fix compiler warnings in Java 11 Signed-off-by: Tadaya Tsuyukubo (cherry picked from commit 5e008f1df513f77ad985ed66c7c77f706506aa97) --- .../java/io/r2dbc/proxy/ProxyConnectionFactoryProvider.java | 2 +- .../r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/r2dbc/proxy/ProxyConnectionFactoryProvider.java b/src/main/java/io/r2dbc/proxy/ProxyConnectionFactoryProvider.java index 72a1c134..0c13071d 100644 --- a/src/main/java/io/r2dbc/proxy/ProxyConnectionFactoryProvider.java +++ b/src/main/java/io/r2dbc/proxy/ProxyConnectionFactoryProvider.java @@ -150,7 +150,7 @@ private void registerProxyListenerClassName(String proxyListenerClassName, Proxy private void registerProxyListenerClass(Class proxyListenerClass, ProxyConnectionFactory.Builder builder) { Object proxyListenerInstance; try { - proxyListenerInstance = proxyListenerClass.newInstance(); + proxyListenerInstance = proxyListenerClass.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new IllegalArgumentException(format("Could not instantiate %s", proxyListenerClass), e); } diff --git a/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java b/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java index bff0f302..3fd06fac 100644 --- a/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java +++ b/src/main/java/io/r2dbc/proxy/callback/ConnectionFactoryCallbackHandler.java @@ -40,7 +40,6 @@ public ConnectionFactoryCallbackHandler(ConnectionFactory connectionFactory, Pro this.connectionFactory = Assert.requireNonNull(connectionFactory, "connectionFactory must not be null"); } - @SuppressWarnings("unchecked") @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Assert.requireNonNull(proxy, "proxy must not be null"); @@ -76,6 +75,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl // Then, instead, use "doOnSuccess()" chained to this mono to call the "afterMethod" callback. // This way, "after-method" is performed before "resource-closure" return Mono.from(result) + .cast(Object.class) .transform(Operators.liftPublisher((publisher, subscriber) -> new ConnectionFactoryCreateMethodInvocationSubscriber(subscriber, executionInfo, proxyConfig))) .map(resultObj -> { From c4f49297fc1e7b6e82acdb2f7a494b3d6c68dc3c Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Tue, 6 Jul 2021 08:30:44 -0700 Subject: [PATCH 67/74] Add native image configurations [closes #100] Signed-off-by: Tadaya Tsuyukubo --- .../r2dbc-proxy/native-image.properties | 1 + .../io.r2dbc/r2dbc-proxy/proxy-config.json | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/main/resources/META-INF/native-image/io.r2dbc/r2dbc-proxy/native-image.properties create mode 100644 src/main/resources/META-INF/native-image/io.r2dbc/r2dbc-proxy/proxy-config.json diff --git a/src/main/resources/META-INF/native-image/io.r2dbc/r2dbc-proxy/native-image.properties b/src/main/resources/META-INF/native-image/io.r2dbc/r2dbc-proxy/native-image.properties new file mode 100644 index 00000000..8320e7b9 --- /dev/null +++ b/src/main/resources/META-INF/native-image/io.r2dbc/r2dbc-proxy/native-image.properties @@ -0,0 +1 @@ +Args = --initialize-at-build-time=io.r2dbc.spi,io.r2dbc.proxy diff --git a/src/main/resources/META-INF/native-image/io.r2dbc/r2dbc-proxy/proxy-config.json b/src/main/resources/META-INF/native-image/io.r2dbc/r2dbc-proxy/proxy-config.json new file mode 100644 index 00000000..d96cff34 --- /dev/null +++ b/src/main/resources/META-INF/native-image/io.r2dbc/r2dbc-proxy/proxy-config.json @@ -0,0 +1,31 @@ +[ + [ + "io.r2dbc.spi.ConnectionFactory", + "io.r2dbc.spi.Wrapped", + "io.r2dbc.proxy.callback.ProxyConfigHolder" + ], + [ + "io.r2dbc.spi.Connection", + "io.r2dbc.spi.Wrapped", + "io.r2dbc.proxy.callback.ConnectionHolder", + "io.r2dbc.proxy.callback.ProxyConfigHolder" + ], + [ + "io.r2dbc.spi.Batch", + "io.r2dbc.spi.Wrapped", + "io.r2dbc.proxy.callback.ConnectionHolder", + "io.r2dbc.proxy.callback.ProxyConfigHolder" + ], + [ + "io.r2dbc.spi.Statement", + "io.r2dbc.spi.Wrapped", + "io.r2dbc.proxy.callback.ConnectionHolder", + "io.r2dbc.proxy.callback.ProxyConfigHolder" + ], + [ + "io.r2dbc.spi.Result", + "io.r2dbc.spi.Wrapped", + "io.r2dbc.proxy.callback.ConnectionHolder", + "io.r2dbc.proxy.callback.ProxyConfigHolder" + ] +] From 74a494bc48b139a141f5e8c152f5e9f822a4bdd2 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Mon, 20 Sep 2021 18:18:13 -0700 Subject: [PATCH 68/74] Upgrade to SPI 0.8.6.RELEASE [resolves #104] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index faec5471..24ff2cf6 100644 --- a/pom.xml +++ b/pom.xml @@ -35,7 +35,7 @@ 1.2.3 UTF-8 UTF-8 - 0.8.5.RELEASE + 0.8.6.RELEASE Dysprosium-SR20 2.2.0.RELEASE 3.5.15 From 80cf659b7db8199e8678b239fdba49a848dbf03e Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Mon, 20 Sep 2021 18:21:34 -0700 Subject: [PATCH 69/74] Update changelog for 0.8.8.RELEASE --- CHANGELOG | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 579d08d8..2bf9d65b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ R2DBC Proxy Changelog ============================= +0.8.8.RELEASE +------------------ +* Upgrade to R2DBC SPI 0.8.6.RELEASE #104 +* Native image support #100 + 0.8.7.RELEASE ------------------ * Fix "afterQuery" callback order #94 From 681bd7bf19fff8c52b1038d1235cad05b4876583 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Tue, 21 Sep 2021 11:46:12 -0700 Subject: [PATCH 70/74] Upgrade to Reactor Dysprosium-SR23 [resolves #105] --- CHANGELOG | 1 + pom.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 2bf9d65b..6300aa04 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ R2DBC Proxy Changelog 0.8.8.RELEASE ------------------ +* Upgrade to Reactor Dysprosium SR23 #105 * Upgrade to R2DBC SPI 0.8.6.RELEASE #104 * Native image support #100 diff --git a/pom.xml b/pom.xml index 24ff2cf6..fbf95611 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,7 @@ UTF-8 UTF-8 0.8.6.RELEASE - Dysprosium-SR20 + Dysprosium-SR23 2.2.0.RELEASE 3.5.15 From 9f0e07ff046db11a032a8e7078b93edbc7fd80dc Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Tue, 21 Sep 2021 19:47:31 -0700 Subject: [PATCH 71/74] Upgrade dependencies - Assertj: `3.21.0` - Junit: `5.8.0` - Mockito: `3.12.4` - Spring Boot: `2.5.4` - Spring Hateoas: `1.3.4` --- pom.xml | 10 +++++----- .../io/r2dbc/proxy/util/ChangeLogReportGenerator.java | 6 ++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index fbf95611..5fbf51f3 100644 --- a/pom.xml +++ b/pom.xml @@ -28,17 +28,17 @@ https://github.com/r2dbc/r2dbc-proxy - 3.17.2 + 3.21.0 1.8 3.0.2 - 5.7.0 + 5.8.0 1.2.3 UTF-8 UTF-8 0.8.6.RELEASE Dysprosium-SR23 - 2.2.0.RELEASE - 3.5.15 + 2.5.4 + 3.12.4 @@ -158,7 +158,7 @@ org.springframework.hateoas spring-hateoas - 1.0.0.RELEASE + 1.3.4 test diff --git a/src/test/java/io/r2dbc/proxy/util/ChangeLogReportGenerator.java b/src/test/java/io/r2dbc/proxy/util/ChangeLogReportGenerator.java index adca0631..b4a64924 100644 --- a/src/test/java/io/r2dbc/proxy/util/ChangeLogReportGenerator.java +++ b/src/test/java/io/r2dbc/proxy/util/ChangeLogReportGenerator.java @@ -48,8 +48,7 @@ public static void main(String... args) { HttpEntity response = webClient // .get().uri(URI_TEMPLATE, MILESTONE_ID) // - .exchange() // - .flatMap(clientResponse -> clientResponse.toEntity(String.class)) // + .exchangeToMono(clientResponse -> clientResponse.toEntity(String.class)) // .block(Duration.ofSeconds(10)); boolean keepChecking = true; @@ -67,8 +66,7 @@ public static void main(String... args) { response = webClient // .get().uri(links.getRequiredLink(IanaLinkRelations.NEXT).expand().getHref()) // - .exchange() // - .flatMap(clientResponse -> clientResponse.toEntity(String.class)) // + .exchangeToMono(clientResponse -> clientResponse.toEntity(String.class)) // .block(Duration.ofSeconds(10)); } else { From 8f2b64f95d8cbb74e1f93dbd6f9c5ab2b3ae451f Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 22 Sep 2021 14:06:35 -0700 Subject: [PATCH 72/74] Upgrade to JUnit 5.8.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5fbf51f3..ae113765 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ 3.21.0 1.8 3.0.2 - 5.8.0 + 5.8.1 1.2.3 UTF-8 UTF-8 From e164381f19ea18baf2b5050f62e21bd221fd4fd2 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 22 Sep 2021 18:07:08 -0700 Subject: [PATCH 73/74] Release 0.8.8.RELEASE [closes #106] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ae113765..b05edcfd 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ io.r2dbc r2dbc-proxy - 0.8.8.BUILD-SNAPSHOT + 0.8.8.RELEASE jar Reactive Relational Database Connectivity - Proxy From acd7365a6437fbba60989ebab5558ec382e2c7d6 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Wed, 22 Sep 2021 18:08:45 -0700 Subject: [PATCH 74/74] Prepare next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b05edcfd..b3206012 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ io.r2dbc r2dbc-proxy - 0.8.8.RELEASE + 0.8.9.BUILD-SNAPSHOT jar Reactive Relational Database Connectivity - Proxy