Open
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)