GH-8056: Add LockRequestHandlerAdvice

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

* Add `LockRequestHandlerAdvice` for exclusive service access against specific `key`.
This commit is contained in:
Artem Bilan
2025-03-24 16:59:28 -04:00
parent c10be96396
commit 2c7f796b42
5 changed files with 401 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ In addition to providing the general mechanism to apply AOP advice classes, Spri
* `CacheRequestHandlerAdvice` (described in xref:handler-advice/classes.adoc#cache-advice[Caching Advice])
* `ReactiveRequestHandlerAdvice` (described in xref:handler-advice/reactive.adoc[Reactive Advice])
* `ContextHolderRequestHandlerAdvice` (described in xref:handler-advice/context-holder.adoc[Context Holder Advice])
* `LockRequestHandlerAdvice` (described in xref:handler-advice/lock.adoc[Lock Advice])
[[expression-advice]]

View File

@@ -0,0 +1,49 @@
[[lock-advice]]
= Lock Advice
Starting with version 6.5, the `LockRequestHandlerAdvice` has been introduced.
This advice evaluates a lock key against request message and performs `LockRegistry.executeLocked()` API.
The goal of the advice is to achieve exclusive access to the service invocation according to the `lockKey` context, meaning that different keys may still get concurrent access to the service.
The `LockRequestHandlerAdvice` requires a xref:distributed-locks.adoc[LockRegistry], and a static, SpEL or function-based lock key callback.
If `lockKey` is evaluated to `null`, no locking is held around service call.
However, a `discardChannel` can be provided - and such a message with null key will be sent to this channel instead.
Also, a `waitLockDuration` option can be provided to use `Lock.tryLock(long, TimeUnit)` API instead of `Lock.lockInterruptibly()`.
Following is a sample how a `LockRequestHandlerAdvice` can be used:
[source, java]
----
@Bean
LockRegistry lockRegistry() {
return new DefaultLockRegistry();
}
@Bean
QueueChannel discardChannel() {
return new QueueChannel();
}
@Bean
LockRequestHandlerAdvice lockRequestHandlerAdvice(LockRegistry lockRegistry, QueueChannel discardChannel) {
LockRequestHandlerAdvice lockRequestHandlerAdvice =
new LockRequestHandlerAdvice(lockRegistry, (message) -> message.getHeaders().get(LOCK_KEY_HEADER));
lockRequestHandlerAdvice.setDiscardChannel(discardChannel);
lockRequestHandlerAdvice.setWaitLockDurationExpressionString("'PT1s'");
return lockRequestHandlerAdvice;
}
AtomicInteger counter = new AtomicInteger();
@ServiceActivator(inputChannel = "inputChannel", adviceChain = "lockRequestHandlerAdvice")
String handleWithDelay(String payload) throws InterruptedException {
int currentCount = this.counter.incrementAndGet();
Thread.sleep("longer_process".equals(payload) ? 2000 : 500);
try {
return payload + "-" + currentCount;
}
finally {
this.counter.decrementAndGet();
}
}
----

View File

@@ -31,6 +31,12 @@ The `AbstractMessageChannel` beans now throw a special `MessageDispatchingExcept
In general, it is a design error to try to produce a message from `afterPropertiesSet()`, `@PostConstruct` or bean definition methods.
The `SmartLifecycle.start()` is preferred way for this kind of logic, or better to do that via inbound channel adapters.
[[x6.5-lock-request-handler-advice]]
== The `LockRequestHandlerAdvice`
A new `LockRequestHandlerAdvice` is introduced to keep the lock for a key based on a request message for message handler invocation.
See xref:handler-advice.adoc[] for more information.
[[x6.5-correlation-changes]]
== The `discardIndividuallyOnExpiry` Option For Correlation Handlers