Avoid rollback after a commit failure in TransactionalOperator
A failure to commit a reactive transaction will complete the transaction and clean up resources. Executing a rollback at that point is invalid, which causes an IllegalTransactionStateException that masks the cause of the commit failure. This change restructures TransactionalOperatorImpl and ReactiveTransactionSupport to avoid executing a rollback after a failed commit. While there, the Mono transaction handling in TransactionalOperator is simplified by moving it to a default method on the interface. Closes gh-27572
This commit is contained in:
committed by
Sébastien Deleuze
parent
95481018d0
commit
edf0ae77e5
@@ -335,11 +335,7 @@ public abstract class AbstractReactiveTransactionAspectTests {
|
||||
|
||||
Mono.from(itb.setName(name))
|
||||
.as(StepVerifier::create)
|
||||
.consumeErrorWith(throwable -> {
|
||||
assertThat(throwable.getClass()).isEqualTo(RuntimeException.class);
|
||||
assertThat(throwable.getCause()).isEqualTo(ex);
|
||||
})
|
||||
.verify();
|
||||
.verifyErrorSatisfies(actual -> assertThat(actual).isEqualTo(ex));
|
||||
|
||||
// Should have invoked target and changed name
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,21 +16,29 @@
|
||||
|
||||
package org.springframework.transaction.reactive;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import reactor.test.publisher.PublisherProbe;
|
||||
|
||||
import org.springframework.transaction.ReactiveTransaction;
|
||||
import org.springframework.transaction.ReactiveTransactionManager;
|
||||
import org.springframework.transaction.support.DefaultTransactionDefinition;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link TransactionalOperator}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Enric Sala
|
||||
*/
|
||||
public class TransactionalOperatorTests {
|
||||
|
||||
@@ -99,6 +107,43 @@ public class TransactionalOperatorTests {
|
||||
assertThat(tm.rollback).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commitFailureWithMono() {
|
||||
ReactiveTransactionManager tm = mock(ReactiveTransactionManager.class);
|
||||
given(tm.getReactiveTransaction(any())).willReturn(Mono.just(mock(ReactiveTransaction.class)));
|
||||
PublisherProbe<Void> commit = PublisherProbe.of(Mono.error(IOException::new));
|
||||
given(tm.commit(any())).willReturn(commit.mono());
|
||||
PublisherProbe<Void> rollback = PublisherProbe.empty();
|
||||
given(tm.rollback(any())).willReturn(rollback.mono());
|
||||
|
||||
TransactionalOperator operator = TransactionalOperator.create(tm, new DefaultTransactionDefinition());
|
||||
Mono.just(true).as(operator::transactional)
|
||||
.as(StepVerifier::create)
|
||||
.verifyError(IOException.class);
|
||||
assertThat(commit.subscribeCount()).isEqualTo(1);
|
||||
rollback.assertWasNotSubscribed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rollbackFailureWithMono() {
|
||||
ReactiveTransactionManager tm = mock(ReactiveTransactionManager.class);
|
||||
given(tm.getReactiveTransaction(any())).willReturn(Mono.just(mock(ReactiveTransaction.class)));
|
||||
PublisherProbe<Void> commit = PublisherProbe.empty();
|
||||
given(tm.commit(any())).willReturn(commit.mono());
|
||||
PublisherProbe<Void> rollback = PublisherProbe.of(Mono.error(IOException::new));
|
||||
given(tm.rollback(any())).willReturn(rollback.mono());
|
||||
|
||||
TransactionalOperator operator = TransactionalOperator.create(tm, new DefaultTransactionDefinition());
|
||||
IllegalStateException actionFailure = new IllegalStateException();
|
||||
Mono.error(actionFailure).as(operator::transactional)
|
||||
.as(StepVerifier::create)
|
||||
.verifyErrorSatisfies(ex -> assertThat(ex)
|
||||
.isInstanceOf(IOException.class)
|
||||
.hasSuppressedException(actionFailure));
|
||||
commit.assertWasNotSubscribed();
|
||||
assertThat(rollback.subscribeCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commitWithFlux() {
|
||||
TransactionalOperator operator = TransactionalOperator.create(tm, new DefaultTransactionDefinition());
|
||||
@@ -120,4 +165,43 @@ public class TransactionalOperatorTests {
|
||||
assertThat(tm.rollback).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commitFailureWithFlux() {
|
||||
ReactiveTransactionManager tm = mock(ReactiveTransactionManager.class);
|
||||
given(tm.getReactiveTransaction(any())).willReturn(Mono.just(mock(ReactiveTransaction.class)));
|
||||
PublisherProbe<Void> commit = PublisherProbe.of(Mono.error(IOException::new));
|
||||
given(tm.commit(any())).willReturn(commit.mono());
|
||||
PublisherProbe<Void> rollback = PublisherProbe.empty();
|
||||
given(tm.rollback(any())).willReturn(rollback.mono());
|
||||
|
||||
TransactionalOperator operator = TransactionalOperator.create(tm, new DefaultTransactionDefinition());
|
||||
Flux.just(1, 2, 3, 4).as(operator::transactional)
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(4)
|
||||
.verifyError(IOException.class);
|
||||
assertThat(commit.subscribeCount()).isEqualTo(1);
|
||||
rollback.assertWasNotSubscribed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rollbackFailureWithFlux() {
|
||||
ReactiveTransactionManager tm = mock(ReactiveTransactionManager.class);
|
||||
given(tm.getReactiveTransaction(any())).willReturn(Mono.just(mock(ReactiveTransaction.class)));
|
||||
PublisherProbe<Void> commit = PublisherProbe.empty();
|
||||
given(tm.commit(any())).willReturn(commit.mono());
|
||||
PublisherProbe<Void> rollback = PublisherProbe.of(Mono.error(IOException::new));
|
||||
given(tm.rollback(any())).willReturn(rollback.mono());
|
||||
|
||||
TransactionalOperator operator = TransactionalOperator.create(tm, new DefaultTransactionDefinition());
|
||||
IllegalStateException actionFailure = new IllegalStateException();
|
||||
Flux.just(1, 2, 3).concatWith(Flux.error(actionFailure)).as(operator::transactional)
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(3)
|
||||
.verifyErrorSatisfies(ex -> assertThat(ex)
|
||||
.isInstanceOf(IOException.class)
|
||||
.hasSuppressedException(actionFailure));
|
||||
commit.assertWasNotSubscribed();
|
||||
assertThat(rollback.subscribeCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user