Skip to content

Commit 511ea81

Browse files
committed
GH-9291: Enhanced unlock() method of JdbcLock to verify successful unlocking
Fixes: #9291 * Modify `unlock()` method of `JdbcLock`: if the lock ownership can not be removed due to data expiration, a `ConcurrentModificationException` should be thrown. * Modify `unlock()` method of `RedisLock`: if the lock ownership can not be removed due to data expiration, a `ConcurrentModificationException` should be thrown. * Maintain test cases
1 parent ee6fd33 commit 511ea81

File tree

9 files changed

+147
-74
lines changed

9 files changed

+147
-74
lines changed

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
* @author Gary Russell
6363
* @author Alexandre Strubel
6464
* @author Ruslan Stelmachenko
65+
* @author Eddie Cho
6566
*
6667
* @since 4.3
6768
*/
@@ -389,9 +390,9 @@ public void close() {
389390
}
390391

391392
@Override
392-
public void delete(String lock) {
393-
this.defaultTransactionTemplate.executeWithoutResult(
394-
transactionStatus -> this.template.update(this.deleteQuery, this.region, lock, this.id));
393+
public boolean delete(String lock) {
394+
return this.defaultTransactionTemplate.execute(
395+
transactionStatus -> this.template.update(this.deleteQuery, this.region, lock, this.id)) > 0;
395396
}
396397

397398
@Override

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

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2023 the original author or authors.
2+
* Copyright 2016-2024 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.
@@ -17,6 +17,7 @@
1717
package org.springframework.integration.jdbc.lock;
1818

1919
import java.time.Duration;
20+
import java.util.ConcurrentModificationException;
2021
import java.util.LinkedHashMap;
2122
import java.util.Map;
2223
import java.util.Map.Entry;
@@ -56,6 +57,7 @@
5657
* @author Unseok Kim
5758
* @author Christian Tzolov
5859
* @author Myeonghyeon Lee
60+
* @author Eddie Cho
5961
*
6062
* @since 4.3
6163
*/
@@ -305,12 +307,21 @@ public void unlock() {
305307
try {
306308
while (true) {
307309
try {
308-
this.mutex.delete(this.path);
309-
return;
310+
if (this.mutex.delete(this.path)) {
311+
return;
312+
}
313+
else {
314+
throw new ConcurrentModificationException();
315+
// the lock is no longer owned by current process, the exception should be handle and rollback the execution result
316+
}
310317
}
311318
catch (TransientDataAccessException | TransactionTimedOutException | TransactionSystemException e) {
312319
// try again
313320
}
321+
catch (ConcurrentModificationException e) {
322+
throw new ConcurrentModificationException("Lock was released in the store due to expiration. " +
323+
"The integrity of data protected by this lock may have been compromised.");
324+
}
314325
catch (Exception e) {
315326
throw new DataAccessResourceFailureException("Failed to release mutex at " + this.path, e);
316327
}

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2021 the original author or authors.
2+
* Copyright 2016-2024 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.
@@ -26,6 +26,7 @@
2626
* @author Dave Syer
2727
* @author Alexandre Strubel
2828
* @author Artem Bilan
29+
* @author Eddie Cho
2930
*
3031
* @since 4.3
3132
*/
@@ -41,8 +42,9 @@ public interface LockRepository extends Closeable {
4142
/**
4243
* Remove a lock from this repository.
4344
* @param lock the lock to remove.
45+
* @return deleted or not.
4446
*/
45-
void delete(String lock);
47+
boolean delete(String lock);
4648

4749
/**
4850
* Remove all the expired locks.

spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/lock/JdbcLockRegistryDelegateTests.java

+19-30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2024 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.
@@ -17,7 +17,6 @@
1717
package org.springframework.integration.jdbc.lock;
1818

1919
import java.util.Random;
20-
import java.util.concurrent.atomic.AtomicBoolean;
2120
import java.util.concurrent.locks.Lock;
2221
import java.util.concurrent.locks.ReentrantLock;
2322

@@ -31,17 +30,17 @@
3130

3231
import static org.assertj.core.api.Assertions.assertThat;
3332
import static org.mockito.ArgumentMatchers.anyString;
34-
import static org.mockito.Mockito.doAnswer;
3533
import static org.mockito.Mockito.mock;
3634
import static org.mockito.Mockito.when;
3735

3836
/**
3937
* @author Olivier Hubaut
4038
* @author Fran Aranda
39+
* @author Eddie Cho
4140
*
4241
* @since 5.2.11
4342
*/
44-
public class JdbcLockRegistryDelegateTests {
43+
class JdbcLockRegistryDelegateTests {
4544

4645
private JdbcLockRegistry registry;
4746

@@ -56,7 +55,7 @@ public void clear() {
5655
}
5756

5857
@Test
59-
public void testLessAmountOfUnlockThanLock() {
58+
void testLessAmountOfUnlockThanLock() {
6059
final Random random = new Random();
6160
final int lockCount = random.nextInt(5) + 1;
6261
final int unlockCount = random.nextInt(lockCount);
@@ -73,11 +72,13 @@ public void testLessAmountOfUnlockThanLock() {
7372
}
7473

7574
@Test
76-
public void testSameAmountOfUnlockThanLock() {
75+
void testSameAmountOfUnlockThanLock() {
7776
final Random random = new Random();
7877
final int lockCount = random.nextInt(5) + 1;
7978

8079
final Lock lock = registry.obtain("foo");
80+
when(repository.delete(anyString())).thenReturn(true);
81+
8182
for (int i = 0; i < lockCount; i++) {
8283
lock.tryLock();
8384
}
@@ -89,53 +90,41 @@ public void testSameAmountOfUnlockThanLock() {
8990
}
9091

9192
@Test
92-
public void testTransientDataAccessException() {
93+
void testTransientDataAccessException() {
9394
final Lock lock = registry.obtain("foo");
9495
lock.tryLock();
9596

96-
final AtomicBoolean shouldThrow = new AtomicBoolean(true);
97-
doAnswer(invocation -> {
98-
if (shouldThrow.getAndSet(false)) {
99-
throw mock(TransientDataAccessException.class);
100-
}
101-
return null;
102-
}).when(repository).delete(anyString());
97+
when(repository.delete(anyString()))
98+
.thenThrow(mock(TransientDataAccessException.class))
99+
.thenReturn(true);
103100

104101
lock.unlock();
105102

106103
assertThat(TestUtils.getPropertyValue(lock, "delegate", ReentrantLock.class).isLocked()).isFalse();
107104
}
108105

109106
@Test
110-
public void testTransactionTimedOutException() {
107+
void testTransactionTimedOutException() {
111108
final Lock lock = registry.obtain("foo");
112109
lock.tryLock();
113110

114-
final AtomicBoolean shouldThrow = new AtomicBoolean(true);
115-
doAnswer(invocation -> {
116-
if (shouldThrow.getAndSet(false)) {
117-
throw mock(TransactionTimedOutException.class);
118-
}
119-
return null;
120-
}).when(repository).delete(anyString());
111+
when(repository.delete(anyString()))
112+
.thenThrow(TransactionTimedOutException.class)
113+
.thenReturn(true);
121114

122115
lock.unlock();
123116

124117
assertThat(TestUtils.getPropertyValue(lock, "delegate", ReentrantLock.class).isLocked()).isFalse();
125118
}
126119

127120
@Test
128-
public void testTransactionSystemException() {
121+
void testTransactionSystemException() {
129122
final Lock lock = registry.obtain("foo");
130123
lock.tryLock();
131124

132-
final AtomicBoolean shouldThrow = new AtomicBoolean(true);
133-
doAnswer(invocation -> {
134-
if (shouldThrow.getAndSet(false)) {
135-
throw mock(TransactionSystemException.class);
136-
}
137-
return null;
138-
}).when(repository).delete(anyString());
125+
when(repository.delete(anyString()))
126+
.thenThrow(TransactionSystemException.class)
127+
.thenReturn(true);
139128

140129
lock.unlock();
141130

spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/lock/JdbcLockRegistryDifferentClientTests.java

+14-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2022 the original author or authors.
2+
* Copyright 2016-2024 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.
@@ -17,6 +17,7 @@
1717
package org.springframework.integration.jdbc.lock;
1818

1919
import java.util.ArrayList;
20+
import java.util.ConcurrentModificationException;
2021
import java.util.List;
2122
import java.util.concurrent.BlockingQueue;
2223
import java.util.concurrent.Callable;
@@ -45,26 +46,28 @@
4546
import org.springframework.util.StopWatch;
4647

4748
import static org.assertj.core.api.Assertions.assertThat;
49+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
4850

4951
/**
5052
* @author Dave Syer
5153
* @author Artem Bilan
5254
* @author Glenn Renfro
5355
* @author Alexandre Strubel
56+
* @author Eddie Cho
5457
*
5558
* @since 4.3
5659
*/
5760
@SpringJUnitConfig(locations = "JdbcLockRegistryTests-context.xml")
5861
@DirtiesContext
59-
public class JdbcLockRegistryDifferentClientTests {
62+
class JdbcLockRegistryDifferentClientTests {
6063

6164
private static final Log LOGGER = LogFactory.getLog(JdbcLockRegistryDifferentClientTests.class);
6265

6366
@Autowired
6467
private JdbcLockRegistry registry;
6568

6669
@Autowired
67-
private LockRepository client;
70+
private DefaultLockRepository client;
6871

6972
@Autowired
7073
private ConfigurableApplicationContext context;
@@ -78,6 +81,7 @@ public class JdbcLockRegistryDifferentClientTests {
7881
public void clear() {
7982
this.registry.expireUnusedOlderThan(0);
8083
this.client.close();
84+
this.client.afterPropertiesSet();
8185
this.child = new AnnotationConfigApplicationContext();
8286
this.child.registerBean("childLockRepository", DefaultLockRepository.class, this.dataSource);
8387
this.child.setParent(this.context);
@@ -92,7 +96,7 @@ public void close() {
9296
}
9397

9498
@Test
95-
public void testSecondThreadLoses() throws Exception {
99+
void testSecondThreadLoses() throws Exception {
96100
for (int i = 0; i < 100; i++) {
97101
final JdbcLockRegistry registry1 = this.registry;
98102
final JdbcLockRegistry registry2 = this.child.getBean(JdbcLockRegistry.class);
@@ -129,7 +133,7 @@ public void testSecondThreadLoses() throws Exception {
129133
}
130134

131135
@Test
132-
public void testBothLock() throws Exception {
136+
void testBothLock() throws Exception {
133137
for (int i = 0; i < 100; i++) {
134138
final JdbcLockRegistry registry1 = this.registry;
135139
final JdbcLockRegistry registry2 = this.child.getBean(JdbcLockRegistry.class);
@@ -185,7 +189,7 @@ public void testBothLock() throws Exception {
185189
}
186190

187191
@Test
188-
public void testOnlyOneLock() throws Exception {
192+
void testOnlyOneLock() throws Exception {
189193
for (int i = 0; i < 100; i++) {
190194
final BlockingQueue<String> locked = new LinkedBlockingQueue<>();
191195
final CountDownLatch latch = new CountDownLatch(20);
@@ -231,7 +235,7 @@ public void testOnlyOneLock() throws Exception {
231235
}
232236

233237
@Test
234-
public void testExclusiveAccess() throws Exception {
238+
void testExclusiveAccess() throws Exception {
235239
DefaultLockRepository client1 = new DefaultLockRepository(dataSource);
236240
client1.setApplicationContext(this.context);
237241
client1.afterPropertiesSet();
@@ -281,7 +285,7 @@ public void testExclusiveAccess() throws Exception {
281285
}
282286

283287
@Test
284-
public void testOutOfDateLockTaken() throws Exception {
288+
void testOutOfDateLockTaken() throws Exception {
285289
DefaultLockRepository client1 = new DefaultLockRepository(dataSource);
286290
client1.setTimeToLive(100);
287291
client1.setApplicationContext(this.context);
@@ -314,7 +318,7 @@ public void testOutOfDateLockTaken() throws Exception {
314318
});
315319
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
316320
data.add(2);
317-
lock1.unlock();
321+
assertThatThrownBy(lock1::unlock).isInstanceOf(ConcurrentModificationException.class);
318322
for (int i = 0; i < 2; i++) {
319323
Integer integer = data.poll(10, TimeUnit.SECONDS);
320324
assertThat(integer).isNotNull();
@@ -323,7 +327,7 @@ public void testOutOfDateLockTaken() throws Exception {
323327
}
324328

325329
@Test
326-
public void testRenewLock() throws Exception {
330+
void testRenewLock() throws Exception {
327331
DefaultLockRepository client1 = new DefaultLockRepository(dataSource);
328332
client1.setTimeToLive(500);
329333
client1.setApplicationContext(this.context);

spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/lock/JdbcLockRegistryTests-context.xml

-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424

2525
<bean id="lockClient" class="org.springframework.integration.jdbc.lock.DefaultLockRepository">
2626
<constructor-arg name="dataSource" ref="dataSource"/>
27-
<property name="insertQuery"
28-
value="INSERT INTO INT_LOCK (REGION, LOCK_KEY, CLIENT_ID, CREATED_DATE) VALUES (?, ?, ?, ?)"/>
2927
</bean>
3028

3129
<tx:annotation-driven/>

0 commit comments

Comments
 (0)