This is an automated email from the ASF dual-hosted git repository.
exceptionfactory 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 374e7cc68b9 NIFI-16284 Wait for Load Balancing to complete in
ClusteredConnectorBacklogIT (#11615)
374e7cc68b9 is described below
commit 374e7cc68b961c0941a517b52543f4720969fffe
Author: Mark Payne <[email protected]>
AuthorDate: Wed Sep 2 21:45:45 2026 -0400
NIFI-16284 Wait for Load Balancing to complete in
ClusteredConnectorBacklogIT (#11615)
- Complete Process Group drop requests only after every connection finishes
- Describe aggregate drop request lifecycle in StandardProcessGroup
Signed-off-by: David Handermann <[email protected]>
---
.../apache/nifi/groups/StandardProcessGroup.java | 17 +++++++
.../nifi/groups/StandardProcessGroupTest.java | 57 ++++++++++++++++++++++
.../connectors/ClusteredConnectorBacklogIT.java | 54 ++++++++++++++------
3 files changed, 112 insertions(+), 16 deletions(-)
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 649aac012de..dec748330ef 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -1660,6 +1660,23 @@ public final class StandardProcessGroup implements
ProcessGroup {
aggregateDropFlowFileStatus.setOriginalSize(aggregateOriginalSize);
aggregateDropFlowFileStatus.setDroppedSize(aggregateDroppedSize);
aggregateDropFlowFileStatus.setCurrentSize(aggregateCurrentSize);
+
+ // Merging never moves the aggregate request into a COMPLETE state;
only handleDropAllFlowFiles does that. The
+ // lifecycle is:
+ // 1. handleDropAllFlowFiles creates the aggregate request and
merges each connection's drop request into it
+ // by calling this method once per connection.
+ // 2. A drop request against an empty queue is already COMPLETE when
it is created. Copying that state onto
+ // the aggregate request would make DropFlowFileRequest.setState
complete the aggregate's Future, which
+ // callers treat as "every connection has been drained", even
though the connections merged later may
+ // still be dropping. The aggregate request is therefore held at
DROPPING_FLOWFILES while merging.
+ // 3. Once every connection has been merged, handleDropAllFlowFiles
sets the aggregate request to COMPLETE,
+ // or to FAILURE, when the Futures of all of those connections
finish. Those Futures are already finished
+ // when each connection's queue was empty, in which case the
aggregate request becomes COMPLETE without
+ // ever waiting.
+ if (aggregateState == DropFlowFileState.COMPLETE) {
+ aggregateState = DropFlowFileState.DROPPING_FLOWFILES;
+ }
+
aggregateDropFlowFileStatus.setState(aggregateState);
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/groups/StandardProcessGroupTest.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/groups/StandardProcessGroupTest.java
index 0e46d63531f..73d41805d43 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/groups/StandardProcessGroupTest.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/groups/StandardProcessGroupTest.java
@@ -21,11 +21,19 @@ import org.apache.nifi.components.state.Scope;
import org.apache.nifi.components.state.StateManager;
import org.apache.nifi.components.state.StateManagerProvider;
import org.apache.nifi.components.state.StateMap;
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.connectable.ConnectableType;
+import org.apache.nifi.connectable.Connection;
import org.apache.nifi.controller.ClusterTopologyProvider;
import org.apache.nifi.controller.NodeTypeProvider;
import org.apache.nifi.controller.ProcessScheduler;
import org.apache.nifi.controller.ReloadComponent;
import org.apache.nifi.controller.flow.FlowManager;
+import org.apache.nifi.controller.queue.DropFlowFileRequest;
+import org.apache.nifi.controller.queue.DropFlowFileState;
+import org.apache.nifi.controller.queue.DropFlowFileStatus;
+import org.apache.nifi.controller.queue.FlowFileQueue;
+import org.apache.nifi.controller.queue.QueueSize;
import org.apache.nifi.controller.service.ControllerServiceProvider;
import org.apache.nifi.encrypt.PropertyEncryptor;
import org.apache.nifi.flow.ExecutionEngine;
@@ -52,6 +60,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -394,6 +403,33 @@ class StandardProcessGroupTest {
assertEquals(expected, leaf.getLoggingAttributes());
}
+ @Test
+ void testDropAllFlowFilesCompletionWaitsForEveryConnection() {
+ when(flowManager.getFlowAnalyzer()).thenReturn(Optional.empty());
+
+ final DropFlowFileRequest emptyDrop = new
DropFlowFileRequest("drop-all");
+ emptyDrop.setOriginalSize(new QueueSize(0, 0L));
+ emptyDrop.setCurrentSize(new QueueSize(0, 0L));
+ emptyDrop.setState(DropFlowFileState.COMPLETE);
+
+ final DropFlowFileRequest pendingDrop = new
DropFlowFileRequest("drop-all");
+ pendingDrop.setOriginalSize(new QueueSize(300, 300L));
+ pendingDrop.setCurrentSize(new QueueSize(300, 300L));
+ pendingDrop.setState(DropFlowFileState.DROPPING_FLOWFILES);
+
+ addConnectionWithDropStatus("empty-connection", emptyDrop);
+ addConnectionWithDropStatus("queued-connection", pendingDrop);
+
+ final DropFlowFileStatus dropStatus =
processGroup.dropAllFlowFiles("drop-all", "test-user");
+ final CompletableFuture<Void> completionFuture =
dropStatus.getCompletionFuture();
+
+ assertFalse(completionFuture.isDone());
+
+ pendingDrop.setState(DropFlowFileState.COMPLETE);
+
+ assertTrue(completionFuture.isDone());
+ }
+
private StandardProcessGroup createStandardProcessGroup(final String id) {
return new StandardProcessGroup(
id,
@@ -413,4 +449,25 @@ class StandardProcessGroupTest {
);
}
+ private void addConnectionWithDropStatus(final String connectionId, final
DropFlowFileStatus dropStatus) {
+ final Connectable source = mock(Connectable.class);
+ final Connectable destination = mock(Connectable.class);
+
when(source.getConnectableType()).thenReturn(ConnectableType.PROCESSOR);
+
when(destination.getConnectableType()).thenReturn(ConnectableType.PROCESSOR);
+ when(source.getProcessGroup()).thenReturn(processGroup);
+ when(destination.getProcessGroup()).thenReturn(processGroup);
+
+ final FlowFileQueue flowFileQueue = mock(FlowFileQueue.class);
+ when(flowFileQueue.dropFlowFiles(eq("drop-all"),
eq("test-user"))).thenReturn(dropStatus);
+
+ final Connection connection = mock(Connection.class);
+ when(connection.getIdentifier()).thenReturn(connectionId);
+ when(connection.getSource()).thenReturn(source);
+ when(connection.getDestination()).thenReturn(destination);
+ when(connection.getFlowFileQueue()).thenReturn(flowFileQueue);
+
when(connection.getVersionedComponentId()).thenReturn(Optional.empty());
+
+ processGroup.addConnection(connection);
+ }
+
}
diff --git
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClusteredConnectorBacklogIT.java
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClusteredConnectorBacklogIT.java
index 13b28c267da..89f19c96c94 100644
---
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClusteredConnectorBacklogIT.java
+++
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClusteredConnectorBacklogIT.java
@@ -21,7 +21,10 @@ import org.apache.nifi.tests.system.NiFiInstanceFactory;
import org.apache.nifi.tests.system.NiFiSystemIT;
import org.apache.nifi.toolkit.client.NiFiClientException;
import org.apache.nifi.web.api.dto.BacklogDTO;
+import org.apache.nifi.web.api.dto.ConnectionDTO;
+import org.apache.nifi.web.api.entity.ConnectionEntity;
import org.apache.nifi.web.api.entity.ConnectorEntity;
+import org.apache.nifi.web.api.entity.ProcessGroupFlowEntity;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -90,22 +93,41 @@ public class ClusteredConnectorBacklogIT extends
NiFiSystemIT {
"Record Backlog", "0",
"Load Balance Strategy", loadBalanceStrategy));
- getClientUtil().startConnector(connector.getId());
- waitForConnectorMinQueueCount(connector.getId(), 40);
- getClientUtil().stopConnector(connector.getId());
-
- final int totalQueued =
getConnectorQueuedFlowFileCount(connector.getId());
- logger.info("Load Balance Strategy {}: cluster has {} queued FlowFiles
before reading backlog", loadBalanceStrategy, totalQueued);
- assertTrue(totalQueued > 0);
-
- final BacklogDTO backlog =
getClientUtil().getConnectorBacklog(connector.getId());
- assertNotNull(backlog);
+ try {
+ getClientUtil().startConnector(connector.getId());
+ waitForConnectorMinQueueCount(connector.getId(), 40);
+ getClientUtil().stopConnector(connector.getId());
+
+ final int totalQueued =
getConnectorQueuedFlowFileCount(connector.getId());
+ logger.info("Load Balance Strategy {}: cluster has {} queued
FlowFiles before reading backlog", loadBalanceStrategy, totalQueued);
+ assertTrue(totalQueued > 0);
+
+ final BacklogDTO backlog =
getClientUtil().getConnectorBacklog(connector.getId());
+ assertNotNull(backlog);
+
+ final long flowFiles = backlog.getFlowFileCount();
+ assertTrue(flowFiles >= 0);
+ assertTrue(flowFiles <= totalQueued,
+ "Cluster aggregate FlowFile count from queue snapshot (" +
flowFiles + ") must not exceed total queued count (" + totalQueued + ")");
+ assertEquals(Long.valueOf(flowFiles), backlog.getRecordCount());
+ assertTrue(backlog.getByteCount() >= 0L);
+ } finally {
+ // Load Balancing keeps redistributing already-queued FlowFiles
between nodes even after the Connector is
+ // stopped. A FlowFile can be in flight - removed from the sending
node's queue but not yet enqueued on the
+ // receiving node - a window in which the teardown's Connector
purge won't see it. Wait for Load Balancing to
+ // settle here, regardless of whether start/stop or the assertions
above passed, so teardown does not fail
+ // trying to delete a Connector that still has FlowFiles queued.
+ waitForLoadBalancingComplete(connector.getId());
+ }
+ }
- final long flowFiles = backlog.getFlowFileCount();
- assertTrue(flowFiles >= 0);
- assertTrue(flowFiles <= totalQueued,
- "Cluster aggregate FlowFile count from queue snapshot (" +
flowFiles + ") must not exceed total queued count (" + totalQueued + ")");
- assertEquals(Long.valueOf(flowFiles), backlog.getRecordCount());
- assertTrue(backlog.getByteCount() >= 0L);
+ private void waitForLoadBalancingComplete(final String connectorId) throws
InterruptedException {
+ waitFor(() -> {
+ final ProcessGroupFlowEntity flowEntity =
getNifiClient().getConnectorClient().getFlow(connectorId);
+ return
flowEntity.getProcessGroupFlow().getFlow().getConnections().stream()
+ .map(ConnectionEntity::getComponent)
+ .map(ConnectionDTO::getLoadBalanceStatus)
+ .noneMatch(ConnectionDTO.LOAD_BALANCE_ACTIVE::equals);
+ }, 100L, "Load Balancing to complete for Connector " + connectorId);
}
}