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

Introduce TransactionExecutionListener callback API #27479

Closed
cdalexndr opened this issue Sep 27, 2021 · 15 comments
Closed

Introduce TransactionExecutionListener callback API #27479

cdalexndr opened this issue Sep 27, 2021 · 15 comments
Assignees
Labels
in: data Issues in data modules (jdbc, orm, oxm, tx) status: feedback-provided Feedback has been provided type: enhancement A general enhancement
Milestone

Comments

@cdalexndr
Copy link

Affects: 5.3.10

Please provide a method to register a listener to be notified when a transaction is created/committed/rolledback/finished.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Sep 27, 2021
@quaff
Copy link
Contributor

quaff commented Sep 28, 2021

You can wrap TransacationManager.

import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public class WrappedTransactionManager implements PlatformTransactionManager {

	private final PlatformTransactionManager underlying;

	@Override
	public TransactionStatus getTransaction(TransactionDefinition transactionDefinition) throws TransactionException {
		// TODO
		return this.underlying.getTransaction(transactionDefinition);
	}

	@Override
	public void commit(TransactionStatus transactionStatus) throws TransactionException {
		// TODO
		this.underlying.commit(transactionStatus);
	}

	@Override
	public void rollback(TransactionStatus transactionStatus) throws TransactionException {
		// TODO
		this.underlying.rollback(transactionStatus);
	}

}

@mdeinum
Copy link
Contributor

mdeinum commented Sep 28, 2021

You can register callbacks using the TransactionSynchronizationManager using the registerSynchronization method. This takes a TransactionSynchronization for which you can then implement the desired methods.

Another option is using the @TransactionalEventListener and make that run on the desired state.

@quaff
Copy link
Contributor

quaff commented Sep 28, 2021

@mdeinum TransactionSynchronization and @TransactionalEventListener does not know when transaction created.

@cdalexndr
Copy link
Author

cdalexndr commented Sep 28, 2021

PlatformTransactionManager.getTransaction can retrieve existing transaction, but returned value can be checked TransactionStatus.isNewTransaction() to handle new transactions.

@cdalexndr
Copy link
Author

It seems that commit/rollback is also called for stacked virtual transactions so I have to also check them:

public class WrappedTransactionManager implements PlatformTransactionManager {

	private final PlatformTransactionManager underlying;

	@Override
	public TransactionStatus getTransaction(TransactionDefinition transactionDefinition) throws TransactionException {
		TransactionStatus status = this.underlying.getTransaction(transactionDefinition);
                if(status.isNewTransaction()){
                   //TODO
                }
                return status;
	}

	@Override
	public void commit(TransactionStatus transactionStatus) throws TransactionException {
		this.underlying.commit(transactionStatus);
                if(transactionStatus.isNewTransaction()){
                   //TODO
                }
	}

	@Override
	public void rollback(TransactionStatus transactionStatus) throws TransactionException {
		this.underlying.rollback(transactionStatus);
                if(transactionStatus.isNewTransaction()){
                   //TODO
                }
	}
}

@cdalexndr
Copy link
Author

cdalexndr commented Sep 28, 2021

WARNING registering a simple wrapper is not enough:

    //copy from org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration
    @Bean
    public PlatformTransactionManager transactionManager(
            ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers ) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManagerCustomizers.ifAvailable( ( customizers ) -> customizers.customize( transactionManager ) );
        return new WrappedTransactionManager ( transactionManager );  //<- WRAPPER HERE
    }

The original JpaTransactionManager implements some interfaces (ResourceTransactionManager, BeanFactoryAware, InitializingBean) used after bean creation, so if the wrapper doesn't forward those calls, erroneous behavior will arise. For example: in the same transaction EntityManager using one connection and JOOQ using another connection, because afterPropertiesSet() method was not forwarded.

Now, the wrapper can be made compatible, but what if, in a new spring version, another spring managed interface is added to JpaTransactionManager? I have to check at every version upgrade to make the wrapper compatible. Same for the non-trivial bean creation method that is a copy of the spring managed one. This becomes a maintenance hell.

This wrapper is a WORKAROUND only, and a spring managed solution must be provided for this feature.

@quaff
Copy link
Contributor

quaff commented Sep 29, 2021

You can try BeanPostProcessor

	@Bean
	static BeanPostProcessor transactionManagerPostProcessor() {
		return new BeanPostProcessor() {

			@Override
			public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
				if (bean instanceof PlatformTransactionManager) {
					ProxyFactory pf = new ProxyFactory(bean);
					pf.addAdvice(new MethodInterceptor() {
						@Override
						public Object invoke(MethodInvocation invocation) throws Throwable {
							Method m = invocation.getMethod();
							if (m.getDeclaringClass() == PlatformTransactionManager.class) {
								//TODO
							}
							return invocation.proceed();
						}
					});
					bean = pf.getProxy();
				}
				return bean;
			}
		};
	}

@snicoll
Copy link
Member

snicoll commented Sep 29, 2021

@cdalexndr rather than asking for a technical solution like this, we'd very much prefer that you explain the use cases that you're trying to implement.

@snicoll snicoll added the status: waiting-for-feedback We need additional information before we can continue label Sep 29, 2021
@cdalexndr
Copy link
Author

cdalexndr commented Sep 29, 2021

My use case: I want to track database transactions (start-end) to dump them when the app health is DOWN, for debugging purposes.
I need to store postgres backend id together with the executing thread, so I can dump thread stacktrace with additional info like query and held/waiting database locks (query pg_stat_activity & pg_locks, filter by backend id).

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Sep 29, 2021
@rstoyanchev rstoyanchev added the in: data Issues in data modules (jdbc, orm, oxm, tx) label Nov 10, 2021
@Cavva79
Copy link

Cavva79 commented May 24, 2022

This issue maybe related to this one #18297.

@Nidhi-Tanwar14
Copy link

Can you try using following phases:-
BEFORE_COMMIT – The event will be handled before the transaction commit.
AFTER_COMPLETION – The event will be handled when the transaction has completed regardless of success.
AFTER_ROLLBACK – The event will be handled when the transaction has rolled back.
AFTER_COMMIT – The event will be handled when the transaction gets committed successfully.

@jhoeller jhoeller changed the title Transaction event listeners Transaction lifecycle listeners Aug 1, 2023
@jhoeller jhoeller added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Aug 1, 2023
@jhoeller jhoeller self-assigned this Aug 1, 2023
@jhoeller jhoeller added this to the 6.1.0-M4 milestone Aug 1, 2023
@jhoeller
Copy link
Contributor

jhoeller commented Aug 5, 2023

I'm introducing a dedicated TransactionExecutionListener contract, with such listeners to be registered on the transaction manager itself. There are before/afterBegin, before/afterCommit and before/afterRollback callbacks which are being triggered by the transaction manager whenever it operates on an actual transaction, receiving the current transaction as a TransactionExecution (the base interface for TransactionStatus as well as ReactiveTransaction) which comes with a few new introspection methods now which are intended for listener implementations.

This is effectively a callback interface for stateless listening to transaction creation/completion steps in a transaction manager. It is primarily meant for observation and statistics, not for resource management purposes where stateful transaction synchronizations are still the way to go. In contrast to transaction synchronizations, the transaction execution listener contract is commonly supported for thread-bound transactions as well as reactive transactions, with a common registration facility in the new ConfigurableTransactionManager interface.

@jhoeller jhoeller changed the title Transaction lifecycle listeners Transaction execution listeners Aug 6, 2023
@jhoeller
Copy link
Contributor

jhoeller commented Aug 6, 2023

I've settled on the term "transaction execution listener" for it, also naming the contract TransactionExecutionListener which goes nicely with the provided TransactionExecution argument in the callback methods. A more general "transaction listener" term can be confused with TransactionalApplicationListener and @TransactionalEventListener.

@jhoeller
Copy link
Contributor

jhoeller commented Aug 6, 2023

@snicoll @wilkinsona I suppose Boot could automatically inject any TransactionExecutionListener beans in the context into a PlatformTransactionManager or ReactiveTransactionManager that it configures, along the following lines:

@Bean
public JdbcTransactionManager transactionManager(DataSource dataSource, Collection<TransactionExecutionListener> transactionExecutionListeners) {
    JdbcTransactionManager tm = new JdbcTransactionManager();
    tm.setDataSource(dataSource);
    tm.setTransactionExecutionListeners(transactionExecutionListeners);
    return tm;
}

@wilkinsona
Copy link
Member

Thanks for the suggestion, @jhoeller. I've opened spring-projects/spring-boot#36770. Sorry for the noise of #31001 which I opened in the wrong tracker.

@sbrannen sbrannen changed the title Transaction execution listeners Introduce TransactionExecutionListener callback API Aug 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: data Issues in data modules (jdbc, orm, oxm, tx) status: feedback-provided Feedback has been provided type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

10 participants