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 5ba2e5a66e9 NIFI-16379 Restore GenerateFlowFile non-unique generation
and batch write behavior (#11711)
5ba2e5a66e9 is described below
commit 5ba2e5a66e9decd20b3ddf3576edf0a81acac0e9
Author: Joe Witt <[email protected]>
AuthorDate: Wed Sep 23 01:27:14 2026 -0700
NIFI-16379 Restore GenerateFlowFile non-unique generation and batch write
behavior (#11711)
---
.../nifi/processors/standard/GenerateFlowFile.java | 136 ++++++++-------------
.../processors/standard/TestGenerateFlowFile.java | 85 ++++++-------
2 files changed, 91 insertions(+), 130 deletions(-)
diff --git
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/GenerateFlowFile.java
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/GenerateFlowFile.java
index f173a97df5b..50a444f01d2 100644
---
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/GenerateFlowFile.java
+++
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/GenerateFlowFile.java
@@ -20,6 +20,8 @@ import org.apache.nifi.annotation.behavior.DynamicProperty;
import org.apache.nifi.annotation.behavior.InputRequirement;
import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
import org.apache.nifi.annotation.behavior.SupportsBatching;
+import org.apache.nifi.annotation.behavior.SystemResource;
+import org.apache.nifi.annotation.behavior.SystemResourceConsideration;
import org.apache.nifi.annotation.behavior.WritesAttribute;
import org.apache.nifi.annotation.behavior.WritesAttributes;
import org.apache.nifi.annotation.configuration.DefaultSchedule;
@@ -42,8 +44,6 @@ import org.apache.nifi.processor.Relationship;
import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.scheduling.SchedulingStrategy;
-import java.io.IOException;
-import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
@@ -59,6 +59,9 @@ import java.util.concurrent.atomic.AtomicReference;
@InputRequirement(Requirement.INPUT_FORBIDDEN)
@CapabilityDescription("This processor creates FlowFiles with random data or
custom content. GenerateFlowFile is useful" +
" for load testing, configuration, and simulation." + " Also see
DuplicateFlowFile for additional load testing.")
+@SystemResourceConsideration(resource = SystemResource.MEMORY, description =
"When Unique FlowFiles is false and Custom Text is not configured, "
+ + "the generated content is retained in memory while the Processor is
scheduled. This intentional tradeoff avoids regenerating identical "
+ + "content for every FlowFile and provides higher throughput for load
testing.")
@DynamicProperty(name = "Generated FlowFile attribute name", value =
"Generated FlowFile attribute value",
expressionLanguageScope = ExpressionLanguageScope.ENVIRONMENT,
description = "Specifies an attribute on generated FlowFiles defined
by the Dynamic Property's key and value." +
@@ -69,9 +72,9 @@ import java.util.concurrent.atomic.AtomicReference;
@DefaultSchedule(strategy = SchedulingStrategy.TIMER_DRIVEN, period = "1 min")
public class GenerateFlowFile extends AbstractProcessor {
- private static final int BUFFER_SIZE = 8192;
-
- private final AtomicReference<GeneratedData> generatedData = new
AtomicReference<>();
+ // Retaining the full non-unique payload is intentional. GenerateFlowFile
is a load-testing processor, and caching avoids
+ // spending CPU to regenerate identical content for every FlowFile. The
memory requirement is documented above.
+ private final AtomicReference<byte[]> data = new AtomicReference<>();
public static final String DATA_FORMAT_BINARY = "Binary";
public static final String DATA_FORMAT_TEXT = "Text";
@@ -100,8 +103,8 @@ public class GenerateFlowFile extends AbstractProcessor {
.build();
public static final PropertyDescriptor UNIQUE_FLOWFILES = new
PropertyDescriptor.Builder()
.name("Unique FlowFiles")
- .description("If true, each FlowFile that is generated will be
unique. If false, all generated FlowFiles will have the same content. "
- + "When Unique FlowFiles is false, the first FlowFile in a
batch is written and additional FlowFiles clone that content.")
+ .description("If true, each FlowFile that is generated will be
unique. If false, a random value will be generated and all FlowFiles "
+ + "will get the same content but this offers much higher
throughput")
.required(true)
.allowableValues("true", "false")
.defaultValue("false")
@@ -173,10 +176,10 @@ public class GenerateFlowFile extends AbstractProcessor {
@OnScheduled
public void onScheduled(final ProcessContext context) {
- if (context.getProperty(UNIQUE_FLOWFILES).asBoolean() ||
context.getProperty(CUSTOM_TEXT).isSet()) {
- generatedData.set(null);
- } else {
- generatedData.set(getGeneratedData(context));
+ if (context.getProperty(UNIQUE_FLOWFILES).asBoolean()) {
+ this.data.set(null);
+ } else if (!context.getProperty(CUSTOM_TEXT).isSet()) {
+ this.data.set(generateData(context));
}
}
@@ -195,44 +198,45 @@ public class GenerateFlowFile extends AbstractProcessor {
return results;
}
- private GeneratedData getGeneratedData(final ProcessContext context) {
- final long byteCount =
context.getProperty(FILE_SIZE).evaluateAttributeExpressions().asDataSize(DataUnit.B).longValue();
- final boolean binary =
context.getProperty(DATA_FORMAT).getValue().equals(DATA_FORMAT_BINARY);
- return new GeneratedData(byteCount, binary, new Random().nextLong());
+ private byte[] generateData(final ProcessContext context) {
+ final int byteCount =
context.getProperty(FILE_SIZE).evaluateAttributeExpressions().asDataSize(DataUnit.B).intValue();
+
+ final Random random = new Random();
+ final byte[] array = new byte[byteCount];
+ if
(context.getProperty(DATA_FORMAT).getValue().equals(DATA_FORMAT_BINARY)) {
+ random.nextBytes(array);
+ } else {
+ for (int i = 0; i < array.length; i++) {
+ final int index = random.nextInt(TEXT_CHARS.length);
+ array[i] = (byte) TEXT_CHARS[index];
+ }
+ }
+
+ return array;
}
@Override
public void onTrigger(final ProcessContext context, final ProcessSession
session) {
+ final byte[] data;
final boolean uniqueData =
context.getProperty(UNIQUE_FLOWFILES).asBoolean();
- final int batchSize = context.getProperty(BATCH_SIZE).asInteger();
- final Map<String, String> generatedAttributes =
getGeneratedAttributes(context);
-
if (uniqueData) {
- for (int i = 0; i < batchSize; i++) {
- session.transfer(createGeneratedFlowFile(session,
getGeneratedData(context), generatedAttributes), SUCCESS);
- }
- return;
- }
-
- final FlowFile first;
- if (context.getProperty(CUSTOM_TEXT).isSet()) {
- first = createCustomTextFlowFile(context, session,
generatedAttributes);
+ data = new byte[0];
} else {
- first = createGeneratedFlowFile(session, generatedData.get(),
generatedAttributes);
- }
-
- for (int i = 1; i < batchSize; i++) {
- session.transfer(session.clone(first), SUCCESS);
+ if (context.getProperty(CUSTOM_TEXT).isSet()) {
+ final Charset charset =
Charset.forName(context.getProperty(CHARSET).getValue());
+ data =
context.getProperty(CUSTOM_TEXT).evaluateAttributeExpressions().getValue().getBytes(charset);
+ } else {
+ data = this.data.get();
+ }
}
- session.transfer(first, SUCCESS);
- }
- private Map<String, String> getGeneratedAttributes(final ProcessContext
context) {
- final Map<String, String> generatedAttributes = new HashMap<>();
- for (final Map.Entry<PropertyDescriptor, String> entry :
context.getProperties().entrySet()) {
- final PropertyDescriptor property = entry.getKey();
+ Map<PropertyDescriptor, String> processorProperties =
context.getProperties();
+ Map<String, String> generatedAttributes = new HashMap<>();
+ for (final Map.Entry<PropertyDescriptor, String> entry :
processorProperties.entrySet()) {
+ PropertyDescriptor property = entry.getKey();
if (property.isDynamic() &&
property.isExpressionLanguageSupported()) {
- generatedAttributes.put(property.getName(),
context.getProperty(property).evaluateAttributeExpressions().getValue());
+ String dynamicValue =
context.getProperty(property).evaluateAttributeExpressions().getValue();
+ generatedAttributes.put(property.getName(), dynamicValue);
}
}
@@ -240,53 +244,17 @@ public class GenerateFlowFile extends AbstractProcessor {
generatedAttributes.put(CoreAttributes.MIME_TYPE.key(),
context.getProperty(MIME_TYPE).getValue());
}
- return generatedAttributes;
- }
-
- private FlowFile createCustomTextFlowFile(final ProcessContext context,
final ProcessSession session, final Map<String, String> generatedAttributes) {
- final Charset charset =
Charset.forName(context.getProperty(CHARSET).getValue());
- final byte[] customData =
context.getProperty(CUSTOM_TEXT).evaluateAttributeExpressions().getValue().getBytes(charset);
- FlowFile flowFile = session.create();
- if (customData.length > 0) {
- flowFile = session.write(flowFile, out -> out.write(customData));
- }
- return finishCreatedFlowFile(session, flowFile, generatedAttributes);
- }
-
- private FlowFile createGeneratedFlowFile(final ProcessSession session,
final GeneratedData data, final Map<String, String> generatedAttributes) {
- FlowFile flowFile = session.create();
- if (data.byteCount() > 0) {
- flowFile = session.write(flowFile, out -> writeGeneratedData(out,
data));
- }
- return finishCreatedFlowFile(session, flowFile, generatedAttributes);
- }
-
- private FlowFile finishCreatedFlowFile(final ProcessSession session,
FlowFile flowFile, final Map<String, String> generatedAttributes) {
- flowFile = session.putAllAttributes(flowFile, generatedAttributes);
- session.getProvenanceReporter().create(flowFile);
- return flowFile;
- }
-
- private static void writeGeneratedData(final OutputStream outputStream,
final GeneratedData generatedData) throws IOException {
- final Random random = new Random(generatedData.seed());
- final byte[] buffer = new byte[(int) Math.min(BUFFER_SIZE,
generatedData.byteCount())];
- long remaining = generatedData.byteCount();
-
- while (remaining > 0) {
- final int length = (int) Math.min(buffer.length, remaining);
- if (generatedData.binary()) {
- random.nextBytes(buffer);
- } else {
- for (int i = 0; i < length; i++) {
- buffer[i] = (byte)
TEXT_CHARS[random.nextInt(TEXT_CHARS.length)];
- }
+ for (int i = 0; i < context.getProperty(BATCH_SIZE).asInteger(); i++) {
+ FlowFile flowFile = session.create();
+ final byte[] writtenData = uniqueData ? generateData(context) :
data;
+ if (writtenData.length > 0) {
+ flowFile = session.write(flowFile, out ->
out.write(writtenData));
}
- outputStream.write(buffer, 0, length);
- remaining -= length;
- }
- }
+ flowFile = session.putAllAttributes(flowFile, generatedAttributes);
- private record GeneratedData(long byteCount, boolean binary, long seed) {
+ session.getProvenanceReporter().create(flowFile);
+ session.transfer(flowFile, SUCCESS);
+ }
}
@Override
diff --git
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestGenerateFlowFile.java
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestGenerateFlowFile.java
index 2941b8e6140..1a11c100094 100644
---
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestGenerateFlowFile.java
+++
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestGenerateFlowFile.java
@@ -16,6 +16,7 @@
*/
package org.apache.nifi.processors.standard;
+import org.apache.nifi.provenance.ProvenanceEventType;
import org.apache.nifi.util.MockFlowFile;
import org.apache.nifi.util.PropertyMigrationResult;
import org.apache.nifi.util.TestRunner;
@@ -36,9 +37,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* Unit tests for the GenerateFlowFile processor.
*/
public class TestGenerateFlowFile {
- private static final String STREAMED_FILE_SIZE = "9 KB";
- private static final long STREAMED_FILE_SIZE_BYTES = 9 * 1024;
-
private TestRunner runner;
@BeforeEach
@@ -48,23 +46,21 @@ public class TestGenerateFlowFile {
@Test
public void testGenerateCustomText() {
- runner.setProperty(GenerateFlowFile.FILE_SIZE, "1B");
+ runner.setProperty(GenerateFlowFile.FILE_SIZE, "1MB");
runner.setProperty(GenerateFlowFile.DATA_FORMAT,
GenerateFlowFile.DATA_FORMAT_TEXT);
runner.setProperty(GenerateFlowFile.CUSTOM_TEXT, "This is my custom
text!");
- runner.setProperty(GenerateFlowFile.BATCH_SIZE, "2");
-
runner.run();
- runner.assertTransferCount(GenerateFlowFile.SUCCESS, 2);
-
runner.getFlowFilesForRelationship(GenerateFlowFile.SUCCESS).get(0).assertContentEquals("This
is my custom text!");
-
runner.getFlowFilesForRelationship(GenerateFlowFile.SUCCESS).get(1).assertContentEquals("This
is my custom text!");
-
runner.getFlowFilesForRelationship(GenerateFlowFile.SUCCESS).get(0).assertAttributeNotExists("mime.type");
+ runner.assertTransferCount(GenerateFlowFile.SUCCESS, 1);
+ MockFlowFile generatedFlowFile =
runner.getFlowFilesForRelationship(GenerateFlowFile.SUCCESS).getFirst();
+ generatedFlowFile.assertContentEquals("This is my custom text!");
+ generatedFlowFile.assertAttributeNotExists("mime.type");
}
@Test
public void testInvalidCustomText() {
- runner.setProperty(GenerateFlowFile.FILE_SIZE, "1B");
+ runner.setProperty(GenerateFlowFile.FILE_SIZE, "1MB");
runner.setProperty(GenerateFlowFile.DATA_FORMAT,
GenerateFlowFile.DATA_FORMAT_BINARY);
runner.setProperty(GenerateFlowFile.CUSTOM_TEXT, "This is my custom
text!");
runner.assertNotValid();
@@ -76,30 +72,51 @@ public class TestGenerateFlowFile {
@Test
public void testFileSizeLargerThanIntegerMaxIsInvalid() {
- // Configuration only — do not runner.run() with this size. GitHub
runners cannot host a 3 GB FlowFile.
runner.setProperty(GenerateFlowFile.FILE_SIZE, "3 GB");
runner.assertNotValid();
}
@Test
- public void testGenerateNonUniqueBinaryContentStreamed() {
- assertStreamedContent(false, GenerateFlowFile.DATA_FORMAT_BINARY);
- }
+ public void testNonUniqueContentReusedAcrossInvocations() {
+ runner.setProperty(GenerateFlowFile.FILE_SIZE, "9 KB");
+ runner.setProperty(GenerateFlowFile.DATA_FORMAT,
GenerateFlowFile.DATA_FORMAT_BINARY);
+ runner.setProperty(GenerateFlowFile.UNIQUE_FLOWFILES, "false");
- @Test
- public void testGenerateNonUniqueTextContentStreamed() {
- assertStreamedContent(false, GenerateFlowFile.DATA_FORMAT_TEXT);
+ runner.run(2);
+
+ runner.assertAllFlowFilesTransferred(GenerateFlowFile.SUCCESS, 2);
+ final List<MockFlowFile> flowFiles =
runner.getFlowFilesForRelationship(GenerateFlowFile.SUCCESS);
+ assertArrayEquals(flowFiles.get(0).getData(),
flowFiles.get(1).getData());
}
@Test
- public void testGenerateUniqueBinaryContentStreamed() {
- assertStreamedContent(true, GenerateFlowFile.DATA_FORMAT_BINARY);
+ public void testUniqueContentGeneratedForEachFlowFile() {
+ runner.setProperty(GenerateFlowFile.FILE_SIZE, "9 KB");
+ runner.setProperty(GenerateFlowFile.DATA_FORMAT,
GenerateFlowFile.DATA_FORMAT_BINARY);
+ runner.setProperty(GenerateFlowFile.UNIQUE_FLOWFILES, "true");
+ runner.setProperty(GenerateFlowFile.BATCH_SIZE, "2");
+
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(GenerateFlowFile.SUCCESS, 2);
+ final List<MockFlowFile> flowFiles =
runner.getFlowFilesForRelationship(GenerateFlowFile.SUCCESS);
+ assertFalse(Arrays.equals(flowFiles.get(0).getData(),
flowFiles.get(1).getData()));
}
@Test
- public void testGenerateUniqueTextContentStreamed() {
- assertStreamedContent(true, GenerateFlowFile.DATA_FORMAT_TEXT);
+ public void testBatchCreatesIndependentFlowFiles() {
+ runner.setProperty(GenerateFlowFile.FILE_SIZE, "9 KB");
+ runner.setProperty(GenerateFlowFile.DATA_FORMAT,
GenerateFlowFile.DATA_FORMAT_BINARY);
+ runner.setProperty(GenerateFlowFile.UNIQUE_FLOWFILES, "false");
+ runner.setProperty(GenerateFlowFile.BATCH_SIZE, "3");
+
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(GenerateFlowFile.SUCCESS, 3);
+ assertEquals(3, runner.getProvenanceEvents().size());
+ assertTrue(runner.getProvenanceEvents().stream()
+ .allMatch(event -> event.getEventType() ==
ProvenanceEventType.CREATE));
}
@Test
@@ -173,28 +190,4 @@ public class TestGenerateFlowFile {
assertEquals(expectedRenamed,
propertyMigrationResult.getPropertiesRenamed());
}
- private void assertStreamedContent(final boolean unique, final String
dataFormat) {
- runner.setProperty(GenerateFlowFile.FILE_SIZE, STREAMED_FILE_SIZE);
- runner.setProperty(GenerateFlowFile.DATA_FORMAT, dataFormat);
- runner.setProperty(GenerateFlowFile.UNIQUE_FLOWFILES,
Boolean.toString(unique));
- runner.setProperty(GenerateFlowFile.BATCH_SIZE, unique ? "2" : "3");
-
- runner.run();
-
- runner.assertAllFlowFilesTransferred(GenerateFlowFile.SUCCESS, unique
? 2 : 3);
- final List<MockFlowFile> flowFiles =
runner.getFlowFilesForRelationship(GenerateFlowFile.SUCCESS);
- final MockFlowFile first = flowFiles.getFirst();
- for (final MockFlowFile flowFile : flowFiles) {
- assertEquals(STREAMED_FILE_SIZE_BYTES, flowFile.getSize());
- }
-
- if (unique) {
- assertFalse(Arrays.equals(first.getData(),
flowFiles.get(1).getData()));
- } else {
- for (int i = 1; i < flowFiles.size(); i++) {
- assertArrayEquals(first.getData(), flowFiles.get(i).getData());
- }
- }
- }
-
}