Rollback reactive transaction on cancel
This commit introduces a change in reactive transaction semantics for cancel signals. Canceling a subscription now rolls back a reactive transaction to achieve a deterministic transaction outcome. Previously, cancel signals committed a transaction which could cause partially committed transactions depending on when the cancel happened.
This commit is contained in:
committed by
Juergen Hoeller
parent
8853f4b883
commit
217b6e37a6
@@ -16,13 +16,19 @@
|
||||
|
||||
package org.springframework.transaction.annotation;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import io.vavr.control.Try;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.transaction.testfixture.CallCountingTransactionManager;
|
||||
import org.springframework.transaction.testfixture.ReactiveCallCountingTransactionManager;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -32,11 +38,14 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
/**
|
||||
* @author Rob Harrop
|
||||
* @author Juergen Hoeller
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class AnnotationTransactionInterceptorTests {
|
||||
|
||||
private final CallCountingTransactionManager ptm = new CallCountingTransactionManager();
|
||||
|
||||
private final ReactiveCallCountingTransactionManager rtm = new ReactiveCallCountingTransactionManager();
|
||||
|
||||
private final AnnotationTransactionAttributeSource source = new AnnotationTransactionAttributeSource();
|
||||
|
||||
private final TransactionInterceptor ti = new TransactionInterceptor(this.ptm, this.source);
|
||||
@@ -169,6 +178,78 @@ public class AnnotationTransactionInterceptorTests {
|
||||
.satisfies(ex -> assertGetTransactionAndRollbackCount(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withMonoSuccess() {
|
||||
ProxyFactory proxyFactory = new ProxyFactory();
|
||||
proxyFactory.setTarget(new TestWithReactive());
|
||||
proxyFactory.addAdvice(new TransactionInterceptor(rtm, this.source));
|
||||
|
||||
TestWithReactive proxy = (TestWithReactive) proxyFactory.getProxy();
|
||||
|
||||
StepVerifier.withVirtualTime(proxy::monoSuccess).thenAwait(Duration.ofSeconds(10)).verifyComplete();
|
||||
assertReactiveGetTransactionAndCommitCount(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withMonoFailure() {
|
||||
ProxyFactory proxyFactory = new ProxyFactory();
|
||||
proxyFactory.setTarget(new TestWithReactive());
|
||||
proxyFactory.addAdvice(new TransactionInterceptor(rtm, this.source));
|
||||
|
||||
TestWithReactive proxy = (TestWithReactive) proxyFactory.getProxy();
|
||||
|
||||
proxy.monoFailure().as(StepVerifier::create).verifyError();
|
||||
assertReactiveGetTransactionAndRollbackCount(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withMonoRollback() {
|
||||
ProxyFactory proxyFactory = new ProxyFactory();
|
||||
proxyFactory.setTarget(new TestWithReactive());
|
||||
proxyFactory.addAdvice(new TransactionInterceptor(rtm, this.source));
|
||||
|
||||
TestWithReactive proxy = (TestWithReactive) proxyFactory.getProxy();
|
||||
|
||||
StepVerifier.withVirtualTime(proxy::monoSuccess).thenAwait(Duration.ofSeconds(1)).thenCancel().verify();
|
||||
assertReactiveGetTransactionAndRollbackCount(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withFluxSuccess() {
|
||||
ProxyFactory proxyFactory = new ProxyFactory();
|
||||
proxyFactory.setTarget(new TestWithReactive());
|
||||
proxyFactory.addAdvice(new TransactionInterceptor(rtm, this.source));
|
||||
|
||||
TestWithReactive proxy = (TestWithReactive) proxyFactory.getProxy();
|
||||
|
||||
StepVerifier.withVirtualTime(proxy::fluxSuccess).thenAwait(Duration.ofSeconds(10)).expectNextCount(1).verifyComplete();
|
||||
assertReactiveGetTransactionAndCommitCount(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withFluxFailure() {
|
||||
ProxyFactory proxyFactory = new ProxyFactory();
|
||||
proxyFactory.setTarget(new TestWithReactive());
|
||||
proxyFactory.addAdvice(new TransactionInterceptor(rtm, this.source));
|
||||
|
||||
TestWithReactive proxy = (TestWithReactive) proxyFactory.getProxy();
|
||||
|
||||
proxy.fluxFailure().as(StepVerifier::create).verifyError();
|
||||
assertReactiveGetTransactionAndRollbackCount(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withFluxRollback() {
|
||||
ProxyFactory proxyFactory = new ProxyFactory();
|
||||
proxyFactory.setTarget(new TestWithReactive());
|
||||
proxyFactory.addAdvice(new TransactionInterceptor(rtm, this.source));
|
||||
|
||||
TestWithReactive proxy = (TestWithReactive) proxyFactory.getProxy();
|
||||
|
||||
StepVerifier.withVirtualTime(proxy::fluxSuccess).thenAwait(Duration.ofSeconds(1)).thenCancel().verify();
|
||||
assertReactiveGetTransactionAndRollbackCount(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withVavrTrySuccess() {
|
||||
ProxyFactory proxyFactory = new ProxyFactory();
|
||||
@@ -342,6 +423,16 @@ public class AnnotationTransactionInterceptorTests {
|
||||
assertThat(this.ptm.rollbacks).isEqualTo(expectedCount);
|
||||
}
|
||||
|
||||
private void assertReactiveGetTransactionAndCommitCount(int expectedCount) {
|
||||
assertThat(this.rtm.begun).isEqualTo(expectedCount);
|
||||
assertThat(this.rtm.commits).isEqualTo(expectedCount);
|
||||
}
|
||||
|
||||
private void assertReactiveGetTransactionAndRollbackCount(int expectedCount) {
|
||||
assertThat(this.rtm.begun).isEqualTo(expectedCount);
|
||||
assertThat(this.rtm.rollbacks).isEqualTo(expectedCount);
|
||||
}
|
||||
|
||||
|
||||
@Transactional
|
||||
public static class TestClassLevelOnly {
|
||||
@@ -452,6 +543,25 @@ public class AnnotationTransactionInterceptorTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public static class TestWithReactive {
|
||||
|
||||
public Mono<Void> monoSuccess() {
|
||||
return Mono.delay(Duration.ofSeconds(10)).then();
|
||||
}
|
||||
|
||||
public Mono<Void> monoFailure() {
|
||||
return Mono.error(new IllegalStateException());
|
||||
}
|
||||
|
||||
public Flux<Object> fluxSuccess() {
|
||||
return Flux.just(new Object()).delayElements(Duration.ofSeconds(10));
|
||||
}
|
||||
|
||||
public Flux<Object> fluxFailure() {
|
||||
return Flux.error(new IllegalStateException());
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public static class TestWithVavrTry {
|
||||
|
||||
@@ -70,8 +70,8 @@ public class TransactionalOperatorTests {
|
||||
.thenAwait()
|
||||
.thenCancel()
|
||||
.verify();
|
||||
assertThat(tm.commit).isTrue();
|
||||
assertThat(tm.rollback).isFalse();
|
||||
assertThat(tm.commit).isFalse();
|
||||
assertThat(tm.rollback).isTrue();
|
||||
assertThat(cancelled).isTrue();
|
||||
}
|
||||
|
||||
@@ -84,8 +84,8 @@ public class TransactionalOperatorTests {
|
||||
.thenAwait()
|
||||
.thenCancel()
|
||||
.verify();
|
||||
assertThat(tm.commit).isTrue();
|
||||
assertThat(tm.rollback).isFalse();
|
||||
assertThat(tm.commit).isFalse();
|
||||
assertThat(tm.rollback).isTrue();
|
||||
assertThat(cancelled).isTrue();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user