This is an automated email from the ASF dual-hosted git repository.
pvillard31 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 306830fcd02 NIFI-16066 Release lingering rename in ConsumeKinesis
(#11385)
306830fcd02 is described below
commit 306830fcd0220ae994b3c1f7f2c5e42aacfcc1fc
Author: Alaksiej Ščarbaty <[email protected]>
AuthorDate: Thu Jul 2 17:03:31 2026 +0200
NIFI-16066 Release lingering rename in ConsumeKinesis (#11385)
* NIFI-16066 Release lingering rename lock when error occurs in
ConsumeKinesis
When an error (e.g. a failed checkpoint copy) occurred while a node held
the rename lock in LegacyCheckpointMigrator, the renameOwner marker was
left on the migration table, blocking all future rename attempts until
the stale-lock timeout elapsed. Release the rename lock on error, on all
three lock-holding paths, so the rename can be retried. The release is
owner-guarded (renameOwner = :owner) so a slow-failing node cannot wipe a
lock another node has already force-acquired via the stale-lock takeover,
and it is best-effort so a transient DynamoDB error while releasing does
not mask the original failure.
Also make waitForTableRenamed treat a rename as complete only once the
migration table has been dropped, not merely when the checkpoint table
has the new schema. The new checkpoint table is created empty before the
copy runs, so schema presence alone does not indicate the migration
finished.
---
.../aws/kinesis/LegacyCheckpointMigrator.java | 81 +++++++++++++-----
.../aws/kinesis/KinesisShardManagerTest.java | 95 ++++++++++++++++++++++
2 files changed, 156 insertions(+), 20 deletions(-)
diff --git
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/main/java/org/apache/nifi/processors/aws/kinesis/LegacyCheckpointMigrator.java
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/main/java/org/apache/nifi/processors/aws/kinesis/LegacyCheckpointMigrator.java
index 3118b70d1a0..e42e6c7023d 100644
---
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/main/java/org/apache/nifi/processors/aws/kinesis/LegacyCheckpointMigrator.java
+++
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/main/java/org/apache/nifi/processors/aws/kinesis/LegacyCheckpointMigrator.java
@@ -85,9 +85,14 @@ final class LegacyCheckpointMigrator {
logger.info("Checkpoint table [{}] is in place but migration table
[{}] still exists from a prior rename; not all checkpoints may have been
migrated, re-copying before deletion",
checkpointTableName, lingeringMigration);
if (acquireRenameLock(lingeringMigration)) {
- CheckpointTableUtils.waitForTableActive(dynamoDbClient, logger,
lingeringMigration);
- CheckpointTableUtils.copyCheckpointItems(dynamoDbClient, logger,
lingeringMigration, checkpointTableName);
- CheckpointTableUtils.deleteTable(dynamoDbClient, logger,
lingeringMigration);
+ try {
+ CheckpointTableUtils.waitForTableActive(dynamoDbClient,
logger, lingeringMigration);
+ CheckpointTableUtils.copyCheckpointItems(dynamoDbClient,
logger, lingeringMigration, checkpointTableName);
+ CheckpointTableUtils.deleteTable(dynamoDbClient, logger,
lingeringMigration);
+ } catch (final Exception e) {
+ clearRenameLock(lingeringMigration);
+ throw e;
+ }
} else {
waitForTableRenamed(lingeringMigration);
}
@@ -114,12 +119,17 @@ final class LegacyCheckpointMigrator {
void renameMigrationTable(final String migrationTableName) {
if (acquireRenameLock(migrationTableName)) {
- CheckpointTableUtils.deleteTable(dynamoDbClient, logger,
checkpointTableName);
- CheckpointTableUtils.waitForTableDeleted(dynamoDbClient, logger,
checkpointTableName);
- CheckpointTableUtils.createNewSchemaTable(dynamoDbClient, logger,
checkpointTableName);
- CheckpointTableUtils.waitForTableActive(dynamoDbClient, logger,
checkpointTableName);
- CheckpointTableUtils.copyCheckpointItems(dynamoDbClient, logger,
migrationTableName, checkpointTableName);
- CheckpointTableUtils.deleteTable(dynamoDbClient, logger,
migrationTableName);
+ try {
+ CheckpointTableUtils.deleteTable(dynamoDbClient, logger,
checkpointTableName);
+ CheckpointTableUtils.waitForTableDeleted(dynamoDbClient,
logger, checkpointTableName);
+ CheckpointTableUtils.createNewSchemaTable(dynamoDbClient,
logger, checkpointTableName);
+ CheckpointTableUtils.waitForTableActive(dynamoDbClient,
logger, checkpointTableName);
+ CheckpointTableUtils.copyCheckpointItems(dynamoDbClient,
logger, migrationTableName, checkpointTableName);
+ CheckpointTableUtils.deleteTable(dynamoDbClient, logger,
migrationTableName);
+ } catch (final Exception e) {
+ clearRenameLock(migrationTableName);
+ throw e;
+ }
} else {
waitForTableRenamed(migrationTableName);
}
@@ -181,11 +191,38 @@ final class LegacyCheckpointMigrator {
"shardId",
AttributeValue.builder().s(CheckpointTableUtils.MIGRATION_MARKER_SHARD_ID).build());
}
+ private void clearRenameLock(final String migrationTableName) {
+ try {
+ final UpdateItemRequest request = UpdateItemRequest.builder()
+ .tableName(migrationTableName)
+ .key(migrationMarkerKey())
+ .updateExpression("REMOVE renameOwner, renameStartedAt")
+ .conditionExpression("renameOwner = :owner")
+ .expressionAttributeValues(Map.of(
+ ":owner",
AttributeValue.builder().s(nodeId).build()))
+ .build();
+ dynamoDbClient.updateItem(request);
+ logger.info("Released rename lock on migration table [{}] after an
error", migrationTableName);
+ } catch (final ConditionalCheckFailedException e) {
+ logger.debug("Rename lock on migration table [{}] is no longer
held by this node; leaving it in place", migrationTableName);
+ } catch (final ResourceNotFoundException e) {
+ logger.debug("Migration table [{}] no longer exists; rename lock
already released", migrationTableName);
+ } catch (final Exception e) {
+ logger.warn("Failed to release rename lock on migration table [{}]
after an error; it will expire via the stale-lock timeout",
+ migrationTableName, e);
+ }
+ }
+
+ private boolean isRenameCompleted(final String migrationTableName) {
+ return CheckpointTableUtils.getTableSchema(dynamoDbClient,
checkpointTableName) == CheckpointTableUtils.TableSchema.NEW
+ && CheckpointTableUtils.getTableSchema(dynamoDbClient,
migrationTableName) == CheckpointTableUtils.TableSchema.NOT_FOUND;
+ }
+
private void waitForTableRenamed(final String migrationTableName) {
for (int i = 0; i < RENAME_POLL_MAX_ATTEMPTS; i++) {
- if (CheckpointTableUtils.getTableSchema(dynamoDbClient,
checkpointTableName)
- == CheckpointTableUtils.TableSchema.NEW) {
- logger.info("Migration table rename complete; table [{}] is
now available", checkpointTableName);
+ if (isRenameCompleted(migrationTableName)) {
+ logger.info("Migration table rename complete; table [{}] is
now available and migration table [{}] has been dropped",
+ checkpointTableName, migrationTableName);
return;
}
@@ -199,14 +236,18 @@ final class LegacyCheckpointMigrator {
logger.warn("Timed out waiting for migration table rename; attempting
stale lock takeover");
if (forceAcquireStaleRenameLock(migrationTableName)) {
- CheckpointTableUtils.deleteTable(dynamoDbClient, logger,
checkpointTableName);
- CheckpointTableUtils.waitForTableDeleted(dynamoDbClient, logger,
checkpointTableName);
- CheckpointTableUtils.createNewSchemaTable(dynamoDbClient, logger,
checkpointTableName);
- CheckpointTableUtils.waitForTableActive(dynamoDbClient, logger,
checkpointTableName);
- CheckpointTableUtils.copyCheckpointItems(dynamoDbClient, logger,
migrationTableName, checkpointTableName);
- CheckpointTableUtils.deleteTable(dynamoDbClient, logger,
migrationTableName);
- } else if (CheckpointTableUtils.getTableSchema(dynamoDbClient,
checkpointTableName)
- == CheckpointTableUtils.TableSchema.NEW) {
+ try {
+ CheckpointTableUtils.deleteTable(dynamoDbClient, logger,
checkpointTableName);
+ CheckpointTableUtils.waitForTableDeleted(dynamoDbClient,
logger, checkpointTableName);
+ CheckpointTableUtils.createNewSchemaTable(dynamoDbClient,
logger, checkpointTableName);
+ CheckpointTableUtils.waitForTableActive(dynamoDbClient,
logger, checkpointTableName);
+ CheckpointTableUtils.copyCheckpointItems(dynamoDbClient,
logger, migrationTableName, checkpointTableName);
+ CheckpointTableUtils.deleteTable(dynamoDbClient, logger,
migrationTableName);
+ } catch (final Exception e) {
+ clearRenameLock(migrationTableName);
+ throw e;
+ }
+ } else if (isRenameCompleted(migrationTableName)) {
logger.info("Migration table rename completed during takeover
attempt");
} else {
throw new ProcessException(
diff --git
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/test/java/org/apache/nifi/processors/aws/kinesis/KinesisShardManagerTest.java
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/test/java/org/apache/nifi/processors/aws/kinesis/KinesisShardManagerTest.java
index c76ba094e3d..57cd94cb2eb 100644
---
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/test/java/org/apache/nifi/processors/aws/kinesis/KinesisShardManagerTest.java
+++
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/test/java/org/apache/nifi/processors/aws/kinesis/KinesisShardManagerTest.java
@@ -51,9 +51,12 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -362,6 +365,98 @@ class KinesisShardManagerTest {
"Migration table must be deleted only after the recovery copy
succeeds");
}
+ /**
+ * When an error occurs while a node holds the rename lock (for example
the checkpoint copy
+ * fails after the new checkpoint table has been recreated), the rename
lock on the migration
+ * table is released so a later attempt can re-acquire it and retry, and
the migration table
+ * is left in place so no checkpoints are lost.
+ */
+ @Test
+ void testRenameMigrationTableReleasesRenameLockOnCopyError() {
+ final AtomicInteger mainTableDescribeCount = new AtomicInteger();
+
when(dynamoDb.describeTable(any(DescribeTableRequest.class))).thenAnswer(invocation
-> {
+ final DescribeTableRequest req = invocation.getArgument(0);
+ if ("test-table".equals(req.tableName())) {
+ if (mainTableDescribeCount.incrementAndGet() <= 3) {
+ throw ResourceNotFoundException.builder().build();
+ }
+ return newSchemaActiveResponse();
+ }
+ if ("test-table_migration".equals(req.tableName())) {
+ return newSchemaActiveResponse();
+ }
+ throw ResourceNotFoundException.builder().build();
+ });
+
+
when(dynamoDb.updateItem(any(UpdateItemRequest.class))).thenReturn(UpdateItemResponse.builder().build());
+
when(dynamoDb.createTable(any(CreateTableRequest.class))).thenReturn(CreateTableResponse.builder().build());
+
when(dynamoDb.deleteTable(any(DeleteTableRequest.class))).thenReturn(DeleteTableResponse.builder().build());
+
+ final Map<String, AttributeValue> checkpointItem = Map.of(
+ "streamName",
AttributeValue.builder().s("test-stream").build(),
+ "shardId", AttributeValue.builder().s("shard-1").build(),
+ "sequenceNumber", AttributeValue.builder().s("12345").build());
+
when(dynamoDb.scan(any(ScanRequest.class))).thenReturn(ScanResponse.builder().items(checkpointItem).build());
+
+ final InternalServerErrorException copyFailure =
InternalServerErrorException.builder()
+ .message("simulated transient DynamoDB failure during
checkpoint copy")
+ .build();
+
when(dynamoDb.putItem(any(PutItemRequest.class))).thenThrow(copyFailure);
+
+ assertThrows(InternalServerErrorException.class, () ->
manager.ensureCheckpointTableExists(),
+ "A failed checkpoint copy under the rename lock must
propagate");
+
+ final ArgumentCaptor<UpdateItemRequest> updateCaptor =
ArgumentCaptor.forClass(UpdateItemRequest.class);
+ verify(dynamoDb, atLeastOnce()).updateItem(updateCaptor.capture());
+ final boolean lockReleased = updateCaptor.getAllValues().stream()
+ .anyMatch(req -> "test-table_migration".equals(req.tableName())
+ && req.updateExpression() != null
+ && req.updateExpression().startsWith("REMOVE
renameOwner"));
+ assertTrue(lockReleased, "Rename lock (renameOwner) must be released
on the migration table when the rename fails");
+
+ final ArgumentCaptor<DeleteTableRequest> deleteCaptor =
ArgumentCaptor.forClass(DeleteTableRequest.class);
+ verify(dynamoDb, times(1)).deleteTable(deleteCaptor.capture());
+ final boolean migrationDeleted = deleteCaptor.getAllValues().stream()
+ .map(DeleteTableRequest::tableName)
+ .anyMatch("test-table_migration"::equals);
+ assertFalse(migrationDeleted, "Migration table must not be deleted
when the rename copy fails");
+ }
+
+ /**
+ * A node that does not hold the rename lock treats the rename as complete
only once the
+ * migration table has been dropped. Because the new checkpoint table is
created empty before
+ * the checkpoint copy runs, the presence of the new schema on the
checkpoint table alone is
+ * not sufficient to conclude the migration finished.
+ */
+ @Test
+ void testWaitForTableRenamedRequiresMigrationTableDropped() {
+ final AtomicInteger migrationDescribeCount = new AtomicInteger();
+
when(dynamoDb.describeTable(any(DescribeTableRequest.class))).thenAnswer(invocation
-> {
+ final DescribeTableRequest req = invocation.getArgument(0);
+ if ("test-table".equals(req.tableName())) {
+ return newSchemaActiveResponse();
+ }
+ if ("test-table_migration".equals(req.tableName())) {
+ // Count 1: findMigrationTable() in
completeLingeringMigration; count 2+: waitForTableRenamed poll loop.
+ if (migrationDescribeCount.incrementAndGet() == 1) {
+ return newSchemaActiveResponse();
+ }
+ throw ResourceNotFoundException.builder().build();
+ }
+ throw ResourceNotFoundException.builder().build();
+ });
+
+ // Losing the rename-lock race: acquireRenameLock's conditional update
fails.
+ when(dynamoDb.updateItem(any(UpdateItemRequest.class)))
+ .thenThrow(ConditionalCheckFailedException.builder().build());
+
+ manager.ensureCheckpointTableExists();
+
+ assertTrue(migrationDescribeCount.get() >= 2,
+ "waitForTableRenamed must confirm the migration table was
dropped, not just that the checkpoint table has the new schema");
+ verify(dynamoDb, never()).deleteTable(any(DeleteTableRequest.class));
+ }
+
private static DescribeTableResponse newSchemaActiveResponse() {
final KeySchemaElement hashKey = KeySchemaElement.builder()
.attributeName("streamName")