Git Product home page Git Product logo

r2dbc-proxy's Introduction

Reactive Relational Database Connectivity Proxy Framework Java CI with Maven Maven Central

This project contains the proxy framework of the R2DBC SPI. R2DBC is a Reactive Foundation project.

Code of Conduct

This project is governed by the R2DBC Code of Conduct. By participating, you are expected to uphold this code of conduct. Please report unacceptable behavior to [email protected].

Maven configuration

Artifacts can be found on Maven Central:

<dependency>
  <groupId>io.r2dbc</groupId>
  <artifactId>r2dbc-proxy</artifactId>
  <version>${version}</version>
</dependency>

If you'd rather like the latest snapshots of the upcoming major version, use our Maven snapshot repository and declare the appropriate dependency version.

<dependency>
  <groupId>io.r2dbc</groupId>
  <artifactId>r2dbc-proxy</artifactId>
  <version>${version}.BUILD-SNAPSHOT</version>
</dependency>

<repository>
  <id>sonatype-nexus-snapshots</id>
  <name>Sonatype OSS Snapshot Repository</name>
  <url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>

R2DBC SPI versions

R2DBC Proxy R2DBC SPI
1.0.x, 1.1.x 1.0.x
0.9.x 0.9.x
0.8.x 0.8.x

Documentation

Getting Started

Here shows how to create a proxy ConnectionFactory.

URL Connection Factory Discovery

ConnectionFactory connectionFactory = ConnectionFactories.get("r2dbc:proxy:<driver>//<host>:<port>>/<database>[?proxyListener=<fqdn>]");

Sample URLs:

# with driver
r2dbc:proxy:postgresql://localhost:5432/myDB?proxyListener=com.example.MyListener

# with pooling
r2dbc:proxy:pool:postgresql://localhost:5432/myDB?proxyListener=com.example.MyListener&maxIdleTime=PT60S

Programmatic Connection Factory Discovery

ConnectionFactory connectionFactory = ConnectionFactories.get(ConnectionFactoryOptions.builder()
   .option(DRIVER, "proxy")
   .option(PROTOCOL, "postgresql")
   .build());

Mono<Connection> connection = connectionFactory.create();

Supported Connection Factory Discovery options:

Option Description
driver Must be proxy
protocol Delegating connection factory driver
proxyListener Comma separated list of fully qualified proxy listener class names (Optional)

When programmatically ConnectionFactoryOptions are constructed, proxyListener option allows following values:

  • Comma separated list of fully qualified proxy listener class names
  • Proxy listener class
  • Proxy listener instance
  • Collection of above

Programmatic creation with ProxyConnectionFactory

ConnectionFactory original = ...

ConnectionFactory connectionFactory = ProxyConnectionFactory.builder(original)
    .onAfterQuery(queryInfo ->
        ...  // after query callback logic
    )
    .onBeforeMethod(methodInfo ->
        ...  // before method callback logic
    )
    .listener(...)  // add listener
    .build();

Publisher<? extends Connection> connectionPublisher = connectionFactory.create();

// Alternative: Creating a Mono using Project Reactor
Mono<Connection> connectionMono = Mono.from(connectionFactory.create());

Samples

r2dbc-proxy-samples repository contains sample listener implementations.

Getting Help

Having trouble with R2DBC? We'd love to help!

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 to see if someone has already reported the problem.
  • If the issue doesn't already exist, create a new issue.
  • 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. You also need JDK 1.8 and Docker to run integration tests.

 $ ./mvnw clean install

If you want to build with the regular mvn command, you will need Maven v3.5.0 or above.

Also see CONTRIBUTING.adoc if you wish to submit pull requests. Commits require Signed-off-by (git commit -s) to ensure Developer Certificate of Origin.

Building the documentation

Building the documentation uses maven asciidoctor plugin.

 $ ./mvnw clean exec:java@generate-micrometer-docs asciidoctor:process-asciidoc

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.

r2dbc-proxy's People

Contributors

deblockt avatar gregturn avatar marcingrzejszczak avatar mp911de avatar mrotteveel avatar nebhale avatar spring-operator avatar ttddyy avatar valery1707 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

r2dbc-proxy's Issues

Add ValueStore API

Currently, custom values can be stored in scope of method and query execution with these methods in MethodExecutionInfo and QueryExecutionInfo.

- void addCustomValue(String key, Object value);
- <T> T getCustomValue(String key, Class<T> type);

To add more scope(bind param, connection), create a new interface ValueStore that defines custom value handling methods.

Proxy mechanism to support converting bind marker

I am experimenting the decorator/proxy to convert custom bind markers to the one that target database can understand.

I have created bind-param-converter branch and added preliminary implementation.
Based on the change, I have created a sample impl that performs parameter conversion.

tl;dr

conn.createStatement("INSERT INTO names (first_name, last_name) VALUES (:FIRST:, :LAST:) ")
    .bind(":LAST:", "FOO")
    .bind(":IGNORE:", "will be ignored")
    .bind(":FIRST:", "foo")
    .execute()

becomes

Query:["INSERT INTO names (first_name, last_name) VALUES (?, ?) "] Bindings:[(foo,FOO)]

Implementation

There are two intercepting points to consider for bind parameter substitution.

  1. Connection#createStatement(sql)
    Here needs to convert the given query by replacing the custom placeholder to actual placeholder that target database can understand.
    e.g.: INSERT INTO names VALUES ( :first_name, :last_name ) => INSERT INTO names VALUES ( ?, ? )
    Additionally, construct a map that contains position information for placeholder to avoid parsing query at bind operations.
    e.g.: {"first_name":0,"last_name":1}
    Also, requires a way to pass this map to the bind operation callback.

  2. bind/bindNull operations
    When performing bind operations, callback should be able to alter the actual behavior including not performing binding at all.
    e.g.: stmt.bind(":first_name", "foo") => stmt.bind(0, "foo")

In the branch commit, BindParameterConverter is the interface for the above callbacks.

In the sample implementation, NamedBindParameterConverter.java covers following usecases:

  • Convert colon surrounded parameter(:name:) to ?
  • Pass the parameter-index map from onCreateStatement to onBind
  • Convert placeholder specific parameters to position index based bindings
  • When :IGNORE: is specified in bind key, skip performing actual bind. (Demonstrating skip behavior).

Need more polishing on the changes, but this is the first step for supporting bind marker replacement story in r2dbc-proxy.

cc @nebhale @mp911de

Self returning methods should return proxy

Following methods return the invoked object itself

  • Batch#add
  • All methods on Statement except execute

Currently, those self returning methods are returning actual result of invocation.
They should return invoked proxy object.

Improve ProxyConfig creation more user friendly

Currently, ProxyConnectionFactory and its builder are the entry point to wrap original ConnectionFactory, register listeners, and create proxy ConnectionFactory.

When other raw r2dbc objects(Connection, Batch, Statement, Result) need to participate r2dbc-proxy framework(wrap to proxy), ProxyFactory provides methods to wrap them.

ProxyFactory is obtained from ProxyConfig and currently it does not have builder API.

We need to provide a builder for ProxyConfig and gives similar experience to ProxyConnectionFactory.Builder when constructing ProxyConfig instance.

[r2dbc/r2dbc-spi#9]

Issue when use the proxy on a ConnectionPool

Bug Report

I found some errors running perf test using the proxy around a ConnectionPool

Versions

  • Driver: 0.8.RELEASE
  • Database: postgres
  • Java: 1.11
  • OS: ubuntu

Current Behavior

Using a proxy around a ConnectionPool do some error running perf test on my api.
We found some error like

  • java.lang.IndexOutOfBoundsException: Source emitted more than one item.
  • org.springframework.transaction.IllegalTransactionStateException: Transaction is already completed - do not call commit or rollback more than once per transaction
  • org.springframework.transaction.TransactionSystemException: Could not commit R2DBC transaction; nested exception is io.r2dbc.postgresql.ExceptionFactory$PostgresqlNonTransientResourceException: [34000] portal "B_986" does not exist

When I remove the proxy all is OK. Using the proxy on the ConnectionFactory instead of the ConnectionPool all is OK.

Table schema

This issue is not linked with a specific table.

Steps to reproduce

Create a Java API performing a get on a transaction using Spring Boot.
The proxy configuration is done using something like that;

@Configuration
public class R2dbcConfiguration {

    private final Tracer tracer;

    public R2dbcConfiguration(Tracer tracer) {
        this.tracer = tracer;
    }

    /**
     * this is a copy/past of org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryConfiguration.ConnectionPoolConnectionFactoryConfiguration
     * waiting an r2dbc zipkin implementation provided by Spring (see https://github.com/spring-projects-experimental/spring-boot-r2dbc/issues/71)
     */
    @Bean(destroyMethod = "dispose")
    public ConnectionPool withTracing(R2dbcProperties properties,
                                  List<ConnectionFactoryOptionsBuilderCustomizer> customizers) {
        ConnectionFactory connectionFactory =  ConnectionFactoryBuilder.create(properties).customize(customizers)
            .build();
        R2dbcProperties.Pool pool = properties.getPool();
        ConnectionPoolConfiguration.Builder builder = ConnectionPoolConfiguration.builder(connectionFactory)
            .maxSize(pool.getMaxSize()).initialSize(pool.getInitialSize()).maxIdleTime(pool.getMaxIdleTime());
        if (StringUtils.hasText(pool.getValidationQuery())) {
            builder.validationQuery(pool.getValidationQuery());
        }
        return new ConnectionPool(builder.build());
    }

    @Primary
    @Bean
    public ConnectionFactory connectionFactoryWithTracing(ConnectionFactory original) {
        return ProxyConnectionFactory.builder(original)
            .listener(new TracingExecutionListener(tracer))
            .build();
    }
}

Expected behavior/code

No exception like without using the proxy.

Use "*Info" instead of "Mono<*Info>" in builder of ProxyConnectionFactory

ProxyConnectionFactory.Builder API defines method/query callbacks as:

  • onBeforeMethod(Consumer<Mono<MethodExecutionInfo>> consumer)
  • onAfterMethod(Consumer<Mono<MethodExecutionInfo>> consumer)
  • onBeforeQuery(Consumer<Mono<QueryExecutionInfo>> consumer)
  • onAfterQuery(Consumer<Mono<QueryExecutionInfo>> consumer)
  • onEachQueryResult(Consumer<Mono<QueryExecutionInfo>> consumer)

Usage of Mono<*Info> is not appropriate since it requires implementor to subscribe it even though the actual value has already emitted.
Instead, they should be Consumer<*Info> to directly receive contextual object as ProxyExecutionListener API defines.

Fix not to cast "DefaultConnectionInfo" on "Connection#close"

Currently, when Connection#close() is performed, callback handler(ConnectionCallbackHandler)
casts the ConnectionInfo to its default implementation class DefaultConnectionInfo to update
connection closing info(setClosed(true)).

This obviously doesn't work if non default implementation of ConnectionInfo is used. (e.g.: when making Connection participate proxy framework via ProxyFactory).

Need to move setClosed() to ConnectionInfo interface, and do not rely on the implementaion class in callback handler.

Update LifeCycleExecutionListener dispatching logic to work with ByteBuddy interceptor

When JDK dynamic proxy is used for creating a proxy, the proxy invocation(InvocationHandler#invoke()) passes a Method object that corresponding to the interface of the proxy instance.
On the other hand, ByteBuddy invocation passes Method of the actually invoked class via @Origin Method parameter.

Currently, in LifeCycleExecutionListener, first set of method dispatching compares Class of passed Method#getDeclaringClass().
For JDK dynamic proxy, this class becomes interface class(e.g.: ConnectionFactory). However, for ByteBuddy case, this class becomes actual target class(e.g.: H2ConnectionFactory).
Therefore, following dispatching mechanism doesn't work:

Class<?> methodDeclaringClass = method.getDeclaringClass();

if (ConnectionFactory.class.equals(methodDeclaringClass)) {
    // ConnectionFactory methods
    if ("create".equals(methodName)) {
      ...
    }
} else if (Connection.class.equals(methodDeclaringClass) {
   ...
} else if (...)

This if-else logic needs to be updated to work with any case regardless of impl or interface Method are passed.

"QueryExecutionInfo#isSuccess" is reported as failure with "ReactiveCrudRepository#save"

From gitter discussion:
https://gitter.im/R2DBC/r2dbc?at=5e1c726865540a529a0c9366

I have reproduced the issue using H2 with ReactiveCrudRepository#save

@GetMapping("/insert")
//    @Transactional
public Mono<City> insert() {
  City city = new City("name", "country");
  return this.repository.save(city)
    .doFinally(signal -> {
      System.out.println("CONTROLLER signal=" + signal);
    });
  };

The method are called in following sequence:

[ 33] [before-method] io.r2dbc.spi.ConnectionFactory#create
[ 34] [after-method] io.r2dbc.spi.ConnectionFactory#create
[ 35] [before-method] io.r2dbc.spi.Connection#createStatement
[ 36] [after-method] io.r2dbc.spi.Connection#createStatement
[ 37] [before-method] io.r2dbc.spi.Statement#bind
[ 38] [after-method] io.r2dbc.spi.Statement#bind
[ 39] [before-method] io.r2dbc.spi.Statement#bind
[ 40] [after-method] io.r2dbc.spi.Statement#bind
[ 41] [before-method] io.r2dbc.spi.Statement#returnGeneratedValues
[ 42] [after-method] io.r2dbc.spi.Statement#returnGeneratedValues
[ 43] [before-query] Query:["INSERT INTO city (name, country) VALUES ($1, $2)"]
[ 44] [before-method] io.r2dbc.spi.Statement#execute
[ 45] [before-method] io.r2dbc.spi.Result#map
[ 46] [before-method] io.r2dbc.spi.Connection#close
[ 47] [after-method] io.r2dbc.spi.Connection#close
CONTROLLER signal=onComplete
[ 48] [after-method] io.r2dbc.spi.Result#map
[ 49] [after-method] io.r2dbc.spi.Statement#execute
[ 50] [after-query] Query:["INSERT INTO city (name, country) VALUES ($1, $2)"]

The (34), 48, 49 receives cancel signal.

According to @mp911de

save(โ€ฆ) calls either insert or update methods and takes the first() element of the result
See https://github.com/spring-projects/spring-data-r2dbc/blob/master/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java#L74-L97

cc/ @aravindtga @Squiry

Support comma separated "proxyListener" parameter in ConnectionFactoryProvider

Hi, this is a first-timers-only issue. This means we've worked to make it more legible to folks who either haven't contributed to our codebase before or even folks who haven't contributed to open source before.

If that's you, we're interested in helping you take the first step and can answer questions and help you out as you do. Note that we're especially interested in contributions from people from groups underrepresented in free and open source software!

If you have contributed before, consider leaving this one for someone new, and looking through our general ideal-for-contribution issues. Thanks!

Background

R2DBC's ConnectionFactories is the gateway to obtain a ConnectionFactory from a ConnectionFactoryOptions object. ConnectionFactoryOptions is a configuration object that holds the connection configuration in order to obtain a ConnectionFactory from the appropriate driver.

In r2dbc-proxy, io.r2dbc.proxy.ProxyConnectionFactoryProvider is the class that receives ConnectionFactoryOptions and produce ConnectionFactory.

Problem

ConnectionFactoryOptions can be constructed by parsing a connection URL string or by programmatically.

For example with this URL:

r2dbc:proxy:postgresql://user@pass:localhost:5432/test-db?proxyListener=com.example.ListenerA,com.example.ListenerB

The parser will generate an Option object where key is proxyListener and value is String of com.example.ListenerA,com.example.ListenerB.

Current implementation of io.r2dbc.proxy.ProxyConnectionFactoryProvider is NOT supporting comma separated list of class names when it handles proxyListener parameter.

Solution

io.r2dbc.proxy.ProxyConnectionFactoryProvider should accept comma separated String value for proxyListener parameter.

When proxyListener option value is a String, treat it as a comma separated list of listener class names. Iterate over each of those listener class names. They need to be instantiated and registered as proxy listeners.

Steps to fix

  • Claim this issue with a comment below and ask any clarifying questions you need.
  • Set up a repository locally following the Contributing Guidelines.
  • Try to fix the issue following the steps above.
  • Commit your changes and start a pull request.

Deliverables

  • Changed io.r2dbc.proxy.ProxyConnectionFactoryProvider
  • Changed unit test io.r2dbc.proxy.ProxyConnectionFactoryProviderTest
  • Changed README.md - description of connection discovery options

related PR: r2dbc/r2dbc-spi#48
related issue: r2dbc/r2dbc-spi#37

Allow sharing proxy handling logic

Proxy creation(ProxyFactory) and proxy handling logic(~CallbackHandler) are separated by design.
This is to allow different proxy mechanism can be applied and to reuse the same proxy handling logic.
Currently, ~CallbackHandler are scoped in package private and cannot be reused outside of the framework.
To facilitate different proxy mechanism to use the handler logic, these ~CallbackHandler classes need to be updated to public classes.

Make invocation to the original object in callback handler replaceable

Currently, when proxy object is called, callback handler performs reflective invocation on actual target instance. When ByteBuddy is used to intercept method calls, this causes a problem.

For example, if ConnectionFactory#create() is instrumented, first create() call gets intercepted. Then, ConnectionFactoryCallbackHandler reflectively calls the original object to get the result. This call will again get intercepted; so, it ends up with infinite loop of interception.
To avoid this loop, ByteBuddy provides @SuperCall annotation on Callable to retrieve actual invocation result.

By making target-method-call as a strategy and replaceable, for ByteBuddy case, it can create a strategy to retrieve actual result from @SuperCall Callable parameter instead of reflectively call on target instance.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.