Skip to content

Commit 3f2b743

Browse files
committed
spring-projectsGH-3444: Add Custom TTL support for RedisLock, and JdbcLock
Fixes: spring-projects#3444 * add `CustomTtlLock`, and `CustomTtlLockRegistry` interfaces * Modify `RedisLockRegistry` to implement the interfaces. * Modify ddl of `INT_LOCK` table, `LockRepository`, `DefaultLockRepository`, and `JdbcLockRegistry` to implement the interfaces. * Fix potential concurrency issue of `unlock` method of `JdbcLock`. * Maintain existing test cases and add new test cases.
1 parent c155d5d commit 3f2b743

File tree

22 files changed

+413
-167
lines changed

22 files changed

+413
-167
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2024 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.support.locks;
18+
19+
import java.util.concurrent.TimeUnit;
20+
import java.util.concurrent.locks.Lock;
21+
22+
/**
23+
* A {@link Lock} implementing this interface supports the spring distributed locks with custom time-to-live value per lock
24+
*
25+
* @author Eddie Cho
26+
*
27+
* @since 6.3
28+
*/
29+
public interface CustomTtlLock extends Lock {
30+
31+
/**
32+
* Attempt to acquire a lock with a specific time-to-live
33+
* @param time the maximum time to wait for the lock unit
34+
* @param unit the time unit of the time argument
35+
* @param customTtl the specific time-to-live for the lock status data
36+
* @param customTtlUnit the time unit of the customTtl argument
37+
* @return true if the lock was acquired and false if the waiting time elapsed before the lock was acquired
38+
* @throws InterruptedException -
39+
* if the current thread is interrupted while acquiring the lock (and interruption of lock acquisition is supported)
40+
*/
41+
boolean tryLock(long time, TimeUnit unit, long customTtl, TimeUnit customTtlUnit) throws InterruptedException;
42+
43+
/**
44+
* Attempt to acquire a lock with a specific time-to-live
45+
* @param customTtl the specific time-to-live for the lock status data
46+
* @param customTtlUnit the time unit of the customTtl argument
47+
*/
48+
void lock(long customTtl, TimeUnit customTtlUnit);
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2024 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+
/**
18+
* A {@link LockRegistry} implementing this interface supports the CustomTtlLock
19+
*
20+
* @author Eddie Cho
21+
*
22+
* @since 6.3
23+
*/
24+
package org.springframework.integration.support.locks;
25+
26+
public interface CustomTtlLockRegistry extends LockRegistry {
27+
28+
CustomTtlLock obtainCustomTtlLock(Object lockKey);
29+
}

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

+24-38
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
*/
@@ -76,19 +77,12 @@ public class DefaultLockRepository
7677
*/
7778
public static final String DEFAULT_TABLE_PREFIX = "INT_";
7879

79-
/**
80-
* Default value for the time-to-live property.
81-
*/
82-
public static final Duration DEFAULT_TTL = Duration.ofSeconds(10);
83-
8480
private final String id;
8581

8682
private final JdbcTemplate template;
8783

8884
private final AtomicBoolean started = new AtomicBoolean();
8985

90-
private Duration ttl = DEFAULT_TTL;
91-
9286
private String prefix = DEFAULT_TABLE_PREFIX;
9387

9488
private String region = "DEFAULT";
@@ -100,7 +94,7 @@ public class DefaultLockRepository
10094

10195
private String deleteExpiredQuery = """
10296
DELETE FROM %sLOCK
103-
WHERE REGION=? AND CREATED_DATE<?
97+
WHERE REGION=? AND EXPIRED_AFTER<?
10498
""";
10599

106100
private String deleteAllQuery = """
@@ -110,24 +104,24 @@ public class DefaultLockRepository
110104

111105
private String updateQuery = """
112106
UPDATE %sLOCK
113-
SET CLIENT_ID=?, CREATED_DATE=?
114-
WHERE REGION=? AND LOCK_KEY=? AND (CLIENT_ID=? OR CREATED_DATE<?)
107+
SET CLIENT_ID=?, EXPIRED_AFTER=?
108+
WHERE REGION=? AND LOCK_KEY=? AND (CLIENT_ID=? OR EXPIRED_AFTER<?)
115109
""";
116110

117111
private String insertQuery = """
118-
INSERT INTO %sLOCK (REGION, LOCK_KEY, CLIENT_ID, CREATED_DATE)
112+
INSERT INTO %sLOCK (REGION, LOCK_KEY, CLIENT_ID, EXPIRED_AFTER)
119113
VALUES (?, ?, ?, ?)
120114
""";
121115

122116
private String countQuery = """
123117
SELECT COUNT(REGION)
124118
FROM %sLOCK
125-
WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=? AND CREATED_DATE>=?
119+
WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=? AND EXPIRED_AFTER>=?
126120
""";
127121

128122
private String renewQuery = """
129123
UPDATE %sLOCK
130-
SET CREATED_DATE=?
124+
SET EXPIRED_AFTER=?
131125
WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=?
132126
""";
133127

@@ -188,14 +182,6 @@ public void setPrefix(String prefix) {
188182
this.prefix = prefix;
189183
}
190184

191-
/**
192-
* Specify the time (in milliseconds) to expire deadlocks.
193-
* @param timeToLive the time to expire deadlocks.
194-
*/
195-
public void setTimeToLive(int timeToLive) {
196-
this.ttl = Duration.ofMillis(timeToLive);
197-
}
198-
199185
/**
200186
* Set a {@link PlatformTransactionManager} for operations.
201187
* Otherwise, a primary {@link PlatformTransactionManager} bean is obtained
@@ -219,8 +205,8 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
219205
* <pre class="code">
220206
* {@code
221207
* UPDATE %sLOCK
222-
* SET CLIENT_ID=?, CREATED_DATE=?
223-
* WHERE REGION=? AND LOCK_KEY=? AND (CLIENT_ID=? OR CREATED_DATE<?)
208+
* SET CLIENT_ID=?, EXPIRED_AFTER=?
209+
* WHERE REGION=? AND LOCK_KEY=? AND (CLIENT_ID=? OR EXPIRED_AFTER<?)
224210
* }
225211
* </pre>
226212
* @param updateQuery the query to update a lock record.
@@ -247,7 +233,7 @@ public String getUpdateQuery() {
247233
* Set a custom {@code INSERT} query for a lock record.
248234
* The {@link #getInsertQuery()} can be used as a template for customization.
249235
* The default query is
250-
* {@code INSERT INTO %sLOCK (REGION, LOCK_KEY, CLIENT_ID, CREATED_DATE) VALUES (?, ?, ?, ?)}.
236+
* {@code INSERT INTO %sLOCK (REGION, LOCK_KEY, CLIENT_ID, EXPIRED_AFTER) VALUES (?, ?, ?, ?)}.
251237
* For example a PostgreSQL {@code ON CONFLICT DO NOTHING} hint can be provided like this:
252238
* <pre class="code">
253239
* {@code
@@ -281,7 +267,7 @@ public String getInsertQuery() {
281267
* <pre class="code">
282268
* {@code
283269
* UPDATE %sLOCK
284-
* SET CREATED_DATE=?
270+
* SET EXPIRED_AFTER=?
285271
* WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=?
286272
* }
287273
* </pre>
@@ -389,23 +375,23 @@ public void close() {
389375
}
390376

391377
@Override
392-
public void delete(String lock) {
393-
this.defaultTransactionTemplate.executeWithoutResult(
394-
transactionStatus -> this.template.update(this.deleteQuery, this.region, lock, this.id));
378+
public boolean delete(String lock) {
379+
return this.defaultTransactionTemplate.execute(
380+
transactionStatus -> this.template.update(this.deleteQuery, this.region, lock, this.id)) > 0;
395381
}
396382

397383
@Override
398-
public boolean acquire(String lock) {
384+
public boolean acquire(String lock, Duration ttlDuration) {
399385
Boolean result =
400386
this.readCommittedTransactionTemplate.execute(
401387
transactionStatus -> {
402-
if (this.template.update(this.updateQuery, this.id, epochMillis(),
403-
this.region, lock, this.id, ttlEpochMillis()) > 0) {
388+
if (this.template.update(this.updateQuery, this.id, ttlEpochMillis(ttlDuration),
389+
this.region, lock, this.id, epochMillis()) > 0) {
404390
return true;
405391
}
406392
try {
407393
return this.template.update(this.insertQuery, this.region, lock, this.id,
408-
epochMillis()) > 0;
394+
ttlEpochMillis(ttlDuration)) > 0;
409395
}
410396
catch (DataIntegrityViolationException ex) {
411397
return false;
@@ -420,27 +406,27 @@ public boolean isAcquired(String lock) {
420406
transactionStatus ->
421407
Integer.valueOf(1).equals(
422408
this.template.queryForObject(this.countQuery,
423-
Integer.class, this.region, lock, this.id, ttlEpochMillis())));
409+
Integer.class, this.region, lock, this.id, epochMillis())));
424410
return Boolean.TRUE.equals(result);
425411
}
426412

427413
@Override
428414
public void deleteExpired() {
429415
this.defaultTransactionTemplate.executeWithoutResult(
430416
transactionStatus ->
431-
this.template.update(this.deleteExpiredQuery, this.region, ttlEpochMillis()));
417+
this.template.update(this.deleteExpiredQuery, this.region, epochMillis()));
432418
}
433419

434420
@Override
435-
public boolean renew(String lock) {
421+
public boolean renew(String lock, Duration ttlDuration) {
436422
final Boolean result = this.defaultTransactionTemplate.execute(
437423
transactionStatus ->
438-
this.template.update(this.renewQuery, epochMillis(), this.region, lock, this.id) > 0);
424+
this.template.update(this.renewQuery, ttlEpochMillis(ttlDuration), this.region, lock, this.id) > 0);
439425
return Boolean.TRUE.equals(result);
440426
}
441427

442-
private Timestamp ttlEpochMillis() {
443-
return Timestamp.valueOf(currentTime().minus(this.ttl));
428+
private Timestamp ttlEpochMillis(Duration ttl) {
429+
return Timestamp.valueOf(currentTime().plus(ttl));
444430
}
445431

446432
private static Timestamp epochMillis() {

0 commit comments

Comments
 (0)