Skip to content

Commit 8b619bb

Browse files
xak2000artembilan
authored andcommitted
GH-3683: Always new TX in DefaultLockRepository
Fixes #3683 If a transaction is already active while `JdbcLockRegistry` uses `DefaultLockRepository` to acquire/release a lock, the repository must execute SQL queries in a separate transaction to prevent problems with blocking, deadlocking etc. It also allows to properly follow transaction isolation level that is set on some methods of `DefaultLockRepository`. Previously all methods of DefaultLockRepository supported the current transaction if there are any. Now all methods will always create new transaction on each call. * Change tests to be more unit-ish * Change `@since` for the `DefaultLockRepositoryTests` to `5.3.10` **Cherry-pick to `5.4.x` & `5.3.x`**
1 parent f0007c0 commit 8b619bb

File tree

2 files changed

+101
-3
lines changed

2 files changed

+101
-3
lines changed

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2020 the original author or authors.
2+
* Copyright 2016-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@
2727
import org.springframework.jdbc.core.JdbcTemplate;
2828
import org.springframework.stereotype.Repository;
2929
import org.springframework.transaction.annotation.Isolation;
30+
import org.springframework.transaction.annotation.Propagation;
3031
import org.springframework.transaction.annotation.Transactional;
3132
import org.springframework.util.Assert;
3233

@@ -44,11 +45,11 @@
4445
* @author Glenn Renfro
4546
* @author Gary Russell
4647
* @author Alexandre Strubel
48+
* @author Ruslan Stelmachenko
4749
*
4850
* @since 4.3
4951
*/
5052
@Repository
51-
@Transactional
5253
public class DefaultLockRepository implements LockRepository, InitializingBean {
5354

5455
/**
@@ -149,17 +150,19 @@ public void afterPropertiesSet() {
149150
this.renewQuery = String.format(this.renewQuery, this.prefix);
150151
}
151152

153+
@Transactional(propagation = Propagation.REQUIRES_NEW)
152154
@Override
153155
public void close() {
154156
this.template.update(this.deleteAllQuery, this.region, this.id);
155157
}
156158

159+
@Transactional(propagation = Propagation.REQUIRES_NEW)
157160
@Override
158161
public void delete(String lock) {
159162
this.template.update(this.deleteQuery, this.region, lock, this.id);
160163
}
161164

162-
@Transactional(isolation = Isolation.SERIALIZABLE)
165+
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE)
163166
@Override
164167
public boolean acquire(String lock) {
165168
if (this.template.update(this.updateQuery, this.id, new Date(), this.region, lock, this.id,
@@ -174,17 +177,20 @@ public boolean acquire(String lock) {
174177
}
175178
}
176179

180+
@Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
177181
@Override
178182
public boolean isAcquired(String lock) {
179183
return this.template.queryForObject(this.countQuery, Integer.class, // NOSONAR query never returns null
180184
this.region, lock, this.id, new Date(System.currentTimeMillis() - this.ttl)) == 1;
181185
}
182186

187+
@Transactional(propagation = Propagation.REQUIRES_NEW)
183188
@Override
184189
public void deleteExpired() {
185190
this.template.update(this.deleteExpiredQuery, this.region, new Date(System.currentTimeMillis() - this.ttl));
186191
}
187192

193+
@Transactional(propagation = Propagation.REQUIRES_NEW)
188194
@Override
189195
public boolean renew(String lock) {
190196
return this.template.update(this.renewQuery, new Date(), this.region, lock, this.id) > 0;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2021 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.lock;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.Mockito.spy;
21+
import static org.mockito.Mockito.times;
22+
import static org.mockito.Mockito.verify;
23+
24+
import java.sql.Connection;
25+
26+
import org.junit.jupiter.api.BeforeEach;
27+
import org.junit.jupiter.api.Test;
28+
29+
import org.springframework.beans.factory.annotation.Autowired;
30+
import org.springframework.test.annotation.DirtiesContext;
31+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
32+
import org.springframework.transaction.annotation.Isolation;
33+
import org.springframework.transaction.annotation.Transactional;
34+
import org.springframework.transaction.support.TransactionSynchronization;
35+
import org.springframework.transaction.support.TransactionSynchronizationManager;
36+
37+
/**
38+
* @author Ruslan Stelmachenko
39+
*
40+
* @since 5.3.10
41+
*/
42+
@SpringJUnitConfig(locations = "JdbcLockRegistryTests-context.xml")
43+
@DirtiesContext
44+
public class DefaultLockRepositoryTests {
45+
46+
@Autowired
47+
private LockRepository client;
48+
49+
@BeforeEach
50+
public void clear() {
51+
this.client.close();
52+
}
53+
54+
@Transactional
55+
@Test
56+
public void testNewTransactionIsStartedWhenTransactionIsAlreadyActive() {
57+
// Make sure a transaction is active
58+
assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue();
59+
60+
TransactionSynchronization transactionSynchronization = spy(TransactionSynchronization.class);
61+
TransactionSynchronizationManager.registerSynchronization(transactionSynchronization);
62+
63+
this.client.acquire("foo"); // 1
64+
this.client.renew("foo"); // 2
65+
this.client.delete("foo"); // 3
66+
this.client.isAcquired("foo"); // 4
67+
this.client.deleteExpired(); // 5
68+
this.client.close(); // 6
69+
70+
// Make sure a transaction is still active
71+
assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue();
72+
// And was suspended for each invocation of @Transactional methods of DefaultLockRepository,
73+
// that confirms that these methods were called in a separate transaction each.
74+
verify(transactionSynchronization, times(6)).suspend();
75+
}
76+
77+
@Transactional(isolation = Isolation.REPEATABLE_READ)
78+
@Test
79+
public void testIsAcquiredFromRepeatableReadTransaction() {
80+
// Make sure a transaction with REPEATABLE_READ isolation level is active
81+
assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue();
82+
assertThat(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel())
83+
.isEqualTo(Connection.TRANSACTION_REPEATABLE_READ);
84+
85+
this.client.acquire("foo");
86+
assertThat(this.client.isAcquired("foo")).isTrue();
87+
88+
this.client.delete("foo");
89+
assertThat(this.client.isAcquired("foo")).isFalse();
90+
}
91+
92+
}

0 commit comments

Comments
 (0)