0

I have a chain of Proxies for JDBC Connection, PreparedStatement and Statement.

ConnectionProxy:

public class HikariConnectionProxy implements InvocationHandler {
    private final HikariDataSource dataSource;
    private final Connection delegate;
    private boolean committed;

    public static Connection newInstance(HikariDataSource dataSource) throws SQLException {
        final Connection connection = dataSource.getConnection();
        return (Connection) Proxy.newProxyInstance(
                connection.getClass().getClassLoader(),
                connection.getClass().getInterfaces(),
                new HikariConnectionProxy(dataSource, connection)
        );
    }

    public static Connection newInstance(HikariDataSource dataSource, String username, String password) throws SQLException {
        final Connection connection = dataSource.getConnection(username, password);
        return (Connection) Proxy.newProxyInstance(
                connection.getClass().getClassLoader(),
                connection.getClass().getInterfaces(),
                new HikariConnectionProxy(dataSource, connection)
        );
    }

    private HikariConnectionProxy(HikariDataSource dataSource, Connection connection) throws SQLException {
        this.dataSource = dataSource;
        delegate = connection;
        checkAutoCommit();
    }

    private void checkAutoCommit() throws SQLException {
        if (!delegate.getAutoCommit()) {
            delegate.setAutoCommit(true);
        }
    }

    private void commit() throws SQLException {
        delegate.commit();
        committed = true;
    }

    private void close() throws SQLException {
      // some code here
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Void.TYPE == method.getReturnType()) {
            final String methodName = method.getName().toLowerCase();
            if ("close".equals(methodName)) {
                this.close();
                return null;
            } else if ("commit".equals(methodName)) {
                this.commit();
                return null;
            }
        } else if (PreparedStatement.class == method.getReturnType()) {
            return ServiceBusPreparedStatementProxy.newInstance((PreparedStatement) method.invoke(delegate, args));
        } else if (Statement.class == method.getReturnType()) {
            return ServiceBusStatementProxy.newInstance((Statement) method.invoke(delegate, args));
        }
        return method.invoke(delegate, args);
    }
}

StatementProxy:

public class ServiceBusStatementProxy implements InvocationHandler {
    final boolean toCheckUnsigned;
    final Statement delegate;

    public static Statement newInstance(Statement statement) throws SQLException {
        return (Statement) Proxy.newProxyInstance(
                statement.getClass().getClassLoader(),
                statement.getClass().getInterfaces(),
                new ServiceBusStatementProxy(statement, statement.getConnection().getMetaData().getDatabaseProductName())
        );
    }

    ServiceBusStatementProxy(Statement statement, String dbName) {
        String dbNameLowerCase = dbName.toLowerCase();
        toCheckUnsigned = dbNameLowerCase.contains("oracle") || dbNameLowerCase.contains("mysql") || dbNameLowerCase.contains("mariadb");
        delegate = statement;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return toCheckUnsigned && ResultSet.class == method.getReturnType()
                ? ServiceBusResultSetProxy.newInstance((ResultSet) method.invoke(delegate, args))
                : method.invoke(delegate, args);
    }
}

PreparedStatementProxy:

public class ServiceBusPreparedStatementProxy extends ServiceBusStatementProxy implements InvocationHandler {
    private static final Predicate<Runnable> isValidArithmetic = runnable -> {
        try {
            runnable.run();
            return true;
        } catch (ArithmeticException e) {
            return false;
        }
    };

    public static PreparedStatement newInstance(PreparedStatement preparedStatement) throws SQLException {
        return (PreparedStatement) Proxy.newProxyInstance(
                preparedStatement.getClass().getClassLoader(),
                preparedStatement.getClass().getInterfaces(),
                new ServiceBusPreparedStatementProxy(preparedStatement, preparedStatement.getConnection().getMetaData().getDatabaseProductName())
        );
    }

    private ServiceBusPreparedStatementProxy(Statement statement, String dbName) {
        super(statement, dbName);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (toCheckUnsigned && Void.TYPE == method.getReturnType()
                && args != null && args.length >= 3 && "setObject".equalsIgnoreCase(method.getName()) && args[1] != null) {
            Class<?> valueClass = args[1].getClass();
            if (BigInteger.class == valueClass) {
                BigInteger bigInt = (BigInteger) args[1];
                if (!isValidArithmetic.test(bigInt::longValueExact)) {
                    ((PreparedStatement) delegate).setString((Integer) args[0], bigInt.toString());
                    return null;
                }
            }
        }
        return super.invoke(proxy, method, args);
    }
}

When I use this connection with a method, that has @Transactional annotation, the transaction doesn't work. When I removed this proxy, everything goes fine with the same @Transactional method.

Is there any explanation for this and how to fix it?

Using this with this data source:

@Service
public class BaseHikariDataSourceFactory implements DataSourceFactory {

    @Value("${hikari.leak.detection.threshold:0}")
    private int leakDetectionThreshold;
    @Value("${hikari.minimum.idle:0}")
    private int minimumIdle;
    @Value("${hikari.idle.timeout:60000}")
    private int idleTimeout;
    @Value("${hikari.maximum.pool.size:10}")
    private int maximumPoolSize;

    @Override
    public DataSource createHikariDataSource(BaseHikariDataSourceParams params) {
        HikariConfig config = new HikariConfig();
        ExternalSystemParams externalSystemParams = params.getExternalSystemParams();

        config.setPoolName(params.getPoolName());
        config.setJdbcUrl(params.getJdbcUrl());
        config.setDriverClassName(externalSystemParams.getDriverClassName());
        config.setUsername(externalSystemParams.getUsername());
        config.setPassword(externalSystemParams.getPassword());
        config.setMaximumPoolSize(Optional.ofNullable(externalSystemParams.getMaximumPoolSize()).orElse(maximumPoolSize));
        config.setIdleTimeout(Optional.ofNullable(externalSystemParams.getIdleTimeout()).orElse(idleTimeout));
        config.setMinimumIdle(Optional.ofNullable(externalSystemParams.getMinimumIdle()).orElse(minimumIdle));
        config.setLeakDetectionThreshold(leakDetectionThreshold);
        config.setRegisterMbeans(true);
        if (externalSystemParams.getConnectionTestQuery() != null) {
            config.setConnectionTestQuery(externalSystemParams.getConnectionTestQuery());
        }
        if (externalSystemParams.getValidationTimeout() != null) {
            config.setValidationTimeout(externalSystemParams.getValidationTimeout());
        }
        return new HikariDataSourceProxyConnection(config);
    }

    @Override
    public DataSource createHikariDataSource(Share share, boolean isWorker) {
        HikariConfig config = new HikariConfig();
        config.setPoolName("Hikari CP " + (isWorker ? share.getAlias() + WORKERS_DS_POSTFIX : share.getAlias()));
        config.setDriverClassName(share.getDriver());
        config.setJdbcUrl(share.getUrl());
        config.setUsername(share.getLogin());
        config.setPassword(share.getPassword());
        config.setMaximumPoolSize(isWorker ? share.getMaxPoolSizeWorkers() : share.getMaxPoolSize());
        config.addDataSourceProperty("useCompression", share.isUseCompression());
        config.setLeakDetectionThreshold(leakDetectionThreshold);
        config.setRegisterMbeans(true);
        return new HikariDataSourceProxyConnection(config);
    }

    private static class HikariDataSourceProxyConnection extends HikariDataSource {
        private final HikariDataSource delegate;

        HikariDataSourceProxyConnection(HikariConfig configuration) {
            delegate = new HikariDataSource(configuration);
        }

        @Override
        public Connection getConnection() throws SQLException {
            return HikariConnectionProxy.newInstance(delegate);
        }

        @Override
        public Connection getConnection(String username, String password) throws SQLException {
            return HikariConnectionProxy.newInstance(delegate, username, password);
        }
    }
}
Mike
  • 347
  • 1
  • 2
  • 15
  • You are messing with auto commit... Don't. – M. Deinum Oct 14 '19 at 12:09
  • @M.Deinum I just check auto-commit when borrowing a connection from the pool. When I still use proxy connection but not proxy statement, everything is ok. – Mike Oct 14 '19 at 12:43
  • No you don't. You are explicitly setting it to `true` it isn't a check it is a set. When using transacitons you shouldn't set it to `true`. Also can you elaborate on what doesn't work? If you get an exception add the stacktrace. – M. Deinum Oct 14 '19 at 12:44
  • @M.Deinum removed this code and this didn't help at all... – Mike Oct 14 '19 at 13:03
  • At least it cleans the code and leaves the auto-commit as is. What about the second part of the question, what doesn't work? Also how do you create a proxy for Hikari with these invocation handlers? – M. Deinum Oct 14 '19 at 13:05
  • @M.Deinum When I want to update entity multiple times in a single transaction I get a deadlock. Looks like proxy statement creates new transaction every time. I have overridden HikariConnectionPool methods that return proxy connections. Deadlock disappear when my proxy connection returns common statements. – Mike Oct 14 '19 at 13:30
  • Could you add the clarification to your question as well as the way you have configured things and are overriding the methods. – M. Deinum Oct 14 '19 at 13:34
  • @M.Deinum just added Datasource example – Mike Oct 14 '19 at 13:47
  • This `DataSourceFactory` where does it come from? I would suggest using a `BeanPostProcessor` instead, that way you can have Spring Boot itself configure the `HikariDataSource` which you then wrap in your extension and replace it. I'm trying to eliminate all that custom code, just to rule out things. Is the deadlock happening on the Java side (`getConnection`) or is it due to a transaction? – M. Deinum Oct 14 '19 at 13:57
  • The issue has been solved with switching proxies to delegates. Too much boilerplate code, but it works. Any ideas why it doesn't work with proxies? – Mike Oct 15 '19 at 09:50
  • https://paste.ubuntu.com/p/s7JQgfQMTn/ – Mike Oct 15 '19 at 10:39

0 Answers0