Skip to content
This repository was archived by the owner on Nov 10, 2025. It is now read-only.

Commit ac74dfd

Browse files
committed
Add lock renewal for KinesisChannelAdapter
Related to spring-cloud/spring-cloud-stream-binder-aws-kinesis#148 The current implementation has a flaw when it uses a distributed lock for an exclusive access to shard for consuming only once at start up. Such a behavior cause the problem when we have a network glitch at runtime, so the lock is broken, but consumer is still active to retry consumption attempts * Add `renewLockIfAny()` logic ot the `ShardConsumer`, so we ensure that we are still a lock holder and don't consume otherwise * Add `unlockFuture` logic to block the `ShardConsumer.stop()` until we really got lock unlocked. Otherwise we end up with the race condition when we are still stopping, but already ready to start a new consumer for the same shard
1 parent c15e345 commit ac74dfd

File tree

1 file changed

+104
-9
lines changed

1 file changed

+104
-9
lines changed

src/main/java/org/springframework/integration/aws/inbound/kinesis/KinesisMessageDrivenChannelAdapter.java

Lines changed: 104 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.Map;
3030
import java.util.Queue;
3131
import java.util.Set;
32+
import java.util.concurrent.CompletableFuture;
3233
import java.util.concurrent.ConcurrentHashMap;
3334
import java.util.concurrent.ConcurrentLinkedQueue;
3435
import java.util.concurrent.ConcurrentSkipListSet;
@@ -915,7 +916,17 @@ void setNotifier(Runnable notifier) {
915916
void stop() {
916917
this.state = ConsumerState.STOP;
917918
if (KinesisMessageDrivenChannelAdapter.this.lockRegistry != null) {
918-
KinesisMessageDrivenChannelAdapter.this.shardConsumerManager.unlock(this.key);
919+
LockCompletableFuture unlockFuture = new LockCompletableFuture(this.key);
920+
KinesisMessageDrivenChannelAdapter.this.shardConsumerManager.unlock(unlockFuture);
921+
try {
922+
unlockFuture.get(1, TimeUnit.SECONDS);
923+
}
924+
catch (Exception ex) {
925+
if (ex instanceof InterruptedException) {
926+
Thread.currentThread().interrupt();
927+
}
928+
logger.info("The lock for key '" + this.key + "' was not unlocked in time", ex);
929+
}
919930
}
920931
if (this.notifier != null) {
921932
this.notifier.run();
@@ -929,6 +940,10 @@ void close() {
929940

930941
void execute() {
931942
if (this.task == null) {
943+
if (!renewLockIfAny()) {
944+
return;
945+
}
946+
932947
switch (this.state) {
933948
case NEW:
934949
case EXPIRED:
@@ -1007,6 +1022,36 @@ void execute() {
10071022
}
10081023
}
10091024

1025+
private boolean renewLockIfAny() {
1026+
if (KinesisMessageDrivenChannelAdapter.this.lockRegistry != null && this.state == ConsumerState.CONSUME) {
1027+
LockCompletableFuture renewLockFuture = new LockCompletableFuture(this.key);
1028+
KinesisMessageDrivenChannelAdapter.this.shardConsumerManager.renewLock(renewLockFuture);
1029+
boolean lockRenewed = false;
1030+
try {
1031+
lockRenewed = renewLockFuture.get(1, TimeUnit.SECONDS);
1032+
}
1033+
catch (Exception ex) {
1034+
if (ex instanceof InterruptedException) {
1035+
Thread.currentThread().interrupt();
1036+
}
1037+
logger.info("The lock for key '" + this.key + "' was not renewed in time", ex);
1038+
}
1039+
1040+
if (!lockRenewed && this.state == ConsumerState.CONSUME) {
1041+
this.state = ConsumerState.STOP;
1042+
this.checkpointer.close();
1043+
if (this.notifier != null) {
1044+
this.notifier.run();
1045+
}
1046+
if (KinesisMessageDrivenChannelAdapter.this.active) {
1047+
KinesisMessageDrivenChannelAdapter.this.shardConsumerManager.addShardToConsume(this.shardOffset);
1048+
}
1049+
return false;
1050+
}
1051+
}
1052+
return true;
1053+
}
1054+
10101055
private Runnable processTask() {
10111056
return () -> {
10121057
GetRecordsRequest getRecordsRequest = new GetRecordsRequest();
@@ -1368,7 +1413,9 @@ private final class ShardConsumerManager implements SchedulingAwareRunnable {
13681413

13691414
private final Map<String, Lock> locks = new HashMap<>();
13701415

1371-
private final Queue<String> forUnlocking = new ConcurrentLinkedQueue<>();
1416+
private final Queue<LockCompletableFuture> forUnlocking = new ConcurrentLinkedQueue<>();
1417+
1418+
private final Queue<LockCompletableFuture> forRenewing = new ConcurrentLinkedQueue<>();
13721419

13731420
ShardConsumerManager() {
13741421
}
@@ -1379,15 +1426,18 @@ void addShardToConsume(KinesisShardOffset kinesisShardOffset) {
13791426
this.shardOffsetsToConsumer.put(lockKey, kinesisShardOffset);
13801427
}
13811428

1382-
void unlock(String lockKey) {
1383-
this.forUnlocking.add(lockKey);
1429+
void unlock(LockCompletableFuture unlockFuture) {
1430+
this.forUnlocking.add(unlockFuture);
1431+
}
1432+
1433+
void renewLock(LockCompletableFuture renewLockFuture) {
1434+
this.forRenewing.add(renewLockFuture);
13841435
}
13851436

13861437
@Override
13871438
public void run() {
13881439
try {
13891440
while (!Thread.currentThread().isInterrupted()) {
1390-
13911441
this.shardOffsetsToConsumer
13921442
.entrySet()
13931443
.removeIf(
@@ -1418,9 +1468,9 @@ public void run() {
14181468
});
14191469

14201470
while (KinesisMessageDrivenChannelAdapter.this.lockRegistry != null) {
1421-
String lockKey = this.forUnlocking.poll();
1422-
if (lockKey != null) {
1423-
Lock lock = this.locks.remove(lockKey);
1471+
LockCompletableFuture forUnlocking = this.forUnlocking.poll();
1472+
if (forUnlocking != null) {
1473+
Lock lock = this.locks.remove(forUnlocking.lockKey);
14241474
if (lock != null) {
14251475
try {
14261476
lock.unlock();
@@ -1429,13 +1479,48 @@ public void run() {
14291479
logger.error("Error during unlocking: " + lock, e);
14301480
}
14311481
}
1482+
forUnlocking.complete(true);
14321483
}
14331484
else {
14341485
break;
14351486
}
14361487
}
14371488

1438-
sleep(250, new IllegalStateException("ShardConsumerManager Thread [" + this + "] has been interrupted"), true);
1489+
while (KinesisMessageDrivenChannelAdapter.this.lockRegistry != null) {
1490+
LockCompletableFuture lockFuture = this.forRenewing.poll();
1491+
if (lockFuture != null) {
1492+
Lock lock = this.locks.get(lockFuture.lockKey);
1493+
if (lock != null) {
1494+
try {
1495+
if (lock.tryLock()) {
1496+
try {
1497+
lockFuture.complete(true);
1498+
}
1499+
finally {
1500+
lock.unlock();
1501+
}
1502+
}
1503+
else {
1504+
lockFuture.complete(false);
1505+
this.locks.remove(lockFuture.lockKey);
1506+
}
1507+
}
1508+
catch (Exception e) {
1509+
logger.error("Error during locking: " + lock, e);
1510+
}
1511+
}
1512+
else {
1513+
lockFuture.complete(false);
1514+
}
1515+
}
1516+
else {
1517+
break;
1518+
}
1519+
}
1520+
1521+
sleep(250,
1522+
new IllegalStateException("ShardConsumerManager Thread [" + this + "] has been interrupted"),
1523+
true);
14391524
}
14401525
}
14411526
finally {
@@ -1461,4 +1546,14 @@ public boolean isLongLived() {
14611546

14621547
}
14631548

1549+
private final static class LockCompletableFuture extends CompletableFuture<Boolean> {
1550+
1551+
private final String lockKey;
1552+
1553+
LockCompletableFuture(String lockKey) {
1554+
this.lockKey = lockKey;
1555+
}
1556+
1557+
}
1558+
14641559
}

0 commit comments

Comments
 (0)