Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Postgresql multi-host failover functionality #120

Closed
mangusbrother opened this issue Jun 18, 2019 · 17 comments
Closed

Support for Postgresql multi-host failover functionality #120

mangusbrother opened this issue Jun 18, 2019 · 17 comments
Assignees
Labels
status: ideal-for-contribution An issue that a contributor can help us with type: enhancement A general enhancement
Milestone

Comments

@mangusbrother
Copy link

Currently this is unable to support a master-slave scenario for postgresql

basically the equivalent of this:

jdbc:postgresql://postgres_master:5432,postgres_slave:5432/myDb

@mp911de mp911de added the type: enhancement A general enhancement label Sep 9, 2019
@mp911de
Copy link
Collaborator

mp911de commented Sep 9, 2019

Happy to review a pull request on how to make Client customizable for various clustering- and failover strategies.

@elvirium
Copy link

elvirium commented Oct 7, 2020

Hello, we have a problem with postgresql setting targetServerType. When we use it in our connection string, we get an exception
io.r2dbc.postgresql.ExceptionFactory$PostgresqlBadGrammarException: unrecognized configuration parameter "targetServerType"

@davecramer
Copy link
Member

Hello, we have a problem with postgresql setting targetServerType. When we use it in our connection string, we get an exception
io.r2dbc.postgresql.ExceptionFactory$PostgresqlBadGrammarException: unrecognized configuration parameter "targetServerType"

This does not exist as of yet. This is a JDBC thing

@kotoant
Copy link

kotoant commented Jan 22, 2021

Hi, I've just written FailoverConnectionFactoryProvider.
It works fine for me.

It works without any changes in ConnectionUrlParser, ConnectionFactoryOptions, ConnectionFactoryProvider implementation. It also works with any driver (not only postgresql).

UPD: fixed several issues in the code.
UPD2: enhanced the code.

package kotoant;

import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryMetadata;
import io.r2dbc.spi.ConnectionFactoryOptions;
import io.r2dbc.spi.ConnectionFactoryProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.Disposable;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/**
 * An implementation of {@link ConnectionFactory} for creating failover connections to delegated {@link ConnectionFactory}s.
 * <p> Usage:
 * <p> 1. Create file in resources directory (on classpath): META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider with the following line:<p>
 * kotoant.FailoverConnectionFactoryProvider
 * <p> 2. Update spring.r2dbc.url - add failover driver before last driver (for example: postgresql):<p>
 * url: r2dbc:pool:failover:postgresql://host1[:port1],host2[:port2]/database[?connectTimeout=PT5S]
 *
 * @author Anton Kotov
 */
public class FailoverConnectionFactoryProvider implements ConnectionFactoryProvider {

    /*
     * Driver option value.
     */
    public static final String FAILOVER_DRIVER = "failover";
    public static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(15);

    private static final String COLON = ":";
    private static final String COMMA = ",";

    @Override
    public ConnectionFactory create(ConnectionFactoryOptions connectionFactoryOptions) {

        String protocol = connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.PROTOCOL);
        if (protocol.trim().length() == 0) {
            throw new IllegalArgumentException(String.format("Protocol %s is not valid.", protocol));
        }
        String[] protocols = protocol.split(COLON, 2);
        String driverDelegate = protocols[0];

        // when protocol does NOT contain COLON, the length becomes 1
        String protocolDelegate = protocols.length == 2 ? protocols[1] : "";

        String host = connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.HOST);
        if (host.trim().length() == 0) {
            throw new IllegalArgumentException(String.format("Host %s is not valid.", host));
        }

        List<ConnectionFactory> connectionFactories = new ArrayList<>();

        String[] hosts = host.trim().split(COMMA);
        for (String hostDelegate : hosts) {

            String[] hostAndPort = hostDelegate.split(COLON, 2);
            hostDelegate = hostAndPort[0];

            // when hostAndPort does NOT contain COLON, the length becomes 1
            Integer portDelegate = hostAndPort.length == 2 ? Integer.parseInt(hostAndPort[1]) : null;

            ConnectionFactoryOptions.Builder newOptions = ConnectionFactoryOptions.builder()
                    .from(connectionFactoryOptions)
                    .option(ConnectionFactoryOptions.DRIVER, driverDelegate)
                    .option(ConnectionFactoryOptions.PROTOCOL, protocolDelegate)
                    .option(ConnectionFactoryOptions.HOST, hostDelegate);

            if (portDelegate != null) {
                newOptions.option(ConnectionFactoryOptions.PORT, portDelegate);
            }

            // Run discovery again to find the actual connection factory
            ConnectionFactory connectionFactory = ConnectionFactories.find(newOptions.build());
            if (connectionFactory == null) {
                throw new IllegalArgumentException(String.format("Could not find delegating driver [%s]", driverDelegate));
            }

            connectionFactories.add(connectionFactory);
        }

        if (connectionFactories.isEmpty()) {
            throw new IllegalArgumentException(String.format("Host %s is not valid.", host));
        }
        if (connectionFactories.size() == 1) {
            return connectionFactories.get(0);
        }
        return FailoverConnectionFactory.newBuilder()
                .connectTimeout(
                        Optional.ofNullable(toDuration(connectionFactoryOptions.getValue(ConnectionFactoryOptions.CONNECT_TIMEOUT)))
                                .orElse(DEFAULT_CONNECT_TIMEOUT)
                )
                .addConnectionFactories(connectionFactories)
                .build();
    }

    @Override
    public boolean supports(ConnectionFactoryOptions connectionFactoryOptions) {
        Objects.requireNonNull(connectionFactoryOptions, "connectionFactoryOptions must not be null");

        String driver = connectionFactoryOptions.getValue(ConnectionFactoryOptions.DRIVER);
        return driver != null && driver.equals(FAILOVER_DRIVER);
    }

    @Override
    public String getDriver() {
        return FAILOVER_DRIVER;
    }

    private static Duration toDuration(Object value) {
        if (value == null) {
            return null;
        }

        if (value instanceof Duration) {
            return ((Duration) value);
        }

        if (value instanceof String) {
            return Duration.parse(value.toString());
        }

        throw new IllegalArgumentException(String.format("Cannot convert value %s into Duration", value));
    }

    public static class FailoverConnectionFactory implements ConnectionFactory {

        private static final Logger logger = LoggerFactory.getLogger(FailoverConnectionFactory.class);

        private final List<ConnectionFactory> connectionFactories;
        private final AtomicInteger currentConnectionFactoryIndex = new AtomicInteger(0);
        private final Duration connectTimeout;
        private final Mono<Connection> create;

        public static Builder newBuilder() {
            return new Builder();
        }

        public Builder toBuilder() {
            return newBuilder()
                    .connectTimeout(this.connectTimeout)
                    .addConnectionFactories(this.connectionFactories);
        }

        @Override
        public Mono<Connection> create() {
            return create;
        }

        @Override
        public ConnectionFactoryMetadata getMetadata() {
            return connectionFactories.get(0).getMetadata();
        }

        private FailoverConnectionFactory(List<ConnectionFactory> connectionFactories, Duration connectTimeout) {
            int connectionFactoriesSize = Objects.requireNonNull(connectionFactories, "connectionFactories must not be null").size();
            if (connectionFactoriesSize < 2) {
                throw new IllegalArgumentException(String.format("connectionFactories size must not be less than 2: [%s]", connectionFactoriesSize));
            }
            this.connectionFactories = connectionFactories;
            Objects.requireNonNull(connectTimeout, "connectTimeout must not be null");
            if (connectTimeout.isNegative()) {
                throw new IllegalArgumentException("connectTimeout must not be negative");
            }
            this.connectTimeout = connectTimeout;

            int numRetries = this.connectionFactories.size() - 1;

            this.create = Mono.defer(this::createConnection).retry(numRetries);
        }

        private Mono<Connection> createConnection() {
            int index = currentConnectionFactoryIndex.get();
            logger.info("Trying to create connection... [index: {}]", index);

            ConnectionHolder holder = new ConnectionHolder();

            return Mono.from(this.connectionFactories.get(index).create())
                    .cast(Connection.class)
                    .doOnNext(holder::setConnection)
                    .timeout(connectTimeout)
                    .doOnNext(connection -> logger.info("Connection has been successfully created [index: {}]", index))
                    .doOnError(throwable -> {
                        holder.dispose();
                        currentConnectionFactoryIndex.compareAndSet(index, (index + 1) % this.connectionFactories.size());
                        logger.info("Failed to create connection [index: {}]", index, throwable);
                    });
        }

        // package-private for testing
        List<ConnectionFactory> getConnectionFactories() {
            return Collections.unmodifiableList(connectionFactories);
        }

        // package-private for testing
        Duration getConnectTimeout() {
            return connectTimeout;
        }

        public static class Builder {

            private final List<ConnectionFactory> connectionFactories = new ArrayList<>();
            private Duration connectTimeout = DEFAULT_CONNECT_TIMEOUT;

            public Builder connectTimeout(Duration connectTimeout) {
                Objects.requireNonNull(connectTimeout, "connectTimeout must not be null");
                if (connectTimeout.isNegative()) {
                    throw new IllegalArgumentException("connectTimeout must not be negative");
                }
                this.connectTimeout = connectTimeout;
                return this;
            }

            public Builder addConnectionFactory(ConnectionFactory connectionFactory) {
                connectionFactories.add(Objects.requireNonNull(connectionFactory, "connectionFactory must not be null"));
                return this;
            }

            public Builder addConnectionFactories(Collection<ConnectionFactory> connectionFactories) {
                Objects.requireNonNull(connectionFactories, "connectionFactories must not be null")
                        .forEach(this::addConnectionFactory);
                return this;
            }

            public FailoverConnectionFactory build() {
                if (connectionFactories.size() < 2) {
                    throw new IllegalArgumentException(String.format("connectionFactories size must not be less than 2: [%s]", connectionFactories.size()));
                }
                return new FailoverConnectionFactory(connectionFactories, connectTimeout);
            }
        }

        private static final class ConnectionHolder implements Disposable {
            private final AtomicBoolean disposed = new AtomicBoolean();
            private final AtomicReference<Connection> connection = new AtomicReference<>();

            public void setConnection(Connection connection) {
                if (!this.connection.compareAndSet(null, connection)) {
                    throw new IllegalStateException("Holder already contains connection");
                }
                if (isDisposed()) {
                    closeConnection();
                }
            }

            @Override
            public void dispose() {
                if (disposed.compareAndSet(false, true)) {
                    closeConnection();
                }
            }

            @Override
            public boolean isDisposed() {
                return disposed.get();
            }

            private void closeConnection() {
                Connection connection = this.connection.getAndSet(null);
                if (connection != null) {
                    connection.close();
                }
            }
        }
    }
}



@osya
Copy link

osya commented Apr 12, 2022

@kotoant What sense in "Create file in resources directory (on classpath): META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider with the following line:

* kotoant.FailoverConnectionFactoryProvider"? How does it work? Does it creates bean?

@kotoant
Copy link

kotoant commented Apr 12, 2022

@kotoant What sense in "Create file in resources directory (on classpath): META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider with the following line:

  • kotoant.FailoverConnectionFactoryProvider"? How does it work? Does it creates bean?

The internal implementation of the driver supports its extension by Service Provider Interface mechanism in Java. It doesn't create bean in terms of Spring or any other DI framework. It just tries to find name of the class with implementation of the interface io.r2dbc.spi.ConnectionFactoryProvider in the text file META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider, if it exists it tries to create the instance of it and use it for resolving particular providers by name. In my case this name is 'failover'.

In my example I use class FailoverConnectionFactoryProvider in package kotoant. If your class is in another package or it has another name, you should specify the correct full class name in file META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider.

@osya
Copy link

osya commented Apr 12, 2022

@kotoant thanks, I got it.

But if the class FailoverConnectionFactoryProvider is located in the library which used from some Java microservice (from separate repository). Then where the io.r2dbc.spi.ConnectionFactoryProvider file should be located - in the library or in the microservice repository?

We are using Spring. So is it possible to create FailoverConnectionFactoryProvider from regular Spring Configuration? Are there some drawbacks in comparison with the creating bean via ServiceLocator?

Currently it is not worked for me:

2022-04-12 18:38:40[main]ERROR o.s.boot.SpringApplication - Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'creditingController' defined in file [E:\Work\VSProjects\Innotech\mmba-workflow\mmba-workflow\build\classes\java\main\ru\vtb\mmba\workflow\controller\CreditingController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'operationServiceImpl' defined in file [E:\Work\VSProjects\Innotech\mmba-workflow\mmba-workflow\build\classes\java\main\ru\vtb\mmba\workflow\service\operation\OperationServiceImpl.class]: Unsatisfied dependency expressed through constructor parameter 8; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'documentNumeratorRepository' defined in file [E:\Work\VSProjects\Innotech\mmba-workflow\mmba-workflow\build\classes\java\main\ru\vtb\mmba\workflow\db\repository\DocumentNumeratorRepository.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'r2dbcDatabaseClient' defined in class path resource [org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryDependentConfiguration.class]: Unsatisfied dependency expressed through method 'r2dbcDatabaseClient' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'connectionFactory' defined in class path resource [org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations$PoolConfiguration$PooledConnectionFactoryConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.r2dbc.pool.ConnectionPool]: Factory method 'connectionFactory' threw exception; nested exception is java.util.ServiceConfigurationError: io.r2dbc.spi.ConnectionFactoryProvider: Provider ru.vtb.mmba.failover.FailoverConnectionFactoryProvider not found
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
	at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
	at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:64)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:730)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:412)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:302)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1290)
	at ru.vtb.mmba.workflow.WorkflowApplication.main(WorkflowApplication.java:10)
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'operationServiceImpl' defined in file [E:\Work\VSProjects\Innotech\mmba-workflow\mmba-workflow\build\classes\java\main\ru\vtb\mmba\workflow\service\operation\OperationServiceImpl.class]: Unsatisfied dependency expressed through constructor parameter 8; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'documentNumeratorRepository' defined in file [E:\Work\VSProjects\Innotech\mmba-workflow\mmba-workflow\build\classes\java\main\ru\vtb\mmba\workflow\db\repository\DocumentNumeratorRepository.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'r2dbcDatabaseClient' defined in class path resource [org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryDependentConfiguration.class]: Unsatisfied dependency expressed through method 'r2dbcDatabaseClient' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'connectionFactory' defined in class path resource [org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations$PoolConfiguration$PooledConnectionFactoryConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.r2dbc.pool.ConnectionPool]: Factory method 'connectionFactory' threw exception; nested exception is java.util.ServiceConfigurationError: io.r2dbc.spi.ConnectionFactoryProvider: Provider ru.vtb.mmba.failover.FailoverConnectionFactoryProvider not found
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
	at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
	at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1380)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1300)
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
	... 19 common frames omitted
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'documentNumeratorRepository' defined in file [E:\Work\VSProjects\Innotech\mmba-workflow\mmba-workflow\build\classes\java\main\ru\vtb\mmba\workflow\db\repository\DocumentNumeratorRepository.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'r2dbcDatabaseClient' defined in class path resource [org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryDependentConfiguration.class]: Unsatisfied dependency expressed through method 'r2dbcDatabaseClient' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'connectionFactory' defined in class path resource [org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations$PoolConfiguration$PooledConnectionFactoryConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.r2dbc.pool.ConnectionPool]: Factory method 'connectionFactory' threw exception; nested exception is java.util.ServiceConfigurationError: io.r2dbc.spi.ConnectionFactoryProvider: Provider ru.vtb.mmba.failover.FailoverConnectionFactoryProvider not found
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
	at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
	at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1380)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1300)
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
	... 33 common frames omitted
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'r2dbcDatabaseClient' defined in class path resource [org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryDependentConfiguration.class]: Unsatisfied dependency expressed through method 'r2dbcDatabaseClient' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'connectionFactory' defined in class path resource [org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations$PoolConfiguration$PooledConnectionFactoryConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.r2dbc.pool.ConnectionPool]: Factory method 'connectionFactory' threw exception; nested exception is java.util.ServiceConfigurationError: io.r2dbc.spi.ConnectionFactoryProvider: Provider ru.vtb.mmba.failover.FailoverConnectionFactoryProvider not found
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:541)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1352)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1195)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
	at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1380)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1300)
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
	... 47 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'connectionFactory' defined in class path resource [org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations$PoolConfiguration$PooledConnectionFactoryConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.r2dbc.pool.ConnectionPool]: Factory method 'connectionFactory' threw exception; nested exception is java.util.ServiceConfigurationError: io.r2dbc.spi.ConnectionFactoryProvider: Provider ru.vtb.mmba.failover.FailoverConnectionFactoryProvider not found
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:658)
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:638)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1352)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1195)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
	at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1380)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1300)
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
	... 61 common frames omitted
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.r2dbc.pool.ConnectionPool]: Factory method 'connectionFactory' threw exception; nested exception is java.util.ServiceConfigurationError: io.r2dbc.spi.ConnectionFactoryProvider: Provider ru.vtb.mmba.failover.FailoverConnectionFactoryProvider not found
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185)
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:653)
	... 75 common frames omitted
Caused by: java.util.ServiceConfigurationError: io.r2dbc.spi.ConnectionFactoryProvider: Provider ru.vtb.mmba.failover.FailoverConnectionFactoryProvider not found
	at java.base/java.util.ServiceLoader.fail(ServiceLoader.java:589)
	at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.nextProviderClass(ServiceLoader.java:1212)
	at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.hasNextService(ServiceLoader.java:1221)
	at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.hasNext(ServiceLoader.java:1265)
	at java.base/java.util.ServiceLoader$2.hasNext(ServiceLoader.java:1300)
	at java.base/java.util.ServiceLoader$3.hasNext(ServiceLoader.java:1385)
	at io.r2dbc.spi.ConnectionFactories.find(ConnectionFactories.java:110)
	at io.r2dbc.spi.ConnectionFactories.get(ConnectionFactories.java:142)
	at org.springframework.boot.r2dbc.ConnectionFactoryBuilder$OptionsCapableWrapper.buildAndWrap(ConnectionFactoryBuilder.java:203)
	at org.springframework.boot.r2dbc.ConnectionFactoryBuilder$PoolingAwareOptionsCapableWrapper.buildAndWrap(ConnectionFactoryBuilder.java:216)
	at org.springframework.boot.r2dbc.ConnectionFactoryBuilder.build(ConnectionFactoryBuilder.java:189)
	at org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryConfigurations.createConnectionFactory(ConnectionFactoryConfigurations.java:68)
	at org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryConfigurations$PoolConfiguration$PooledConnectionFactoryConfiguration.connectionFactory(ConnectionFactoryConfigurations.java:92)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154)
	... 76 common frames omitted

@kotoant
Copy link

kotoant commented Apr 12, 2022

@osya
It is not possible to create the class via regular spring configuration. Because r2dbc specification assumes that you can use any framework - not just spring, or you can use framework-less approach. So the developers of the specification created the only way to extend the functionality - and it is elegant and very commonly used way - many other frameworks use it (for example quarkus in their test library).

It does not matter where the file (with all required parent directories including META-INF/...) is located, because it should be on classpath in runtime. So you can add it to the classpath via java -cp ... but it works only when you use java with main class - without -jar option. If you use -jar, it means you have some uber-jar. All you need is to check that that uber-jar file contains the file META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider. The easiest way to do that is to unzip the jar file and check that in the root of the file you can see directory META-INF with subdirectory services - with text file io.r2dbc.spi.ConnectionFactoryProvider.

As I remember if you use maven-spring-boot plugin for creating the uber-jar file, if you put META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider file in your application module (not library) resources directory - then the directory META-INF with all subdirs and files will be placed in the target jar out-of-the box.

If you place the file in some library for reuse - I don't remember exactly - how it works - but you can test it the way I described above. Also I used workaround for other similar tasks - I used maven-shade-plugin for creating uber-jar. This plugin have different resource transformers - for your goal you need https://maven.apache.org/plugins/maven-shade-plugin/examples/resource-transformers.html#ServicesResourceTransformer . So if you have different dependency libraries with their own META-INF/services/... the plugin creates one directory with combined file - and everything will work. I used it without any issues. More over similar functionality is very useful if you use spring boot auto-configuration. Because it works via the same mechanism - but it tries to find auto-confifguration classes in file resources/META-INF/spring.factories. So it also very often task to combine different files form different dependency-libs in one file. For that purpose I used https://maven.apache.org/plugins/maven-shade-plugin/examples/resource-transformers.html#AppendingTransformer. You can see the examples under the link.

As I mentioned above - I'm not sure - whether spring-boot-plugin maven performs all required transformation out of the box (maybe It works - without any issues - you can check and let me and others know), but I'm sure that you can achieve the desired result with maven-shade-plugin.

You asked very basic questions. I suggest you to familiarize yourself with key concept of java and spring.

If you have any other questions I don't promise that I will answer you. I already told you so much with detailed explanations - so it is surprise for me that you still have issues with such basic questions :)

@osya
Copy link

osya commented Apr 13, 2022

@kotoant , thank your for your explanation. FailoverConnectionFactoryProvider just works. The io.r2dbc.spi.ConnectionFactoryProvider I put in the library (not in the microservice which use this library). And we use Gradle, not Maven. And we tried to create this bean via Spring JavaConfig, via @Order & @Service annotations, but without success

@harishvashistha
Copy link

@mp911de why you guys are not providing this feature in this reactive library. Do not you want your users to have failover cluster facility? Please ease the process and let this feature enabled in your library.

@harishvashistha
Copy link

@osya @kotoant is the workaround above you are using in your Production setup without any problem to solve the problem mentioned in the title of this issue?

Not sure when mainteners are going the solve this issue officially for all.

@osya
Copy link

osya commented May 28, 2022

@harishvashistha we are using this solution not yet in production. Currently it is testing in different our staging areas. But it will be in production I suppose after a month

kressi added a commit to kressi/r2dbc-postgresql that referenced this issue May 29, 2022
@mp911de mp911de closed this as completed in 4390792 Jun 1, 2022
mp911de added a commit that referenced this issue Jun 1, 2022
Refactor connection creation from composeable ConnectionStrategy into composeable ConnectionFunctions and a parameter-less ConnectionStrategy that holds all connection target details.

Refactor SSL fallback into ConnectionFunction as SSL is part of the initial handshake. Move startup options into ConnectionSettings.

Simplify sink subscriptions into Flux composition for easier synchronization of closed connections. Add duration style parser.

Add license headers and since tags, update documentation.

[#120][resolves #474][#203]

Signed-off-by: Mark Paluch <mpaluch@vmware.com>
@mp911de mp911de added this to the 1.0.0.M1 milestone Jun 1, 2022
@mp911de mp911de changed the title Postgresql failover cluster missing functionality Support for Postgresql multi-host failover functionality Jun 1, 2022
@kotoant
Copy link

kotoant commented Jun 1, 2022

@harishvashistha we are using the solution I provide in production without any issues.

@osya
Copy link

osya commented Jul 4, 2022

@kotoant In case of using Spring instead of <condition> ... throw new IllegalArgumentException(...) it is better to use corresponding method from org.springframework.util.Assert

@kotoant
Copy link

kotoant commented Jul 4, 2022

@kotoant In case of using Spring instead of <condition> ... throw new IllegalArgumentException(...) it is better to use corresponding method from org.springframework.util.Assert

Look at my code: it doesn't depend on any framework. It depends only on r2dbc, reactor and slf4j. So potential clients and users may use it without modifications - even if they don't use spring and whatever-framework.

Basically it is one of the main principles do develop client API: do not use extra dependencies.

@aliakmikh
Copy link

@kotoant Will your approach work for a pool of connections, for example for io.r2dbc.pool.ConnectionPool factory?

@kotoant
Copy link

kotoant commented Sep 12, 2023

@kotoant Will your approach work for a pool of connections, for example for io.r2dbc.pool.ConnectionPool factory?

@aliakmikh
Yes, it will work, but my approach is very simple: it just creates several connection factories (so I am not sure that under the hood all resources are reused and not duplicated: clients, etc). So it 100% works, but I am not sure that it is optimal.

If you need the solution for postgres, the team already provided official solution for the problem. As I remember it works for spi 1.0.0:

#474

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: ideal-for-contribution An issue that a contributor can help us with type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants