Skip to content

Commit 1db1294

Browse files
committed
NIFI-15934 Handle oversized FlowFiles and empty header keys in PublishAMQP
1 parent 74c0727 commit 1db1294

2 files changed

Lines changed: 102 additions & 7 deletions

File tree

  • nifi-extension-bundles/nifi-amqp-bundle/nifi-amqp-processors/src

nifi-extension-bundles/nifi-amqp-bundle/nifi-amqp-processors/src/main/java/org/apache/nifi/amqp/processors/PublishAMQP.java

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.apache.nifi.expression.ExpressionLanguageScope;
3434
import org.apache.nifi.flowfile.FlowFile;
3535
import org.apache.nifi.migration.PropertyConfiguration;
36+
import org.apache.nifi.processor.DataUnit;
3637
import org.apache.nifi.processor.ProcessContext;
3738
import org.apache.nifi.processor.ProcessSession;
3839
import org.apache.nifi.processor.Relationship;
@@ -75,6 +76,7 @@
7576
@ReadsAttribute(attribute = AbstractAMQPProcessor.AMQP_CLUSTER_ID_ATTRIBUTE, description = "The ID of the AMQP Cluster"),
7677
})
7778
public class PublishAMQP extends AbstractAMQPProcessor<AMQPPublisher> {
79+
private static final long MAXIMUM_INPUT_FLOWFILE_SIZE_LIMIT = 128 * 1024 * 1024L;
7880

7981
public static final PropertyDescriptor EXCHANGE = new PropertyDescriptor.Builder()
8082
.name("Exchange Name")
@@ -108,6 +110,16 @@ public class PublishAMQP extends AbstractAMQPProcessor<AMQPPublisher> {
108110
.allowableValues(DeliveryGuarantee.class)
109111
.defaultValue(DeliveryGuarantee.AT_MOST_ONCE)
110112
.build();
113+
public static final PropertyDescriptor MAXIMUM_INPUT_FLOWFILE_SIZE = new PropertyDescriptor.Builder()
114+
.name("Maximum Input FlowFile Size")
115+
.description("Maximum size of an input FlowFile that will be read into memory before publishing. PublishAMQP reads FlowFile content into a byte array "
116+
+ "before publishing, so FlowFiles larger than this value are routed to failure before content is read. Configure this value according to "
117+
+ "broker limits and available JVM memory.")
118+
.required(true)
119+
.defaultValue("128 MB")
120+
.expressionLanguageSupported(ExpressionLanguageScope.ENVIRONMENT)
121+
.addValidator(StandardValidators.createDataSizeBoundsValidator(1, MAXIMUM_INPUT_FLOWFILE_SIZE_LIMIT))
122+
.build();
111123
public static final PropertyDescriptor HEADERS_SOURCE = new PropertyDescriptor.Builder()
112124
.name("Headers Source")
113125
.description("The source of the headers which will be applied to the published message.")
@@ -150,6 +162,7 @@ public class PublishAMQP extends AbstractAMQPProcessor<AMQPPublisher> {
150162
EXCHANGE,
151163
ROUTING_KEY,
152164
DELIVERY_GUARANTEE,
165+
MAXIMUM_INPUT_FLOWFILE_SIZE,
153166
HEADERS_SOURCE,
154167
HEADERS_PATTERN,
155168
HEADER_SEPARATOR
@@ -165,7 +178,7 @@ public class PublishAMQP extends AbstractAMQPProcessor<AMQPPublisher> {
165178
/**
166179
* Will construct AMQP message by extracting its body from the incoming {@link FlowFile}. AMQP Properties will be extracted from the
167180
* {@link FlowFile} and converted to {@link BasicProperties} to be sent along with the message. Upon success the incoming {@link FlowFile} is
168-
* transferred to 'success' {@link Relationship} and upon failure FlowFile is penalized and transferred to the 'failure' {@link Relationship}
181+
* transferred to 'success' {@link Relationship} and upon failure FlowFile is transferred to the 'failure' {@link Relationship}
169182
* <br>
170183
* <p>
171184
* NOTE: Attributes extracted from {@link FlowFile} are considered candidates for AMQP properties if their names are prefixed with
@@ -180,6 +193,14 @@ protected void processResource(final Connection connection, final AMQPPublisher
180193
return;
181194
}
182195

196+
final long maximumInputFlowFileSize = context.getProperty(MAXIMUM_INPUT_FLOWFILE_SIZE).evaluateAttributeExpressions().asDataSize(DataUnit.B).longValue();
197+
if (flowFile.getSize() > maximumInputFlowFileSize) {
198+
getLogger().warn("FlowFile {} with size {} bytes exceeds configured maximum input FlowFile size of {} bytes; routing to failure",
199+
flowFile, flowFile.getSize(), maximumInputFlowFileSize);
200+
session.transfer(flowFile, REL_FAILURE);
201+
return;
202+
}
203+
183204
final String routingKey = context.getProperty(ROUTING_KEY).evaluateAttributeExpressions(flowFile).getValue();
184205
if (routingKey == null) {
185206
throw new IllegalArgumentException("Failed to determine 'routing key' with provided value '"
@@ -201,7 +222,7 @@ protected void processResource(final Connection connection, final AMQPPublisher
201222
session.rollback();
202223
throw e;
203224
} catch (AMQPException e) {
204-
session.transfer(session.penalize(flowFile), REL_FAILURE);
225+
session.transfer(flowFile, REL_FAILURE);
205226
throw e;
206227
}
207228

@@ -235,7 +256,7 @@ public void migrateProperties(final PropertyConfiguration config) {
235256
* Extracts contents of the {@link FlowFile} as byte array.
236257
*/
237258
private byte[] extractMessage(final FlowFile flowFile, ProcessSession session) {
238-
final byte[] messageContent = new byte[(int) flowFile.getSize()];
259+
final byte[] messageContent = new byte[Math.toIntExact(flowFile.getSize())];
239260
session.read(flowFile, in -> StreamUtils.fillBuffer(in, messageContent, true));
240261
return messageContent;
241262
}
@@ -332,16 +353,26 @@ private Map<String, Object> validateAMQPHeaderProperty(final String amqpPropValu
332353
for (String strEntry : strEntries) {
333354
final String[] kv = strEntry.split("=", -1); // without using limit, trailing delimiter would be ignored
334355
if (kv.length == 2) {
335-
headers.put(kv[0].trim(), kv[1].trim());
356+
addHeader(headers, amqpPropValue, strEntry, kv[0], kv[1].trim());
336357
} else if (kv.length == 1) {
337-
headers.put(kv[0].trim(), null);
358+
addHeader(headers, amqpPropValue, strEntry, kv[0], null);
338359
} else {
339360
getLogger().warn("Malformed key value pair in AMQP header property ({}): {}", amqpPropValue, strEntry);
340361
}
341362
}
342363
return headers;
343364
}
344365

366+
private void addHeader(final Map<String, Object> headers, final String amqpPropValue, final String strEntry, final String headerKey, final Object headerValue) {
367+
final String trimmedHeaderKey = headerKey.trim();
368+
if (trimmedHeaderKey.isEmpty()) {
369+
getLogger().warn("Skipping AMQP header with empty key in property ({}): {}", amqpPropValue, strEntry);
370+
return;
371+
}
372+
373+
headers.put(trimmedHeaderKey, headerValue);
374+
}
375+
345376
protected Pattern getPattern(ProcessContext context, InputHeaderSource selectedHeaderSource) {
346377
return switch (selectedHeaderSource) {
347378
case FLOWFILE_ATTRIBUTES -> Pattern.compile(context.getProperty(HEADERS_PATTERN).evaluateAttributeExpressions().getValue());

nifi-extension-bundles/nifi-amqp-bundle/nifi-amqp-processors/src/test/java/org/apache/nifi/amqp/processors/PublishAMQPTest.java

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
import java.util.concurrent.ExecutorService;
3838

3939
import static org.junit.jupiter.api.Assertions.assertEquals;
40+
import static org.junit.jupiter.api.Assertions.assertFalse;
4041
import static org.junit.jupiter.api.Assertions.assertNotNull;
42+
import static org.junit.jupiter.api.Assertions.assertNull;
4143
import static org.junit.jupiter.api.Assertions.assertTrue;
4244

4345
public class PublishAMQPTest {
@@ -151,7 +153,7 @@ public void validateMalformedHeaderIgnoredAndPublishToSuccess() throws Exception
151153
expectedHeaders.put("foo3", null);
152154

153155
final Map<String, String> attributes = new HashMap<>();
154-
attributes.put(AbstractAMQPProcessor.AMQP_HEADERS_ATTRIBUTE, "foo=(bar,bar)|foo2=bar2|foo3|foo4=malformed=|foo5=mal=formed");
156+
attributes.put(AbstractAMQPProcessor.AMQP_HEADERS_ATTRIBUTE, "foo=(bar,bar)|foo2=bar2|foo3|foo4=malformed=|foo5=mal=formed||=ignored");
155157

156158
runner.enqueue("Hello Joe".getBytes(), attributes);
157159

@@ -171,6 +173,33 @@ public void validateMalformedHeaderIgnoredAndPublishToSuccess() throws Exception
171173
assertNotNull(channel.basicGet("queue2", true));
172174
}
173175

176+
@Test
177+
public void validateEmptyHeaderKeysIgnoredAndPublishToSuccess() throws Exception {
178+
setConnectionProperties(runner);
179+
runner.setProperty(PublishAMQP.HEADER_SEPARATOR, "|");
180+
181+
final Map<String, Object> expectedHeaders = new HashMap<>();
182+
expectedHeaders.put("foo", "bar");
183+
expectedHeaders.put("foo2", null);
184+
expectedHeaders.put("foo3", "");
185+
186+
final Map<String, String> attributes = new HashMap<>();
187+
attributes.put(AbstractAMQPProcessor.AMQP_HEADERS_ATTRIBUTE, "foo=bar|=missing| |foo2| foo3 = ");
188+
189+
runner.enqueue("Hello Joe".getBytes(), attributes);
190+
191+
runner.run();
192+
193+
final MockFlowFile successFF = runner.getFlowFilesForRelationship(PublishAMQP.REL_SUCCESS).getFirst();
194+
assertNotNull(successFF);
195+
196+
final Channel channel = pubProc.getConnection().createChannel();
197+
final GetResponse msg1 = channel.basicGet("queue1", true);
198+
assertNotNull(msg1);
199+
200+
assertEquals(expectedHeaders, msg1.getProps().getHeaders());
201+
}
202+
174203
@Test
175204
public void validateFailedPublishAndTransferToFailure() {
176205
setConnectionProperties(runner);
@@ -181,7 +210,42 @@ public void validateFailedPublishAndTransferToFailure() {
181210
runner.run();
182211

183212
assertTrue(runner.getFlowFilesForRelationship(PublishAMQP.REL_SUCCESS).isEmpty());
184-
assertNotNull(runner.getFlowFilesForRelationship(PublishAMQP.REL_FAILURE).getFirst());
213+
final MockFlowFile failureFlowFile = runner.getFlowFilesForRelationship(PublishAMQP.REL_FAILURE).getFirst();
214+
assertNotNull(failureFlowFile);
215+
assertFalse(failureFlowFile.isPenalized());
216+
runner.assertPenalizeCount(0);
217+
}
218+
219+
@Test
220+
public void validateOversizedFlowFileTransferredToFailureWithoutPublishing() throws Exception {
221+
setConnectionProperties(runner);
222+
runner.setProperty(PublishAMQP.MAXIMUM_INPUT_FLOWFILE_SIZE, "4 B");
223+
224+
runner.enqueue("Hello".getBytes());
225+
226+
runner.run();
227+
228+
assertTrue(runner.getFlowFilesForRelationship(PublishAMQP.REL_SUCCESS).isEmpty());
229+
final MockFlowFile failureFlowFile = runner.getFlowFilesForRelationship(PublishAMQP.REL_FAILURE).getFirst();
230+
assertNotNull(failureFlowFile);
231+
assertFalse(failureFlowFile.isPenalized());
232+
runner.assertPenalizeCount(0);
233+
234+
final Channel channel = pubProc.getConnection().createChannel();
235+
assertNull(channel.basicGet("queue1", true));
236+
}
237+
238+
@Test
239+
public void validateMaximumInputFlowFileSizeProperty() {
240+
assertEquals("Maximum Input FlowFile Size", PublishAMQP.MAXIMUM_INPUT_FLOWFILE_SIZE.getName());
241+
assertEquals("128 MB", PublishAMQP.MAXIMUM_INPUT_FLOWFILE_SIZE.getDefaultValue());
242+
243+
setConnectionProperties(runner);
244+
runner.setProperty(PublishAMQP.MAXIMUM_INPUT_FLOWFILE_SIZE, "128 MB");
245+
runner.assertValid();
246+
247+
runner.setProperty(PublishAMQP.MAXIMUM_INPUT_FLOWFILE_SIZE, "129 MB");
248+
runner.assertNotValid();
185249
}
186250

187251
@Test

0 commit comments

Comments
 (0)