Skip to content

Commit 17b63da

Browse files
committed
spring-projectsGH-3444: Custom TTL per LOCK in JdbcLockRegistry
1 parent 5e6ae90 commit 17b63da

File tree

18 files changed

+235
-148
lines changed

18 files changed

+235
-148
lines changed

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() {

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

+75-11
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.
@@ -28,6 +28,8 @@
2828
import org.springframework.dao.CannotAcquireLockException;
2929
import org.springframework.dao.DataAccessResourceFailureException;
3030
import org.springframework.dao.TransientDataAccessException;
31+
import org.springframework.integration.support.locks.CustomTtlLock;
32+
import org.springframework.integration.support.locks.CustomTtlLockRegistry;
3133
import org.springframework.integration.support.locks.ExpirableLockRegistry;
3234
import org.springframework.integration.support.locks.RenewableLockRegistry;
3335
import org.springframework.integration.util.UUIDConverter;
@@ -56,10 +58,11 @@
5658
* @author Unseok Kim
5759
* @author Christian Tzolov
5860
* @author Myeonghyeon Lee
61+
* @author Eddie Cho
5962
*
6063
* @since 4.3
6164
*/
62-
public class JdbcLockRegistry implements ExpirableLockRegistry, RenewableLockRegistry {
65+
public class JdbcLockRegistry implements ExpirableLockRegistry, CustomTtlLockRegistry, RenewableLockRegistry {
6366

6467
private static final int DEFAULT_IDLE = 100;
6568

@@ -83,12 +86,25 @@ protected boolean removeEldestEntry(Entry<String, JdbcLock> eldest) {
8386

8487
private int cacheCapacity = DEFAULT_CAPACITY;
8588

89+
/**
90+
* Default value for the time-to-live property.
91+
*/
92+
public static final Duration DEFAULT_TTL = Duration.ofSeconds(10);
93+
94+
private final Duration ttl;
95+
8696
/**
8797
* Construct an instance based on the provided {@link LockRepository}.
8898
* @param client the {@link LockRepository} to rely on.
8999
*/
90100
public JdbcLockRegistry(LockRepository client) {
91101
this.client = client;
102+
this.ttl = DEFAULT_TTL;
103+
}
104+
105+
public JdbcLockRegistry(LockRepository client, long expireAfter) {
106+
this.client = client;
107+
this.ttl = convertToDuration(expireAfter, TimeUnit.MILLISECONDS);
92108
}
93109

94110
/**
@@ -113,6 +129,11 @@ public void setCacheCapacity(int cacheCapacity) {
113129

114130
@Override
115131
public Lock obtain(Object lockKey) {
132+
return this.obtainCustomTtlLock(lockKey);
133+
}
134+
135+
@Override
136+
public CustomTtlLock obtainCustomTtlLock(Object lockKey) {
116137
Assert.isInstanceOf(String.class, lockKey);
117138
String path = pathFor((String) lockKey);
118139
this.lock.lock();
@@ -165,7 +186,12 @@ public void renewLock(Object lockKey) {
165186
}
166187
}
167188

168-
private static final class JdbcLock implements Lock {
189+
private static Duration convertToDuration(long time, TimeUnit timeUnit) {
190+
long timeInMilliseconds = TimeUnit.MILLISECONDS.convert(time, timeUnit);
191+
return Duration.ofMillis(timeInMilliseconds);
192+
}
193+
194+
private final class JdbcLock implements CustomTtlLock {
169195

170196
private final LockRepository mutex;
171197

@@ -189,10 +215,20 @@ public long getLastUsed() {
189215

190216
@Override
191217
public void lock() {
218+
lock(JdbcLockRegistry.this.ttl);
219+
}
220+
221+
@Override
222+
public void lock(long customTtl, TimeUnit customTtlUnit) {
223+
Duration customTtlDuration = convertToDuration(customTtl, customTtlUnit);
224+
lock(customTtlDuration);
225+
}
226+
227+
private void lock(Duration ttl) {
192228
this.delegate.lock();
193229
while (true) {
194230
try {
195-
while (!doLock()) {
231+
while (!doLock(ttl)) {
196232
Thread.sleep(this.idleBetweenTries.toMillis());
197233
}
198234
break;
@@ -223,7 +259,7 @@ public void lockInterruptibly() throws InterruptedException {
223259
this.delegate.lockInterruptibly();
224260
while (true) {
225261
try {
226-
while (!doLock()) {
262+
while (!doLock(JdbcLockRegistry.this.ttl)) {
227263
Thread.sleep(this.idleBetweenTries.toMillis());
228264
if (Thread.currentThread().isInterrupted()) {
229265
throw new InterruptedException();
@@ -259,6 +295,16 @@ public boolean tryLock() {
259295

260296
@Override
261297
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
298+
return tryLock(time, unit, JdbcLockRegistry.this.ttl);
299+
}
300+
301+
@Override
302+
public boolean tryLock(long time, TimeUnit unit, long customTtl, TimeUnit customTtlUnit) throws InterruptedException {
303+
Duration customTtlDuration = convertToDuration(customTtl, customTtlUnit);
304+
return tryLock(time, unit, customTtlDuration);
305+
}
306+
307+
private boolean tryLock(long time, TimeUnit unit, Duration ttl) throws InterruptedException {
262308
long now = System.currentTimeMillis();
263309
if (!this.delegate.tryLock(time, unit)) {
264310
return false;
@@ -267,7 +313,7 @@ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
267313
boolean acquired;
268314
while (true) {
269315
try {
270-
while (!(acquired = doLock()) && System.currentTimeMillis() < expire) { //NOSONAR
316+
while (!(acquired = doLock(ttl)) && System.currentTimeMillis() < expire) { //NOSONAR
271317
Thread.sleep(this.idleBetweenTries.toMillis());
272318
}
273319
if (!acquired) {
@@ -285,8 +331,8 @@ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
285331
}
286332
}
287333

288-
private boolean doLock() {
289-
boolean acquired = this.mutex.acquire(this.path);
334+
private boolean doLock(Duration ttl) {
335+
boolean acquired = this.mutex.acquire(this.path, ttl);
290336
if (acquired) {
291337
this.lastUsed = System.currentTimeMillis();
292338
}
@@ -305,13 +351,22 @@ public void unlock() {
305351
try {
306352
while (true) {
307353
try {
308-
this.mutex.delete(this.path);
309-
return;
354+
if (this.mutex.delete(this.path)) {
355+
return;
356+
}
357+
else {
358+
throw new IllegalStateException();
359+
// the lock is no longer owned by current process, the exception should be handle and rollback the execution result
360+
}
310361
}
311362
catch (TransientDataAccessException | TransactionTimedOutException | TransactionSystemException e) {
312363
// try again
313364
}
314365
catch (Exception e) {
366+
if (e instanceof IllegalStateException) {
367+
throw new IllegalStateException("Lock was released in the store due to expiration. " +
368+
"The integrity of data protected by this lock may have been compromised.");
369+
}
315370
throw new DataAccessResourceFailureException("Failed to release mutex at " + this.path, e);
316371
}
317372
}
@@ -331,12 +386,21 @@ public boolean isAcquiredInThisProcess() {
331386
}
332387

333388
public boolean renew() {
389+
return renew(JdbcLockRegistry.this.ttl);
390+
}
391+
392+
public boolean renew(long customTtl, TimeUnit customTtlTimeUnit) {
393+
Duration customTtlDuration = convertToDuration(customTtl, customTtlTimeUnit);
394+
return renew(customTtlDuration);
395+
}
396+
397+
private boolean renew(Duration ttl) {
334398
if (!this.delegate.isHeldByCurrentThread()) {
335399
throw new IllegalMonitorStateException("The current thread doesn't own mutex at " + this.path);
336400
}
337401
while (true) {
338402
try {
339-
boolean renewed = this.mutex.renew(this.path);
403+
boolean renewed = this.mutex.renew(this.path, ttl);
340404
if (renewed) {
341405
this.lastUsed = System.currentTimeMillis();
342406
}

0 commit comments

Comments
 (0)