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 bf7085c3daf NIFI-16248 Added Response Merger for Connector Purge 
Requests (#11587)
bf7085c3daf is described below

commit bf7085c3dafd90b216fbcaeaa2eb833cb5a9fce3
Author: skeossei <[email protected]>
AuthorDate: Tue Aug 25 08:00:22 2026 -0700

    NIFI-16248 Added Response Merger for Connector Purge Requests (#11587)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../http/StandardHttpResponseMapper.java           |   2 +
 .../ConnectorPurgeRequestEndpointMerger.java       |  86 +++++++++++++
 .../http/StandardHttpResponseMapperTest.java       |  44 +++++++
 .../ConnectorPurgeRequestEndpointMergerTest.java   | 134 +++++++++++++++++++++
 .../endpoints/DropRequestEndpointMergerTest.java   | 103 ++++++++++++++++
 5 files changed, 369 insertions(+)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java
index 7c942385b31..8fdd9a498e0 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java
@@ -31,6 +31,7 @@ import 
org.apache.nifi.cluster.coordination.http.endpoints.ConnectorEndpointMerg
 import 
org.apache.nifi.cluster.coordination.http.endpoints.ConnectorFlowEndpointMerger;
 import 
org.apache.nifi.cluster.coordination.http.endpoints.ConnectorPropertyGroupEndpointMerger;
 import 
org.apache.nifi.cluster.coordination.http.endpoints.ConnectorPropertyGroupNamesEndpointMerger;
+import 
org.apache.nifi.cluster.coordination.http.endpoints.ConnectorPurgeRequestEndpointMerger;
 import 
org.apache.nifi.cluster.coordination.http.endpoints.ConnectorStatusEndpointMerger;
 import 
org.apache.nifi.cluster.coordination.http.endpoints.ConnectorsEndpointMerger;
 import 
org.apache.nifi.cluster.coordination.http.endpoints.ControllerBulletinsEndpointMerger;
@@ -182,6 +183,7 @@ public class StandardHttpResponseMapper implements 
HttpResponseMapper {
         endpointMergers.add(new RuleViolationEndpointMerger());
         endpointMergers.add(new DropRequestEndpointMerger());
         endpointMergers.add(new DropAllFlowFilesRequestEndpointMerger());
+        endpointMergers.add(new ConnectorPurgeRequestEndpointMerger());
         endpointMergers.add(new ListFlowFilesEndpointMerger());
         endpointMergers.add(new ComponentStateEndpointMerger());
         endpointMergers.add(new BulletinBoardEndpointMerger());
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ConnectorPurgeRequestEndpointMerger.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ConnectorPurgeRequestEndpointMerger.java
new file mode 100644
index 00000000000..1911c0622d5
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ConnectorPurgeRequestEndpointMerger.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.dto.DropRequestDTO;
+import org.apache.nifi.web.api.entity.DropRequestEntity;
+
+import java.net.URI;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+public class ConnectorPurgeRequestEndpointMerger extends 
AbstractSingleDTOEndpoint<DropRequestEntity, DropRequestDTO> {
+    private static final int TOTAL_PERCENT_COMPLETED = 100;
+    private static final Pattern CONNECTOR_PURGE_REQUESTS_URI = 
Pattern.compile("/nifi-api/connectors/[a-f0-9\\-]{36}/purge-requests");
+    private static final Pattern CONNECTOR_PURGE_REQUEST_URI = 
Pattern.compile("/nifi-api/connectors/[a-f0-9\\-]{36}/purge-requests/[a-f0-9\\-]{36}");
+
+    @Override
+    public boolean canHandle(final URI uri, final String method) {
+        final String path = uri.getPath();
+        if ("POST".equalsIgnoreCase(method)) {
+            return CONNECTOR_PURGE_REQUESTS_URI.matcher(path).matches();
+        }
+
+        return ("GET".equalsIgnoreCase(method) || 
"DELETE".equalsIgnoreCase(method))
+                && CONNECTOR_PURGE_REQUEST_URI.matcher(path).matches();
+    }
+
+    @Override
+    protected Class<DropRequestEntity> getEntityClass() {
+        return DropRequestEntity.class;
+    }
+
+    @Override
+    protected DropRequestDTO getDto(final DropRequestEntity entity) {
+        return entity.getDropRequest();
+    }
+
+    @Override
+    protected void mergeResponses(final DropRequestDTO clientDto, final 
Map<NodeIdentifier, DropRequestDTO> dtoMap,
+                                  final Set<NodeResponse> successfulResponses, 
final Set<NodeResponse> problematicResponses) {
+        final Set<String> failureReasons = new LinkedHashSet<>();
+        boolean allFinished = true;
+        int percentCompleted = TOTAL_PERCENT_COMPLETED;
+
+        for (final DropRequestDTO nodeDto : dtoMap.values()) {
+            if (nodeDto.getFailureReason() != null) {
+                failureReasons.add(nodeDto.getFailureReason());
+            }
+
+            allFinished &= nodeDto.isFinished();
+            percentCompleted = Math.min(percentCompleted, 
nodeDto.getPercentCompleted());
+        }
+
+        if (failureReasons.isEmpty()) {
+            percentCompleted = allFinished ? TOTAL_PERCENT_COMPLETED : 
percentCompleted;
+            clientDto.setState(allFinished ? "Complete" : "In Progress");
+        } else {
+            final String failureReason = String.join("; ", failureReasons);
+            allFinished = true;
+            percentCompleted = TOTAL_PERCENT_COMPLETED;
+            clientDto.setFailureReason(failureReason);
+            clientDto.setState("Failed: " + failureReason);
+        }
+
+        clientDto.setFinished(allFinished);
+        clientDto.setPercentCompleted(percentCompleted);
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapperTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapperTest.java
new file mode 100644
index 00000000000..51552c6b0b4
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapperTest.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.cluster.coordination.http;
+
+import org.apache.nifi.util.NiFiProperties;
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class StandardHttpResponseMapperTest {
+    private static final String CONNECTOR_ID = 
"abcdef00-abcd-abcd-abcd-abcdef000000";
+    private static final String REQUEST_ID = 
"00000000-0000-0000-0000-000000000001";
+
+    private final StandardHttpResponseMapper mapper = new 
StandardHttpResponseMapper(NiFiProperties.createBasicNiFiProperties((String) 
null));
+
+    @Test
+    void testConnectorPurgeRequestsInterpreted() {
+        
assertTrue(mapper.isResponseInterpreted(URI.create("/nifi-api/connectors/" + 
CONNECTOR_ID + "/purge-requests"), "POST"));
+        
assertTrue(mapper.isResponseInterpreted(URI.create("/nifi-api/connectors/" + 
CONNECTOR_ID + "/purge-requests/" + REQUEST_ID), "GET"));
+        
assertTrue(mapper.isResponseInterpreted(URI.create("/nifi-api/connectors/" + 
CONNECTOR_ID + "/purge-requests/" + REQUEST_ID), "DELETE"));
+    }
+
+    @Test
+    void testUnsupportedConnectorPurgeRequestsNotInterpreted() {
+        
assertFalse(mapper.isResponseInterpreted(URI.create("/nifi-api/connectors/" + 
CONNECTOR_ID + "/purge-requests"), "GET"));
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ConnectorPurgeRequestEndpointMergerTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ConnectorPurgeRequestEndpointMergerTest.java
new file mode 100644
index 00000000000..a74399c0027
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ConnectorPurgeRequestEndpointMergerTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.dto.DropRequestDTO;
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ConnectorPurgeRequestEndpointMergerTest {
+    private static final String HTTP_DELETE = "DELETE";
+    private static final String HTTP_GET = "GET";
+    private static final String HTTP_POST = "POST";
+    private static final String HTTP_PUT = "PUT";
+    private static final String CONNECTOR_ID = 
"abcdef00-abcd-abcd-abcd-abcdef000000";
+    private static final String REQUEST_ID = 
"00000000-0000-0000-0000-000000000001";
+    private static final String PURGE_REQUESTS_URI = "/nifi-api/connectors/" + 
CONNECTOR_ID + "/purge-requests";
+    private static final String PURGE_REQUEST_URI = PURGE_REQUESTS_URI + "/" + 
REQUEST_ID;
+    private static final String FAILURE_REASON = "Failed to purge queue";
+    private static final int NO_PERCENT_COMPLETED = 0;
+    private static final int PARTIAL_PERCENT_COMPLETED = 40;
+    private static final int TOTAL_PERCENT_COMPLETED = 100;
+
+    private final ConnectorPurgeRequestEndpointMerger merger = new 
ConnectorPurgeRequestEndpointMerger();
+
+    @Test
+    void testCanHandleConnectorPurgeRequests() {
+        assertTrue(merger.canHandle(URI.create(PURGE_REQUESTS_URI), 
HTTP_POST));
+        assertTrue(merger.canHandle(URI.create(PURGE_REQUEST_URI), HTTP_GET));
+        assertTrue(merger.canHandle(URI.create(PURGE_REQUEST_URI), 
HTTP_DELETE));
+    }
+
+    @Test
+    void testCanHandleRejectsUnrelatedRequests() {
+        assertFalse(merger.canHandle(URI.create(PURGE_REQUESTS_URI), 
HTTP_GET));
+        assertFalse(merger.canHandle(URI.create(PURGE_REQUESTS_URI), 
HTTP_DELETE));
+        assertFalse(merger.canHandle(URI.create(PURGE_REQUEST_URI), 
HTTP_POST));
+        assertFalse(merger.canHandle(URI.create(PURGE_REQUEST_URI), HTTP_PUT));
+        
assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/not-a-uuid/purge-requests"),
 HTTP_POST));
+        assertFalse(merger.canHandle(URI.create(PURGE_REQUESTS_URI + 
"/not-a-uuid"), HTTP_GET));
+        assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/" + 
CONNECTOR_ID + "/backlog-requests"), HTTP_POST));
+        assertFalse(merger.canHandle(URI.create("/nifi-api/processors/" + 
CONNECTOR_ID + "/purge-requests"), HTTP_POST));
+    }
+
+    @Test
+    void testMergeWaitsForSlowestNode() {
+        final DropRequestDTO clientRequest = request(true, 
TOTAL_PERCENT_COMPLETED, null);
+        final Map<NodeIdentifier, DropRequestDTO> requests = new 
LinkedHashMap<>();
+        requests.put(nodeIdentifier(1), clientRequest);
+        requests.put(nodeIdentifier(2), request(false, 
PARTIAL_PERCENT_COMPLETED, null));
+
+        merger.mergeResponses(clientRequest, requests, null, null);
+
+        assertFalse(clientRequest.isFinished());
+        assertEquals(PARTIAL_PERCENT_COMPLETED, 
clientRequest.getPercentCompleted());
+        assertEquals("In Progress", clientRequest.getState());
+    }
+
+    @Test
+    void testMergeCompletesWhenAllNodesFinish() {
+        final DropRequestDTO clientRequest = request(true, 
TOTAL_PERCENT_COMPLETED, null);
+        final Map<NodeIdentifier, DropRequestDTO> requests = new 
LinkedHashMap<>();
+        requests.put(nodeIdentifier(1), clientRequest);
+        requests.put(nodeIdentifier(2), request(true, TOTAL_PERCENT_COMPLETED, 
null));
+
+        merger.mergeResponses(clientRequest, requests, null, null);
+
+        assertTrue(clientRequest.isFinished());
+        assertEquals(TOTAL_PERCENT_COMPLETED, 
clientRequest.getPercentCompleted());
+        assertEquals("Complete", clientRequest.getState());
+    }
+
+    @Test
+    void testMergePropagatesFailure() {
+        final DropRequestDTO clientRequest = request(false, 
PARTIAL_PERCENT_COMPLETED, null);
+        final Map<NodeIdentifier, DropRequestDTO> requests = new 
LinkedHashMap<>();
+        requests.put(nodeIdentifier(1), clientRequest);
+        requests.put(nodeIdentifier(2), request(true, NO_PERCENT_COMPLETED, 
FAILURE_REASON));
+
+        merger.mergeResponses(clientRequest, requests, null, null);
+
+        assertTrue(clientRequest.isFinished());
+        assertEquals(TOTAL_PERCENT_COMPLETED, 
clientRequest.getPercentCompleted());
+        assertEquals(FAILURE_REASON, clientRequest.getFailureReason());
+        assertEquals("Failed: " + FAILURE_REASON, clientRequest.getState());
+    }
+
+    private static DropRequestDTO request(
+            final boolean finished,
+            final int percentCompleted,
+            final String failureReason
+    ) {
+        final DropRequestDTO request = new DropRequestDTO();
+        request.setFinished(finished);
+        request.setPercentCompleted(percentCompleted);
+        request.setFailureReason(failureReason);
+        return request;
+    }
+
+    private static NodeIdentifier nodeIdentifier(final int index) {
+        return new NodeIdentifier(
+                "node-" + index,
+                "localhost",
+                8000 + index,
+                "localhost",
+                8100 + index,
+                "localhost",
+                8200 + index,
+                8300 + index,
+                false
+        );
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/DropRequestEndpointMergerTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/DropRequestEndpointMergerTest.java
new file mode 100644
index 00000000000..53825740fca
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/DropRequestEndpointMergerTest.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.controller.queue.DropFlowFileState;
+import org.apache.nifi.web.api.dto.DropRequestDTO;
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DropRequestEndpointMergerTest {
+    private static final String CONNECTION_ID = 
"12345678-1234-1234-1234-123456789012";
+    private static final String REQUEST_ID = 
"00000000-0000-0000-0000-000000000001";
+
+    private final DropRequestEndpointMerger merger = new 
DropRequestEndpointMerger();
+
+    @Test
+    void testCanHandleFlowFileQueueDropRequests() {
+        assertTrue(merger.canHandle(URI.create("/nifi-api/flowfile-queues/" + 
CONNECTION_ID + "/drop-requests"), "POST"));
+        assertTrue(merger.canHandle(URI.create("/nifi-api/flowfile-queues/" + 
CONNECTION_ID + "/drop-requests/" + REQUEST_ID), "GET"));
+        assertTrue(merger.canHandle(URI.create("/nifi-api/flowfile-queues/" + 
CONNECTION_ID + "/drop-requests/" + REQUEST_ID), "DELETE"));
+    }
+
+    @Test
+    void testCanHandleRejectsUnsupportedRequests() {
+        
assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/abcdef00-abcd-abcd-abcd-abcdef000000/purge-requests"),
 "POST"));
+        
assertFalse(merger.canHandle(URI.create("/nifi-api/flowfile-queues/not-a-uuid/drop-requests"),
 "POST"));
+    }
+
+    @Test
+    void testMergeWaitsForAllNodesAndAggregatesCounts() {
+        final DropRequestDTO clientRequest = dropRequest(true, 0, 4, 40L, 6, 
60L);
+        final Map<NodeIdentifier, DropRequestDTO> requests = new 
LinkedHashMap<>();
+        requests.put(nodeIdentifier(1), clientRequest);
+        requests.put(nodeIdentifier(2), dropRequest(false, 3, 6, 60L, 7, 70L));
+
+        merger.mergeResponses(clientRequest, requests, null, null);
+
+        assertFalse(clientRequest.isFinished());
+        assertEquals(3, clientRequest.getCurrentCount());
+        assertEquals(30L, clientRequest.getCurrentSize());
+        assertEquals(10, clientRequest.getDroppedCount());
+        assertEquals(100L, clientRequest.getDroppedSize());
+        assertEquals(13, clientRequest.getOriginalCount());
+        assertEquals(130L, clientRequest.getOriginalSize());
+        assertEquals(76, clientRequest.getPercentCompleted());
+    }
+
+    private static DropRequestDTO dropRequest(
+            final boolean finished,
+            final int currentCount,
+            final int droppedCount,
+            final long droppedSize,
+            final int originalCount,
+            final long originalSize
+    ) {
+        final DropRequestDTO request = new DropRequestDTO();
+        request.setFinished(finished);
+        request.setCurrentCount(currentCount);
+        request.setCurrentSize(currentCount * 10L);
+        request.setDroppedCount(droppedCount);
+        request.setDroppedSize(droppedSize);
+        request.setOriginalCount(originalCount);
+        request.setOriginalSize(originalSize);
+        request.setState(finished ? DropFlowFileState.COMPLETE.toString() : 
DropFlowFileState.DROPPING_FLOWFILES.toString());
+        return request;
+    }
+
+    private static NodeIdentifier nodeIdentifier(final int index) {
+        return new NodeIdentifier(
+                "node-" + index,
+                "localhost",
+                8000 + index,
+                "localhost",
+                8100 + index,
+                "localhost",
+                8200 + index,
+                8300 + index,
+                false
+        );
+    }
+}

Reply via email to