spring-projects / spring-projects/spring-framework
TransactionalEventListener(phase = BEFORE_COMMIT) not called if the event was fired by another transactional listener in the same BEFORE_COMMIT phase
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 60.2k
- Forks
- 38.8k
- Avg merge
- 5d 2h
- Merged PRs (30d)
- 27
Description
Description
An event published within the handling of a transactional event listener with its phase set to BEFORE_COMMIT will NOT be handled by another transactional event listener which is also assigned to the BEFORE_COMMIT phase.
Expected
If a BEFORE_COMMIT listener publishes an event listened to by another BEFORE_COMMIT listener, that event listener should get called in the already running before commit phase.
sequenceDiagram;
participant Component
participant EventPublisher
participant Transaction
participant A as Listener A
participant B as Listener B
Component-->>+Transaction: start
Component-->>EventPublisher: publish event A;
EventPublisher-->>Transaction: defer listener A execution to commit;
Component-->>Transaction: commit
Transaction-->>A: handle event
A-->>EventPublisher: publish event B;
EventPublisher-->>Transaction: defer listener B execution to commit;
Transaction-->>B: handle event
Transaction-->>-Component: committed
Actual
If a BEFORE_COMMIT listener publishes an event listened to by another BEFORE_COMMIT listener, that event listener does not get called and the event is "lost".
sequenceDiagram;
participant Component
participant EventPublisher
participant Transaction
participant A as Listener A
participant B as Listener B
Component-->>+Transaction: start
Component-->>EventPublisher: publish event A;
EventPublisher-->>Transaction: defer listener A execution to commit;
Component-->>Transaction: commit
Transaction-->>A: handle event
A-->>EventPublisher: publish event B;
EventPublisher-->>Transaction: defer listener B execution to commit;
Transaction-->>-Component: committed
Affected versions
Tested with Spring Boot 3.5 and 3.4. Both show the same behavior.
Reproduction
Small Spring Boot test for reproduction.
Maven POM:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>de.nimelrian</groupId>
<artifactId>before-commit-reproduction</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureJdbc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.event.ApplicationEvents;
import org.springframework.test.context.event.RecordApplicationEvents;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.transaction.support.TransactionTemplate;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@AutoConfigureJdbc
@RecordApplicationEvents
class DemoApplicationTests {
private static final Logger log = LoggerFactory.getLogger(DemoApplicationTests.class);
@Test
void while_committing_additionally_published_events_should_be_handled(
@Autowired TransactionTemplate txTemplate,
@Autowired ApplicationEventPublisher applicationEventPublisher,
@Autowired ApplicationEvents applicationEvents
) {
txTemplate.executeWithoutResult(tx -> {
log.info("Publishing EventA");
applicationEventPublisher.publishEvent(new EventA());
log.info("Committing transaction");
});
assertThat(applicationEvents.stream(EventA.class)).as("EventA should have been published").hasSize(1);
assertThat(applicationEvents.stream(EventB.class)).as("EventB should have been published by listener for EventA").hasSize(1);
assertThat(applicationEvents.stream(EventC.class)).as("EventC should have been published by listener for EventB").hasSize(1); // This fails
}
@Configuration
static class TestContextConfiguration {
private final ApplicationEventPublisher applicationEventPublisher;
TestContextConfiguration(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
void handleEventA(EventA eventA) {
log.info("handleEventA");
applicationEventPublisher.publishEvent(new EventB());
}
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
void handleEventB(EventB eventB) {
log.info("handleEventB");
applicationEventPublisher.publishEvent(new EventC());
}
}
record EventA() {
}
record EventB() {
}
record EventC() {
}
}
Log output:
INFO [demo] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
INFO [demo] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:9e2b7744-82f9-47aa-a3b3-5b866c6a8e06 user=SA
INFO [demo] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
INFO [demo] [ main] de.nimelrian.demo.DemoApplicationTests : Publishing EventA
INFO [demo] [ main] de.nimelrian.demo.DemoApplicationTests : Committing transaction
INFO [demo] [ main] de.nimelrian.demo.DemoApplicationTests : handleEventA
java.lang.AssertionError: [EventC should have been published by listener for EventB]
Expected size: 1 but was: 0 in:
[]
at de.nimelrian.demo.DemoApplicationTests.while_committing_additionally_published_events_should_be_handled(DemoApplicationTests.java:40)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Run the provided regression scenario and trace TransactionalEventListener handling for TransactionPhase.BEFORE_COMMIT when events are published through ApplicationEventPublisher during TransactionTemplate commit. Add or adjust a regression test so EventA leads to EventB and EventC, then verify the test passes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100