Skip to content

Commit 96bea49

Browse files
artembilangaryrussell
authored andcommitted
GH-8748: JDBC locks: use READ_COMMITTED isolation (#8749)
Fixes #8748 The Oracle DB throws `ORA-08177: can't serialize access for this transaction` when other transaction on the row has begun * Change the isolation for `DefaultLockRepository.acquire()` transaction to the `READ_COMMITTED` for what database automatically and silently restarts the entire SQL statement, and no error occurs. * Add `oracle` dependencies to JDBC module * Introduce `OracleContainerTest` and implement it for `OracleLockRegistryTests` **Cherry-pick to `6.1.x` & `6.0.x`**
1 parent 5489b15 commit 96bea49

File tree

4 files changed

+229
-4
lines changed

4 files changed

+229
-4
lines changed

Diff for: build.gradle

+3
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ ext {
9292
mockitoVersion = '4.10.0'
9393
mongoDriverVersion = '4.8.2'
9494
mysqlVersion = '8.0.33'
95+
oracleVersion = '23.3.0.23.09'
9596
pahoMqttClientVersion = '1.2.5'
9697
postgresVersion = '42.5.4'
9798
r2dbch2Version = '1.0.0.RELEASE'
@@ -725,8 +726,10 @@ project('spring-integration-jdbc') {
725726
testImplementation "org.apache.commons:commons-dbcp2:$commonsDbcp2Version"
726727
testImplementation 'org.testcontainers:mysql'
727728
testImplementation 'org.testcontainers:postgresql'
729+
testImplementation 'org.testcontainers:oracle-xe'
728730

729731
testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind'
732+
testRuntimeOnly "com.oracle.database.jdbc:ojdbc11:$oracleVersion"
730733
}
731734
}
732735

Diff for: spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/lock/DefaultLockRepository.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public class DefaultLockRepository
103103

104104
private TransactionTemplate readOnlyTransactionTemplate;
105105

106-
private TransactionTemplate serializableTransactionTemplate;
106+
private TransactionTemplate readCommittedTransactionTemplate;
107107

108108
/**
109109
* Constructor that initializes the client id that will be associated for
@@ -206,9 +206,9 @@ public void afterSingletonsInstantiated() {
206206
this.readOnlyTransactionTemplate = new TransactionTemplate(this.transactionManager, transactionDefinition);
207207

208208
transactionDefinition.setReadOnly(false);
209-
transactionDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
209+
transactionDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
210210

211-
this.serializableTransactionTemplate = new TransactionTemplate(this.transactionManager, transactionDefinition);
211+
this.readCommittedTransactionTemplate = new TransactionTemplate(this.transactionManager, transactionDefinition);
212212
}
213213

214214
@Override
@@ -226,7 +226,7 @@ public void delete(String lock) {
226226
@Override
227227
public boolean acquire(String lock) {
228228
Boolean result =
229-
this.serializableTransactionTemplate.execute(
229+
this.readCommittedTransactionTemplate.execute(
230230
transactionStatus -> {
231231
if (this.template.update(this.updateQuery, this.id, LocalDateTime.now(ZoneOffset.UTC),
232232
this.region, lock, this.id,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.jdbc.oracle;
18+
19+
import javax.sql.DataSource;
20+
21+
import org.apache.commons.dbcp2.BasicDataSource;
22+
import org.junit.jupiter.api.BeforeAll;
23+
import org.testcontainers.containers.OracleContainer;
24+
import org.testcontainers.junit.jupiter.Testcontainers;
25+
import org.testcontainers.utility.DockerImageName;
26+
27+
/**
28+
* The base contract for JUnit tests based on the container for Oracle.
29+
* The Testcontainers 'reuse' option must be disabled,so, Ryuk container is started
30+
* and will clean all the containers up from this test suite after JVM exit.
31+
* Since the Oracle container instance is shared via static property, it is going to be
32+
* started only once per JVM, therefore the target Docker container is reused automatically.
33+
*
34+
* @author Artem Bilan
35+
*
36+
* @since 6.0.8
37+
*/
38+
@Testcontainers(disabledWithoutDocker = true)
39+
public interface OracleContainerTest {
40+
41+
OracleContainer ORACLE_CONTAINER =
42+
new OracleContainer(DockerImageName.parse("gvenzl/oracle-xe:21-slim-faststart"))
43+
.withInitScript("org/springframework/integration/jdbc/schema-oracle.sql");
44+
45+
@BeforeAll
46+
static void startContainer() {
47+
ORACLE_CONTAINER.start();
48+
}
49+
50+
static DataSource dataSource() {
51+
BasicDataSource dataSource = new BasicDataSource();
52+
dataSource.setDriverClassName(ORACLE_CONTAINER.getDriverClassName());
53+
dataSource.setUrl(ORACLE_CONTAINER.getJdbcUrl());
54+
dataSource.setUsername(ORACLE_CONTAINER.getUsername());
55+
dataSource.setPassword(ORACLE_CONTAINER.getPassword());
56+
return dataSource;
57+
}
58+
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.jdbc.oracle;
18+
19+
import java.util.concurrent.CountDownLatch;
20+
import java.util.concurrent.Future;
21+
import java.util.concurrent.TimeUnit;
22+
import java.util.concurrent.atomic.AtomicBoolean;
23+
import java.util.concurrent.locks.Lock;
24+
25+
import org.junit.jupiter.api.Test;
26+
27+
import org.springframework.beans.factory.annotation.Autowired;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.core.task.AsyncTaskExecutor;
31+
import org.springframework.core.task.SimpleAsyncTaskExecutor;
32+
import org.springframework.integration.jdbc.lock.DefaultLockRepository;
33+
import org.springframework.integration.jdbc.lock.JdbcLockRegistry;
34+
import org.springframework.integration.jdbc.lock.LockRepository;
35+
import org.springframework.jdbc.support.JdbcTransactionManager;
36+
import org.springframework.test.annotation.DirtiesContext;
37+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
38+
import org.springframework.transaction.PlatformTransactionManager;
39+
40+
import static org.assertj.core.api.Assertions.assertThat;
41+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
42+
import static org.assertj.core.api.Assertions.assertThatNoException;
43+
44+
/**
45+
* @author Artem Bilan
46+
*
47+
* @since 6.0.8
48+
*/
49+
@SpringJUnitConfig
50+
@DirtiesContext
51+
public class OracleLockRegistryTests implements OracleContainerTest {
52+
53+
@Autowired
54+
AsyncTaskExecutor taskExecutor;
55+
56+
@Autowired
57+
JdbcLockRegistry registry;
58+
59+
@Test
60+
public void twoThreadsSameLock() throws Exception {
61+
final Lock lock1 = this.registry.obtain("foo");
62+
final AtomicBoolean locked = new AtomicBoolean();
63+
final CountDownLatch latch1 = new CountDownLatch(1);
64+
final CountDownLatch latch2 = new CountDownLatch(1);
65+
final CountDownLatch latch3 = new CountDownLatch(1);
66+
lock1.lockInterruptibly();
67+
this.taskExecutor.execute(() -> {
68+
Lock lock2 = this.registry.obtain("foo");
69+
try {
70+
latch1.countDown();
71+
lock2.lockInterruptibly();
72+
latch2.await(10, TimeUnit.SECONDS);
73+
locked.set(true);
74+
}
75+
catch (InterruptedException e) {
76+
Thread.currentThread().interrupt();
77+
}
78+
finally {
79+
lock2.unlock();
80+
latch3.countDown();
81+
}
82+
});
83+
assertThat(latch1.await(10, TimeUnit.SECONDS)).isTrue();
84+
assertThat(locked.get()).isFalse();
85+
lock1.unlock();
86+
latch2.countDown();
87+
assertThat(latch3.await(10, TimeUnit.SECONDS)).isTrue();
88+
assertThat(locked.get()).isTrue();
89+
}
90+
91+
@Test
92+
public void twoThreadsSecondFailsToGetLock() throws Exception {
93+
final Lock lock1 = this.registry.obtain("foo");
94+
lock1.lockInterruptibly();
95+
final AtomicBoolean locked = new AtomicBoolean();
96+
final CountDownLatch latch = new CountDownLatch(1);
97+
Future<Object> result = taskExecutor.submit(() -> {
98+
Lock lock2 = this.registry.obtain("foo");
99+
locked.set(lock2.tryLock(200, TimeUnit.MILLISECONDS));
100+
latch.countDown();
101+
try {
102+
lock2.unlock();
103+
}
104+
catch (Exception e) {
105+
return e;
106+
}
107+
return null;
108+
});
109+
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
110+
assertThat(locked.get()).isFalse();
111+
lock1.unlock();
112+
Object ise = result.get(10, TimeUnit.SECONDS);
113+
assertThat(ise).isInstanceOf(IllegalMonitorStateException.class);
114+
assertThat(((Exception) ise).getMessage()).contains("own");
115+
}
116+
117+
@Test
118+
public void lockRenewed() {
119+
Lock lock = this.registry.obtain("foo");
120+
121+
assertThat(lock.tryLock()).isTrue();
122+
123+
assertThatNoException()
124+
.isThrownBy(() -> this.registry.renewLock("foo"));
125+
126+
lock.unlock();
127+
}
128+
129+
@Test
130+
public void lockRenewExceptionNotOwned() {
131+
this.registry.obtain("foo");
132+
133+
assertThatExceptionOfType(IllegalMonitorStateException.class)
134+
.isThrownBy(() -> this.registry.renewLock("foo"));
135+
}
136+
137+
@Configuration
138+
public static class Config {
139+
140+
@Bean
141+
AsyncTaskExecutor taskExecutor() {
142+
return new SimpleAsyncTaskExecutor();
143+
}
144+
145+
@Bean
146+
public PlatformTransactionManager transactionManager() {
147+
return new JdbcTransactionManager(OracleContainerTest.dataSource());
148+
}
149+
150+
@Bean
151+
public DefaultLockRepository defaultLockRepository() {
152+
return new DefaultLockRepository(OracleContainerTest.dataSource());
153+
}
154+
155+
@Bean
156+
public JdbcLockRegistry jdbcLockRegistry(LockRepository lockRepository) {
157+
return new JdbcLockRegistry(lockRepository);
158+
}
159+
160+
}
161+
162+
}
163+

0 commit comments

Comments
 (0)