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 39ca75d868 NIFI-13914 Fixed FlowFile Prioritization for Stateless 
Execution (#9432)
39ca75d868 is described below

commit 39ca75d868ffe0e7df465798a77c33e74d154b7c
Author: Mark Payne <[email protected]>
AuthorDate: Mon Oct 21 16:39:12 2024 -0400

    NIFI-13914 Fixed FlowFile Prioritization for Stateless Execution (#9432)
    
    Ensure that when data is enqueued in the StatelessFlowFileQueue we order 
the data properly before adding to its internal queue. Added system tests to 
verify that the data is properly ordered while running within stateless and 
properly ordered when coming out, if using a FIFO prioritizer.
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../stateless/queue/StatelessFlowFileQueue.java    |  16 +++-
 .../processors/tests/system/ReOrderFlowFiles.java  |  88 ++++++++++++++++++
 .../processors/tests/system/VerifyEvenThenOdd.java | 100 +++++++++++++++++++++
 .../services/org.apache.nifi.processor.Processor   |   2 +
 .../apache/nifi/tests/system/NiFiClientUtil.java   |  27 ++++++
 .../tests/system/stateless/StatelessBasicsIT.java  |  56 ++++++++++++
 6 files changed, 286 insertions(+), 3 deletions(-)

diff --git 
a/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/queue/StatelessFlowFileQueue.java
 
b/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/queue/StatelessFlowFileQueue.java
index 03ee282ff2..caaa607b10 100644
--- 
a/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/queue/StatelessFlowFileQueue.java
+++ 
b/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/queue/StatelessFlowFileQueue.java
@@ -18,7 +18,6 @@
 package org.apache.nifi.stateless.queue;
 
 import org.apache.nifi.controller.queue.DropFlowFileStatus;
-import org.apache.nifi.controller.status.FlowFileAvailability;
 import org.apache.nifi.controller.queue.ListFlowFileStatus;
 import org.apache.nifi.controller.queue.LoadBalanceCompression;
 import org.apache.nifi.controller.queue.LoadBalanceStrategy;
@@ -27,6 +26,8 @@ import org.apache.nifi.controller.queue.QueueDiagnostics;
 import org.apache.nifi.controller.queue.QueueSize;
 import org.apache.nifi.controller.repository.FlowFileRecord;
 import org.apache.nifi.controller.repository.SwapSummary;
+import org.apache.nifi.controller.status.FlowFileAvailability;
+import org.apache.nifi.flowfile.FlowFile;
 import org.apache.nifi.flowfile.FlowFilePrioritizer;
 import org.apache.nifi.processor.FlowFileFilter;
 import org.apache.nifi.util.FormatUtils;
@@ -34,8 +35,10 @@ import org.apache.nifi.util.FormatUtils;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
@@ -165,8 +168,15 @@ public class StatelessFlowFileQueue implements 
DrainableFlowFileQueue {
 
     @Override
     public void putAll(final Collection<FlowFileRecord> flowFiles) {
-        this.flowFiles.addAll(flowFiles);
-        flowFiles.forEach(ff -> totalBytes.addAndGet(ff.getSize()));
+        // Order the FlowFiles in the same order they were transferred by the 
Processor. This ensures that we keep the ordering provided by the Processor.
+        // This is not important for the Standard NiFi engine because it uses 
the StandardFlowFileQueue, which maintains the order of FlowFiles as configured.
+        // However, in stateless, we want to keep the order that the upstream 
processor processes the data.
+        final List<FlowFileRecord> orderedFlowFiles = new 
ArrayList<>(flowFiles);
+        orderedFlowFiles.sort(Comparator.comparingLong((FlowFileRecord 
flowFile) -> Optional.ofNullable(flowFile.getLastQueueDate()).orElse(0L))
+            .thenComparingLong(FlowFile::getQueueDateIndex));
+
+        this.flowFiles.addAll(orderedFlowFiles);
+        orderedFlowFiles.forEach(ff -> totalBytes.addAndGet(ff.getSize()));
     }
 
     @Override
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/ReOrderFlowFiles.java
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/ReOrderFlowFiles.java
new file mode 100644
index 0000000000..3f671149fe
--- /dev/null
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/ReOrderFlowFiles.java
@@ -0,0 +1,88 @@
+/*
+ * 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.processors.tests.system;
+
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.expression.AttributeExpression;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.FlowFileFilter;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.util.List;
+import java.util.Set;
+
+@CapabilityDescription("Selects FlowFiles that match the given criteria and 
transfers them to the 'success' relationship. Then, selects all other FlowFiles 
and transfers them " +
+                       "to the success relationship. Note that this Processor 
will not work properly if it is scheduled to run while its incoming queue(s) 
are being populated. " +
+                       "This is meant to be used only for purposes of testing 
in a Stateless execution engine and makes use of FlowFileFilters.")
+public class ReOrderFlowFiles extends AbstractProcessor {
+    protected static PropertyDescriptor FIRST_SELECTION_CRITERIA = new 
PropertyDescriptor.Builder()
+        .name("First Group Selection Criteria")
+        .description("An Expression Language expression that evaluates to true 
or false. FlowFiles that evaluate to true will be transferred first; others " +
+                     "will be transferred after.")
+        .required(true)
+        
.addValidator(StandardValidators.createAttributeExpressionLanguageValidator(AttributeExpression.ResultType.BOOLEAN,
 false))
+        .build();
+
+    private static final Relationship REL_SUCCESS = new Relationship.Builder()
+        .name("success")
+        .description("All FlowFiles are transferred to this relationship.")
+        .build();
+
+    private static final Set<Relationship> relationships = Set.of(REL_SUCCESS);
+    private static final List<PropertyDescriptor> properties = 
List.of(FIRST_SELECTION_CRITERIA);
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return relationships;
+    }
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return properties;
+    }
+
+    @Override
+    public void onTrigger(final ProcessContext context, final ProcessSession 
session) throws ProcessException {
+        final PropertyValue selectionValue = 
context.getProperty(FIRST_SELECTION_CRITERIA);
+
+        final List<FlowFile> matching = session.get(new FlowFileFilter() {
+            @Override
+            public FlowFileFilterResult filter(final FlowFile flowFile) {
+                final boolean selected = 
selectionValue.evaluateAttributeExpressions(flowFile).asBoolean();
+                return selected ? FlowFileFilterResult.ACCEPT_AND_CONTINUE : 
FlowFileFilterResult.REJECT_AND_CONTINUE;
+            }
+        });
+
+        final List<FlowFile> unmatched = session.get(new FlowFileFilter() {
+            @Override
+            public FlowFileFilterResult filter(final FlowFile flowFile) {
+                return FlowFileFilterResult.ACCEPT_AND_CONTINUE;
+            }
+        });
+
+        session.transfer(matching, REL_SUCCESS);
+        session.transfer(unmatched, REL_SUCCESS);
+    }
+}
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyEvenThenOdd.java
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyEvenThenOdd.java
new file mode 100644
index 0000000000..5adf559cda
--- /dev/null
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyEvenThenOdd.java
@@ -0,0 +1,100 @@
+/*
+ * 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.processors.tests.system;
+
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+@CapabilityDescription("Ensures that all FlowFiles that are in the Processor's 
incoming queue are ordered such that the value of the key attribute " +
+                       "is even for all FlowFiles before the first FlowFile 
with an odd value. If the FlowFiles are ordered correctly, they are transferred 
to " +
+                       "the 'success' relationship; otherwise, they are 
transferred to the 'failure' relationship. The name of the key attribute is 
configurable. " +
+                       "This is used to ensure that data is properly ordered 
while running within a Stateless flow.")
+public class VerifyEvenThenOdd extends AbstractProcessor {
+
+    protected static final PropertyDescriptor ATTRIBUTE_NAME = new 
PropertyDescriptor.Builder()
+        .name("Attribute Name")
+        .description("The name of the attribute to check for even or odd 
values")
+        .required(true)
+        .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+        .build();
+
+    private static final List<PropertyDescriptor> properties = 
List.of(ATTRIBUTE_NAME);
+
+    protected static final Relationship REL_SUCCESS = new 
Relationship.Builder()
+        .name("success")
+        .description("All FlowFiles are transferred to this relationship in 
the event that all queued FlowFiles are ordered correctly.")
+        .build();
+
+    protected static final Relationship REL_FAILURE = new 
Relationship.Builder()
+        .name("failure")
+        .description("All FlowFiles are transferred to this relationship in 
the event that any queued FlowFiles are not ordered correctly.")
+        .build();
+
+    private static final Set<Relationship> relationships = Set.of(REL_SUCCESS, 
REL_FAILURE);
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return properties;
+    }
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return relationships;
+    }
+
+    @Override
+    public void onTrigger(final ProcessContext context, final ProcessSession 
session) throws ProcessException {
+        final List<FlowFile> allFlowFiles = new ArrayList<>();
+        List<FlowFile> batch;
+        while (!(batch = session.get(1000)).isEmpty()) {
+            allFlowFiles.addAll(batch);
+        }
+
+        boolean oddSeen = false;
+        boolean ordered = true;
+        for (final FlowFile flowFile : allFlowFiles) {
+            final String value = 
flowFile.getAttribute(context.getProperty(ATTRIBUTE_NAME).getValue());
+            final int intValue = Integer.parseInt(value);
+
+            final boolean even = intValue % 2 == 0;
+            if (even && oddSeen) {
+                ordered = false;
+                break;
+            } else if (!even) {
+                oddSeen = true;
+            }
+        }
+
+        if (ordered) {
+            session.transfer(allFlowFiles, REL_SUCCESS);
+        } else {
+            session.transfer(allFlowFiles, REL_FAILURE);
+        }
+    }
+}
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
index b86667f6a4..0b344481fc 100644
--- 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -35,6 +35,7 @@ org.apache.nifi.processors.tests.system.PartitionText
 org.apache.nifi.processors.tests.system.PassThrough
 org.apache.nifi.processors.tests.system.PassThroughRequiresInstanceClassLoading
 org.apache.nifi.processors.tests.system.CountPrimaryNodeChangeEvents
+org.apache.nifi.processors.tests.system.ReOrderFlowFiles
 org.apache.nifi.processors.tests.system.ReplaceWithFile
 org.apache.nifi.processors.tests.system.ReverseContents
 org.apache.nifi.processors.tests.system.RoundRobinFlowFiles
@@ -50,6 +51,7 @@ org.apache.nifi.processors.tests.system.UpdateContent
 org.apache.nifi.processors.tests.system.UnzipFlowFile
 org.apache.nifi.processors.tests.system.ValidateFileExists
 org.apache.nifi.processors.tests.system.VerifyContents
+org.apache.nifi.processors.tests.system.VerifyEvenThenOdd
 org.apache.nifi.processors.tests.system.WriteFlowFileCountToFile
 org.apache.nifi.processors.tests.system.WriteLifecycleEvents
 org.apache.nifi.processors.tests.system.WriteToFile
diff --git 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/NiFiClientUtil.java
 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/NiFiClientUtil.java
index 44b4fc5013..dbdcddd053 100644
--- 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/NiFiClientUtil.java
+++ 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/NiFiClientUtil.java
@@ -1551,6 +1551,33 @@ public class NiFiClientUtil {
         return getConnectionClient().updateConnection(updatedEntity);
     }
 
+    public ConnectionEntity updateConnectionBackpressure(final 
ConnectionEntity connectionEntity, final long flowFileCount, final long bytes) 
throws NiFiClientException, IOException {
+        final ConnectionDTO connectionDto = new ConnectionDTO();
+        connectionDto.setBackPressureDataSizeThreshold(bytes + " B");
+        connectionDto.setBackPressureObjectThreshold(flowFileCount);
+        connectionDto.setId(connectionEntity.getId());
+
+        final ConnectionEntity updatedEntity = new ConnectionEntity();
+        updatedEntity.setComponent(connectionDto);
+        updatedEntity.setId(connectionEntity.getId());
+        updatedEntity.setRevision(connectionEntity.getRevision());
+
+        return getConnectionClient().updateConnection(updatedEntity);
+    }
+
+    public ConnectionEntity updateConnectionPrioritizer(final ConnectionEntity 
connectionEntity, final String prioritizerName) throws NiFiClientException, 
IOException {
+        final ConnectionDTO connectionDto = new ConnectionDTO();
+        connectionDto.setPrioritizers(List.of("org.apache.nifi.prioritizer." + 
prioritizerName));
+        connectionDto.setId(connectionEntity.getId());
+
+        final ConnectionEntity updatedEntity = new ConnectionEntity();
+        updatedEntity.setComponent(connectionDto);
+        updatedEntity.setId(connectionEntity.getId());
+        updatedEntity.setRevision(connectionEntity.getRevision());
+
+        return getConnectionClient().updateConnection(updatedEntity);
+    }
+
     public DropRequestEntity emptyQueue(final String connectionId) throws 
NiFiClientException, IOException {
         final ConnectionClient connectionClient = getConnectionClient();
 
diff --git 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/stateless/StatelessBasicsIT.java
 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/stateless/StatelessBasicsIT.java
index 6357f80561..c48874d82d 100644
--- 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/stateless/StatelessBasicsIT.java
+++ 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/stateless/StatelessBasicsIT.java
@@ -46,6 +46,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -82,6 +83,61 @@ public class StatelessBasicsIT extends NiFiSystemIT {
         startClaimantCount = getClaimantCounts();
     }
 
+    @Test
+    public void testOrderingIntraSession() throws NiFiClientException, 
IOException, InterruptedException {
+        final int batchSize = 100;
+
+        statelessGroup = getClientUtil().createProcessGroup("Stateless", 
"root");
+        getClientUtil().markStateless(statelessGroup, "1 min");
+
+        final ProcessorEntity generate = 
getClientUtil().createProcessor(GENERATE_FLOWFILE, statelessGroup.getId());
+        final Map<String, String> generateProperties = new HashMap<>();
+        generateProperties.put("Text", HELLO_WORLD);
+        generateProperties.put("Batch Size", String.valueOf(batchSize));
+        generateProperties.put("Counter", "${nextInt()}");
+        getClientUtil().updateProcessorProperties(generate, 
generateProperties);
+
+        final ProcessorEntity router = 
getClientUtil().createProcessor("ReOrderFlowFiles", statelessGroup.getId());
+        getClientUtil().updateProcessorProperties(router, Map.of("First Group 
Selection Criteria", "${Counter:mod(2):equals(0)}"));
+
+        // Verify that FlowFiles are ordered correctly within stateless flow.
+        final ProcessorEntity verifyProcessor = 
getClientUtil().createProcessor("VerifyEvenThenOdd", statelessGroup.getId());
+        getClientUtil().updateProcessorProperties(verifyProcessor, 
Map.of("Attribute Name", "Counter"));
+
+        final PortEntity outputPort = getClientUtil().createOutputPort("Out", 
statelessGroup.getId());
+
+        getClientUtil().createConnection(generate, router, "success");
+        getClientUtil().createConnection(router, verifyProcessor, "success");
+        getClientUtil().createConnection(verifyProcessor, outputPort, 
"success");
+        getClientUtil().setAutoTerminatedRelationships(verifyProcessor, 
"failure");
+
+        final ProcessorEntity terminate = 
getClientUtil().createProcessor(TERMINATE_FLOWFILE);
+        final ConnectionEntity outputToTerminate = 
getClientUtil().createConnection(outputPort, terminate);
+        getClientUtil().updateConnectionPrioritizer(outputToTerminate, 
"FirstInFirstOutPrioritizer");
+
+        getClientUtil().waitForValidProcessor(generate.getId());
+        getClientUtil().waitForValidProcessor(router.getId());
+        getClientUtil().startProcessGroupComponents(statelessGroup.getId());
+
+        waitForQueueCount(outputToTerminate.getId(), batchSize);
+        getClientUtil().stopProcessGroupComponents(statelessGroup.getId());
+
+        final List<String> actualCounterValues = new ArrayList<>();
+        for (int i = 0; i < batchSize; i++) {
+            final FlowFileEntity flowFileEntity = 
getClientUtil().getQueueFlowFile(outputToTerminate.getId(), i);
+            
actualCounterValues.add(flowFileEntity.getFlowFile().getAttributes().get("Counter"));
+        }
+
+        int expectedCounter = 0;
+        for (int i = 0; i < batchSize; i++) {
+            assertEquals(String.valueOf(expectedCounter), 
actualCounterValues.get(i));
+            expectedCounter += 2;
+            if (expectedCounter >= batchSize) {
+                expectedCounter = 1;
+            }
+        }
+    }
+
     @Test
     public void testOneInOneOut() throws NiFiClientException, IOException, 
InterruptedException {
         createFlowShell();

Reply via email to