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:
Enric Sala
2021-10-18 11:35:45 +02:00
committed by Sébastien Deleuze
parent 95481018d0
commit edf0ae77e5
7 changed files with 173 additions and 94 deletions

View File

@@ -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

View File

@@ -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);
}
}