Skip to content

Commit 0b3814c

Browse files
authored
Ensure isolation level is applied to subsequent transactions after setting (#202)
Motivation: Aligning Connection#setTransactionIsolationLevel Behavior with r2dbc-spi Specification. See #192 for more. Modification: Change session isolation level as well after invoking `Connection#setTransactionIsolationLevel`. Result: All subsequent transactions will be applied the isolation level set by `Connection#setTransactionIsolationLevel`.
1 parent b3a0fc9 commit 0b3814c

File tree

4 files changed

+100
-4
lines changed

4 files changed

+100
-4
lines changed

src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public final class MySqlConnection implements Connection, Lifecycle, ConnectionS
124124

125125
private final MySqlConnectionMetadata metadata;
126126

127-
private final IsolationLevel sessionLevel;
127+
private volatile IsolationLevel sessionLevel;
128128

129129
private final QueryCache queryCache;
130130

@@ -298,13 +298,27 @@ public IsolationLevel getTransactionIsolationLevel() {
298298
return currentLevel;
299299
}
300300

301+
/**
302+
* Gets session transaction isolation level(Only for testing).
303+
*
304+
* @return session transaction isolation level.
305+
*/
306+
IsolationLevel getSessionTransactionIsolationLevel() {
307+
return sessionLevel;
308+
}
309+
301310
@Override
302311
public Mono<Void> setTransactionIsolationLevel(IsolationLevel isolationLevel) {
303312
requireNonNull(isolationLevel, "isolationLevel must not be null");
304313

305-
// Set next transaction isolation level.
306-
return QueryFlow.executeVoid(client, "SET TRANSACTION ISOLATION LEVEL " + isolationLevel.asSql())
307-
.doOnSuccess(ignored -> setIsolationLevel(isolationLevel));
314+
// Set subsequent transaction isolation level.
315+
return QueryFlow.executeVoid(client, "SET SESSION TRANSACTION ISOLATION LEVEL " + isolationLevel.asSql())
316+
.doOnSuccess(ignored -> {
317+
this.sessionLevel = isolationLevel;
318+
if (!this.isInTransaction()) {
319+
this.currentLevel = isolationLevel;
320+
}
321+
});
308322
}
309323

310324
@Override

src/main/java/io/asyncer/r2dbc/mysql/message/client/TextQueryMessage.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.asyncer.r2dbc.mysql.ConnectionContext;
2020
import io.netty.buffer.ByteBuf;
2121
import io.netty.buffer.ByteBufAllocator;
22+
import java.util.Objects;
2223
import reactor.core.publisher.Mono;
2324

2425
import java.nio.charset.Charset;
@@ -67,6 +68,23 @@ public Mono<ByteBuf> encode(ByteBufAllocator allocator, ConnectionContext contex
6768
});
6869
}
6970

71+
@Override
72+
public boolean equals(Object o) {
73+
if (this == o) {
74+
return true;
75+
}
76+
if (o == null || getClass() != o.getClass()) {
77+
return false;
78+
}
79+
TextQueryMessage that = (TextQueryMessage) o;
80+
return Objects.equals(sql, that.sql);
81+
}
82+
83+
@Override
84+
public int hashCode() {
85+
return Objects.hash(sql);
86+
}
87+
7088
@Override
7189
public String toString() {
7290
return "TextQueryMessage{sql=REDACTED}";

src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,51 @@ void transactionDefinitionIsolationLevel() {
139139
}));
140140
}
141141

142+
@Test
143+
void setTransactionLevelNotInTransaction() {
144+
complete(connection ->
145+
// check initial session isolation level
146+
Mono.fromSupplier(connection::getTransactionIsolationLevel)
147+
.doOnSuccess(it -> assertThat(it).isEqualTo(REPEATABLE_READ))
148+
.then(connection.beginTransaction())
149+
.doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue())
150+
.then(Mono.fromSupplier(connection::getTransactionIsolationLevel))
151+
.doOnSuccess(it -> assertThat(it).isEqualTo(REPEATABLE_READ))
152+
.then(connection.rollbackTransaction())
153+
.then(connection.setTransactionIsolationLevel(READ_COMMITTED))
154+
// ensure that session isolation level is changed
155+
.then(Mono.fromSupplier(connection::getTransactionIsolationLevel))
156+
.doOnSuccess(it -> assertThat(it).isEqualTo(READ_COMMITTED))
157+
.then(connection.beginTransaction())
158+
.doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue())
159+
// ensure transaction isolation level applies to subsequent transactions
160+
.then(Mono.fromSupplier(connection::getTransactionIsolationLevel))
161+
.doOnSuccess(it -> assertThat(it).isEqualTo(READ_COMMITTED))
162+
);
163+
}
164+
165+
@Test
166+
void setTransactionLevelInTransaction() {
167+
complete(connection ->
168+
// check initial session transaction isolation level
169+
Mono.fromSupplier(connection::getTransactionIsolationLevel)
170+
.doOnSuccess(it -> assertThat(it).isEqualTo(REPEATABLE_READ))
171+
.then(connection.beginTransaction())
172+
.then(connection.setTransactionIsolationLevel(READ_COMMITTED))
173+
// ensure that current transaction isolation level is not changed
174+
.then(Mono.fromSupplier(connection::getTransactionIsolationLevel))
175+
.doOnSuccess(it -> assertThat(it).isNotEqualTo(READ_COMMITTED))
176+
.then(connection.rollbackTransaction())
177+
.doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse())
178+
// ensure that session isolation level is changed after rollback
179+
.then(Mono.fromSupplier(connection::getTransactionIsolationLevel))
180+
.doOnSuccess(it -> assertThat(it).isEqualTo(READ_COMMITTED))
181+
// ensure transaction isolation level applies to subsequent transactions
182+
.then(connection.beginTransaction())
183+
.doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue())
184+
);
185+
}
186+
142187
@Test
143188
void transactionDefinition() {
144189
// The WITH CONSISTENT SNAPSHOT phrase can only be used with the REPEATABLE READ isolation level.

src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,20 @@
1919
import io.asyncer.r2dbc.mysql.cache.Caches;
2020
import io.asyncer.r2dbc.mysql.client.Client;
2121
import io.asyncer.r2dbc.mysql.codec.Codecs;
22+
import io.asyncer.r2dbc.mysql.message.client.ClientMessage;
23+
import io.asyncer.r2dbc.mysql.message.client.TextQueryMessage;
2224
import io.r2dbc.spi.IsolationLevel;
2325
import org.assertj.core.api.ThrowableTypeAssert;
2426
import org.junit.jupiter.api.Test;
27+
import reactor.core.publisher.Flux;
28+
import reactor.test.StepVerifier;
2529

2630
import static org.assertj.core.api.Assertions.assertThat;
2731
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
32+
import static org.mockito.ArgumentMatchers.any;
33+
import static org.mockito.ArgumentMatchers.eq;
2834
import static org.mockito.Mockito.mock;
35+
import static org.mockito.Mockito.when;
2936

3037
/**
3138
* Unit tests for {@link MySqlConnection}.
@@ -125,6 +132,18 @@ void badSetTransactionIsolationLevel() {
125132
assertThatIllegalArgumentException().isThrownBy(() -> noPrepare.setTransactionIsolationLevel(null));
126133
}
127134

135+
@Test
136+
void shouldSetTransactionIsolationLevelSuccessfully() {
137+
ClientMessage message = new TextQueryMessage("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE");
138+
when(client.exchange(eq(message), any())).thenReturn(Flux.empty());
139+
140+
noPrepare.setTransactionIsolationLevel(IsolationLevel.SERIALIZABLE)
141+
.as(StepVerifier::create)
142+
.verifyComplete();
143+
144+
assertThat(noPrepare.getSessionTransactionIsolationLevel()).isEqualTo(IsolationLevel.SERIALIZABLE);
145+
}
146+
128147
@SuppressWarnings("ConstantConditions")
129148
@Test
130149
void badValidate() {

0 commit comments

Comments
 (0)