GH-3866: DefLockRepository: expose query setters (#8606)

Fixes https://github.com/spring-projects/spring-integration/issues/3866

Some RDBMS vendors (or their JDBC drivers) may just log the problem
without throwing an exception.

* Expose setters for UPDATE and INSERTS queries in the `DefaultLockRepository`
to let end-user to modify them respectively, e.g. be able to add a PostgreSQL
`ON CONFLICT DO NOTHING` hint.
* Refactor `DefaultLockRepository` to `Instant` instead of `LocalDateTime`
with zone offset.
* Refactor `ttl` property to `Duration` type
* Fix `dead-lock` typo to `deadlock`
This commit is contained in:
Artem Bilan
2023-05-01 17:27:20 -04:00
committed by GitHub
parent 33d13a4649
commit 90bc65ea49
4 changed files with 127 additions and 18 deletions

View File

@@ -16,9 +16,8 @@
package org.springframework.integration.jdbc.lock;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.time.Duration;
import java.time.Instant;
import java.util.UUID;
import javax.sql.DataSource;
@@ -66,13 +65,13 @@ public class DefaultLockRepository
/**
* Default value for the time-to-live property.
*/
public static final int DEFAULT_TTL = 10000;
public static final Duration DEFAULT_TTL = Duration.ofSeconds(10);
private final String id;
private final JdbcTemplate template;
private int ttl = DEFAULT_TTL;
private Duration ttl = DEFAULT_TTL;
private String prefix = DEFAULT_TABLE_PREFIX;
@@ -168,11 +167,11 @@ public class DefaultLockRepository
}
/**
* Specify the time (in milliseconds) to expire dead-locks.
* @param timeToLive the time to expire dead-locks.
* Specify the time (in milliseconds) to expire deadlocks.
* @param timeToLive the time to expire deadlocks.
*/
public void setTimeToLive(int timeToLive) {
this.ttl = timeToLive;
this.ttl = Duration.ofMillis(timeToLive);
}
/**
@@ -191,6 +190,99 @@ public class DefaultLockRepository
this.applicationContext = applicationContext;
}
/**
* Set a custom {@code UPDATE} query for a lock record.
* The {@link #getUpdateQuery()} can be used as a template for customization.
* The default query is:
* <pre class="code">
* {@code
* UPDATE %sLOCK
* SET CLIENT_ID=?, CREATED_DATE=?
* WHERE REGION=? AND LOCK_KEY=? AND (CLIENT_ID=? OR CREATED_DATE<?)
* }
* </pre>
* @param updateQuery the query to update a lock record.
* @since 6.1
* @see #getUpdateQuery()
* @see #setPrefix(String)
*/
public void setUpdateQuery(String updateQuery) {
this.updateQuery = updateQuery;
}
/**
* Return the current update query.
* Can be used in a setter as a concatenation of the default query and some extra hint.
* @return the current update query.
* @since 6.1
* @see #setUpdateQuery(String)
*/
public String getUpdateQuery() {
return this.updateQuery;
}
/**
* Set a custom {@code INSERT} query for a lock record.
* The {@link #getInsertQuery()} can be used as a template for customization.
* The default query is
* {@code INSERT INTO %sLOCK (REGION, LOCK_KEY, CLIENT_ID, CREATED_DATE) VALUES (?, ?, ?, ?)}.
* For example a PostgreSQL {@code ON CONFLICT DO NOTHING} hint can be provided like this:
* <pre class="code">
* {@code
* lockRepository.setInsertQuery(lockRepository.getInsertQuery() + " ON CONFLICT DO NOTHING");
* }
* </pre>
* @param insertQuery the insert query for a lock record.
* @since 6.1
* @see #getInsertQuery()
* @see #setPrefix(String)
*/
public void setInsertQuery(String insertQuery) {
this.insertQuery = insertQuery;
}
/**
* Return the current insert query.
* Can be used in a setter as a concatenation of the default query and some extra hint.
* @return the current insert query.
* @since 6.1
* @see #setInsertQuery(String)
*/
public String getInsertQuery() {
return this.insertQuery;
}
/**
* Set a custom {@code INSERT} query for a lock record.
* The {@link #getRenewQuery()} can be used as a template for customization.
* The default query is:
* <pre class="code">
* {@code
* UPDATE %sLOCK
* SET CREATED_DATE=?
* WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=?
* }
* </pre>
* @param renewQuery the update query to renew a lock record.
* @since 6.1
* @see #getRenewQuery()
* @see #setPrefix(String)
*/
public void setRenewQuery(String renewQuery) {
this.renewQuery = renewQuery;
}
/**
* Return the current renew query.
* Can be used in a setter as a concatenation of a default query and some extra hint.
* @return the current renew query.
* @since 6.1
* @see #setRenewQuery(String)
*/
public String getRenewQuery() {
return this.renewQuery;
}
@Override
public void afterPropertiesSet() {
this.deleteQuery = String.format(this.deleteQuery, this.prefix);
@@ -249,14 +341,13 @@ public class DefaultLockRepository
Boolean result =
this.serializableTransactionTemplate.execute(
transactionStatus -> {
if (this.template.update(this.updateQuery, this.id, LocalDateTime.now(ZoneOffset.UTC),
this.region, lock, this.id,
LocalDateTime.now(ZoneOffset.UTC).minus(this.ttl, ChronoUnit.MILLIS)) > 0) {
if (this.template.update(this.updateQuery, this.id, Instant.now(),
this.region, lock, this.id, Instant.now().minus(this.ttl)) > 0) {
return true;
}
try {
return this.template.update(this.insertQuery, this.region, lock, this.id,
LocalDateTime.now(ZoneOffset.UTC)) > 0;
Instant.now()) > 0;
}
catch (DataIntegrityViolationException ex) {
return false;
@@ -272,7 +363,7 @@ public class DefaultLockRepository
Integer.valueOf(1).equals(
this.template.queryForObject(this.countQuery,
Integer.class, this.region, lock, this.id,
LocalDateTime.now(ZoneOffset.UTC).minus(this.ttl, ChronoUnit.MILLIS))));
Instant.now().minus(this.ttl))));
return Boolean.TRUE.equals(result);
}
@@ -280,16 +371,14 @@ public class DefaultLockRepository
public void deleteExpired() {
this.defaultTransactionTemplate.executeWithoutResult(
transactionStatus ->
this.template.update(this.deleteExpiredQuery, this.region,
LocalDateTime.now(ZoneOffset.UTC).minus(this.ttl, ChronoUnit.MILLIS)));
this.template.update(this.deleteExpiredQuery, this.region, Instant.now().minus(this.ttl)));
}
@Override
public boolean renew(String lock) {
final Boolean result = this.defaultTransactionTemplate.execute(
transactionStatus ->
this.template.update(this.renewQuery, LocalDateTime.now(ZoneOffset.UTC),
this.region, lock, this.id) > 0);
this.template.update(this.renewQuery, Instant.now(), this.region, lock, this.id) > 0);
return Boolean.TRUE.equals(result);
}

View File

@@ -24,6 +24,8 @@
<bean id="lockClient" class="org.springframework.integration.jdbc.lock.DefaultLockRepository">
<constructor-arg name="dataSource" ref="dataSource"/>
<property name="insertQuery"
value="INSERT INTO INT_LOCK (REGION, LOCK_KEY, CLIENT_ID, CREATED_DATE) VALUES (?, ?, ?, ?)"/>
</bean>
<tx:annotation-driven/>

View File

@@ -1143,7 +1143,7 @@ The `INT_` can be changed according to the target database design requirements.
Therefore, you must use `prefix` property on the `DefaultLockRepository` bean definition.
Sometimes, one application has moved to such a state that it cannot release the distributed lock and remove the particular record in the database.
For this purpose, such dead-locks can be expired by the other application on the next locking invocation.
For this purpose, such deadlocks can be expired by the other application on the next locking invocation.
The `timeToLive` (TTL) option on the `DefaultLockRepository` is provided for this purpose.
You may also want to specify `CLIENT_ID` for the locks stored for a given `DefaultLockRepository` instance.
If so, you can specify the `id` to be associated with the `DefaultLockRepository` as a constructor parameter.
@@ -1162,6 +1162,17 @@ See its JavaDocs for more information.
String with version 6.0, the `DefaultLockRepository` can be supplied with a `PlatformTransactionManager` instead of relying on the primary bean from the application context.
String with version 6.1, the `DefaultLockRepository` can be configured for custom `insert`, `update` and `renew` queries.
For this purpose the respective setters and getters are exposed.
For example, an insert query for PostgreSQL hint can be configured like this:
====
[source,java]
----
lockRepository.setInsertQuery(lockRepository.getInsertQuery() + " ON CONFLICT DO NOTHING");
----
====
[[jdbc-metadata-store]]
=== JDBC Metadata Store

View File

@@ -80,3 +80,10 @@ See <<./file.adoc#watch-service-directory-scanner, `WatchServiceDirectoryScanner
The Java DSL API for Rabbit Streams (the `RabbitStream` factory) exposes additional properties for simple configurations.
See <<./amqp.adoc#rmq-streams, `RabbitMQ Stream Queue Support`>> for more information.
[[x6.1-jdbc]]
=== JDBC Changes
The `DefaultLockRepository` now exposes setters for `insert`, `update` and `renew` queries.
See <<./jdbc.adoc#jdbc-lock-registry, JDBC Lock Registry>> for more information.