Search code examples
javaspringspring-transactions

Forbid @Transactional in current thread in Spring


I'm looking for solution for the next problem. I use Spring and I have some executor's thread which should not be blocked (at least reading/writing database). There are several tasks which could be executed via this executor, I don't know which could use @Transactional method or any other ways to get access to database. I want to forbid open new transaction in executor's thread, how to do it?

Small example

@Component
public class Service {

    public void execute() {
        System.out.println("I don't need transaction to execute");
    }
}

@Component
public class Service2 {

    @Transactional
    public void execute() {
        System.out.println("I've opened new transaction!");
    }
}

@Component
public class NonTransactionalExecutor {

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    @Autowired
    private Service service;

    @@Autowired
    private Service2 service2;

    public void doInExecutor() {
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                // Is there anything I could do to get exception on service2.execute?
                service.execute();
                service2.execute();
            }
        });
    }
}

P.S. In real world when method is annotated with @Transactional it tries to open new transaction with current TransactionManager in database (I'm using Postgres). I want to get exception instead of database reads/writes.

   java.lang.Thread.State: RUNNABLE
    at java.net.SocketInputStream.socketRead0(Native Method)
    at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
    at java.net.SocketInputStream.read(SocketInputStream.java:171)
    at java.net.SocketInputStream.read(SocketInputStream.java:141)
    at org.postgresql.core.VisibleBufferedInputStream.readMore(VisibleBufferedInputStream.java:140)
    at org.postgresql.core.VisibleBufferedInputStream.ensureBytes(VisibleBufferedInputStream.java:109)
    at org.postgresql.core.VisibleBufferedInputStream.read(VisibleBufferedInputStream.java:67)
    at org.postgresql.core.PGStream.receiveChar(PGStream.java:293)
    at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:1936)
    at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:306)
    - locked <4cd92d89> (a org.postgresql.core.v3.QueryExecutorImpl)
    at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:441)
    at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:365)
    at org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:307)
    at org.postgresql.jdbc.PgStatement.executeCachedSql(PgStatement.java:293)
    at org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:270)
    at org.postgresql.jdbc.PgStatement.executeQuery(PgStatement.java:224)
    at com.mchange.v2.c3p0.impl.DefaultConnectionTester.activeCheckConnection(DefaultConnectionTester.java:286)
    at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool$1PooledConnectionResourcePoolManager.testPooledConnection(C3P0PooledConnectionPool.java:510)
    at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool$1PooledConnectionResourcePoolManager.testPooledConnection(C3P0PooledConnectionPool.java:464)
    at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool$1PooledConnectionResourcePoolManager.refurbishResourceOnCheckout(C3P0PooledConnectionPool.java:339)
    - locked <73ed3d84> (a java.lang.Object)
    at com.mchange.v2.resourcepool.BasicResourcePool.attemptRefurbishResourceOnCheckout(BasicResourcePool.java:1733)
    at com.mchange.v2.resourcepool.BasicResourcePool.checkoutResource(BasicResourcePool.java:558)
    at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutAndMarkConnectionInUse(C3P0PooledConnectionPool.java:758)
    at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutPooledConnection(C3P0PooledConnectionPool.java:685)
    at com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource.getConnection(AbstractPoolBackedDataSource.java:140)
    at org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl.getConnection(DatasourceConnectionProviderImpl.java:139)
    at org.hibernate.internal.AbstractSessionImpl$NonContextualJdbcConnectionAccess.obtainConnection(AbstractSessionImpl.java:380)
    at org.hibernate.engine.jdbc.internal.LogicalConnectionImpl.obtainConnection(LogicalConnectionImpl.java:228)
    at org.hibernate.engine.jdbc.internal.LogicalConnectionImpl.getConnection(LogicalConnectionImpl.java:171)
    at org.hibernate.internal.SessionImpl.connection(SessionImpl.java:450)
    at org.springframework.orm.hibernate4.HibernateTransactionManager.doBegin(HibernateTransactionManager.java:450)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:373)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:427)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:276)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208)

Solution

  • You could probably use the decorator pattern for this. Implement your own PlatformTransactionManager and pass your current manager to it as a delegate. In getTransaction, throw an exception if the current thread is not what you're expecting.

    public class MyTransactionManager implements PlatformTransactionManager {
        private final PlatformTransactionManager delegate;
    
        public MyTransactionManager(PlatformTransactionManager delegate) {
            this.delegate = delegate;
        }
    
        @Override
        public void commit(TransactionStatus status) {
            delegate.commit(status);
        }
    
        @Override
        public TransactionStatus getTransaction(TransactionDefinition definition) {
            if (Thread.getCurrentThread() == /*something*/) {
                return delegate.getTransaction(definition);
            }
            else {
                throw new RuntimeException();
            }
        }
    
        @Override
        public void rollback(TransactionStatus status) {
            delegate.rollback(status);
        }
    }