Skip to content

Commit b2e1707

Browse files
committed
Merge branch 'main' into NIFI-15901
2 parents b897995 + 6f65bc3 commit b2e1707

33 files changed

Lines changed: 2734 additions & 265 deletions

File tree

nifi-extension-bundles/nifi-splunk-bundle/nifi-splunk-processors/src/main/java/org/apache/nifi/processors/splunk/PutSplunk.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,26 @@ public void cleanup() {
101101
}
102102

103103
@Override
104-
public void onTrigger(ProcessContext context, ProcessSessionFactory sessionFactory) throws ProcessException {
104+
public void onTrigger(final ProcessContext context, final ProcessSessionFactory sessionFactory) throws ProcessException {
105105
// first complete any batches from previous executions
106+
boolean completedAny = false;
106107
FlowFileMessageBatch batch;
107108
while ((batch = completeBatches.poll()) != null) {
108109
batch.completeSession();
110+
completedAny = true;
109111
}
110112

111113
// create a session and try to get a FlowFile, if none available then close any idle senders
112114
final ProcessSession session = sessionFactory.createSession();
113115
final FlowFile flowFile = session.get();
114116

115117
if (flowFile == null) {
118+
// The processor is annotated with @TriggerWhenEmpty so onTrigger is invoked even with no input,
119+
// allowing async send callbacks to drain completeBatches. Yield when nothing was drained to avoid
120+
// a busy scheduling loop on an idle processor.
121+
if (!completedAny) {
122+
context.yield();
123+
}
116124
return;
117125
}
118126

nifi-extension-bundles/nifi-splunk-bundle/nifi-splunk-processors/src/test/java/org/apache/nifi/processors/splunk/TestPutSplunk.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@
4242
import java.util.concurrent.TimeUnit;
4343

4444
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
45+
import static org.junit.jupiter.api.Assertions.assertFalse;
4546
import static org.junit.jupiter.api.Assertions.assertNotNull;
4647
import static org.junit.jupiter.api.Assertions.assertNull;
48+
import static org.junit.jupiter.api.Assertions.assertTrue;
4749

4850
public class TestPutSplunk {
4951

@@ -261,6 +263,31 @@ public void testCompletingPreviousBatchOnNextExecution() throws Exception {
261263
checkReceivedAllData(message);
262264
}
263265

266+
@Test
267+
@Timeout(value = DEFAULT_TEST_TIMEOUT_PERIOD, unit = TimeUnit.MILLISECONDS)
268+
public void testYieldsWhenIdle() throws Exception {
269+
createTestServer(TransportProtocol.TCP);
270+
271+
runner.run(1);
272+
273+
assertTrue(runner.isYieldCalled(), "Processor should yield when no FlowFile is available to avoid busy scheduling");
274+
}
275+
276+
@Test
277+
@Timeout(value = DEFAULT_TEST_TIMEOUT_PERIOD, unit = TimeUnit.MILLISECONDS)
278+
public void testDoesNotYieldWhenFlowFileProcessed() throws Exception {
279+
createTestServer(TransportProtocol.TCP);
280+
final String message = "This is one message, should send the whole FlowFile";
281+
282+
runner.enqueue(message);
283+
runner.run(1);
284+
runner.assertAllFlowFilesTransferred(PutSplunk.REL_SUCCESS, 1);
285+
286+
checkReceivedAllData(message);
287+
288+
assertFalse(runner.isYieldCalled(), "Processor should not yield after successfully processing a FlowFile");
289+
}
290+
264291
@Test
265292
@Timeout(value = DEFAULT_TEST_TIMEOUT_PERIOD, unit = TimeUnit.MILLISECONDS)
266293
public void testUnableToCreateConnectionShouldRouteToFailure() {

nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/connectable/ConnectionUtils.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ public static FlowFileCloneResult clone(final FlowFileRecord flowFile, final Col
101101
}
102102

103103
private static RepositoryRecord createRepositoryRecord(final FlowFileRecord flowFile, final FlowFileQueue destinationQueue) {
104-
final StandardRepositoryRecord repoRecord = new StandardRepositoryRecord(null, flowFile);
104+
// The FlowFile is being introduced to this repository for the first time, so it is tracked as a CREATE record.
105+
final StandardRepositoryRecord repoRecord = new StandardRepositoryRecord(destinationQueue);
105106
repoRecord.setWorking(flowFile, Collections.emptyMap(), false);
106107
repoRecord.setDestination(destinationQueue);
107108
return repoRecord;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.nifi.connectable;
18+
19+
import org.apache.nifi.connectable.ConnectionUtils.FlowFileCloneResult;
20+
import org.apache.nifi.controller.queue.FlowFileQueue;
21+
import org.apache.nifi.controller.repository.ContentRepository;
22+
import org.apache.nifi.controller.repository.FlowFileRecord;
23+
import org.apache.nifi.controller.repository.FlowFileRepository;
24+
import org.apache.nifi.controller.repository.RepositoryRecord;
25+
import org.apache.nifi.controller.repository.RepositoryRecordType;
26+
import org.apache.nifi.controller.repository.StandardFlowFileRecord;
27+
import org.apache.nifi.controller.repository.claim.ContentClaim;
28+
import org.junit.jupiter.api.BeforeEach;
29+
import org.junit.jupiter.api.Test;
30+
import org.mockito.Mockito;
31+
32+
import java.util.ArrayList;
33+
import java.util.List;
34+
import java.util.concurrent.atomic.AtomicLong;
35+
36+
import static org.junit.jupiter.api.Assertions.assertEquals;
37+
import static org.junit.jupiter.api.Assertions.assertNotNull;
38+
import static org.junit.jupiter.api.Assertions.assertTrue;
39+
import static org.mockito.Mockito.when;
40+
41+
public class TestConnectionUtils {
42+
43+
private FlowFileRepository flowFileRepository;
44+
private ContentRepository contentRepository;
45+
46+
@BeforeEach
47+
public void setup() {
48+
flowFileRepository = Mockito.mock(FlowFileRepository.class);
49+
contentRepository = Mockito.mock(ContentRepository.class);
50+
51+
final AtomicLong nextId = new AtomicLong(1000L);
52+
when(flowFileRepository.getNextFlowFileSequence()).thenAnswer(invocation -> nextId.getAndIncrement());
53+
}
54+
55+
@Test
56+
public void testCloneSingleDestinationProducesCreateRecord() {
57+
final FlowFileRecord flowFile = new StandardFlowFileRecord.Builder()
58+
.id(1L)
59+
.addAttribute("uuid", "test-uuid-1")
60+
.build();
61+
62+
final FlowFileQueue destinationQueue = Mockito.mock(FlowFileQueue.class);
63+
final Connection destination = mockConnection(destinationQueue);
64+
65+
final FlowFileCloneResult result = ConnectionUtils.clone(flowFile, List.of(destination), flowFileRepository, contentRepository);
66+
67+
final List<RepositoryRecord> records = result.getRepositoryRecords();
68+
assertEquals(1, records.size());
69+
final RepositoryRecord record = records.get(0);
70+
assertEquals(RepositoryRecordType.CREATE, record.getType());
71+
assertEquals(destinationQueue, record.getDestination());
72+
assertNotNull(record.getCurrent());
73+
assertEquals(flowFile.getId(), record.getCurrent().getId());
74+
}
75+
76+
@Test
77+
public void testCloneMultipleDestinationsAllProduceCreateRecords() {
78+
final ContentClaim contentClaim = Mockito.mock(ContentClaim.class);
79+
final FlowFileRecord flowFile = new StandardFlowFileRecord.Builder()
80+
.id(1L)
81+
.addAttribute("uuid", "test-uuid-1")
82+
.contentClaim(contentClaim)
83+
.size(1024L)
84+
.build();
85+
86+
final FlowFileQueue queueOne = Mockito.mock(FlowFileQueue.class);
87+
final FlowFileQueue queueTwo = Mockito.mock(FlowFileQueue.class);
88+
final FlowFileQueue queueThree = Mockito.mock(FlowFileQueue.class);
89+
final List<Connection> destinations = List.of(
90+
mockConnection(queueOne),
91+
mockConnection(queueTwo),
92+
mockConnection(queueThree));
93+
94+
final FlowFileCloneResult result = ConnectionUtils.clone(flowFile, destinations, flowFileRepository, contentRepository);
95+
96+
final List<RepositoryRecord> records = result.getRepositoryRecords();
97+
assertEquals(destinations.size(), records.size());
98+
99+
// Each record produced by clone() represents a FlowFile being introduced to the repository,
100+
// whether it is the original routed FlowFile or a sibling clone.
101+
assertTrue(records.stream().allMatch(record -> record.getType() == RepositoryRecordType.CREATE));
102+
103+
// The clones (one per additional destination beyond the first) should each have triggered a
104+
// claimant count increment on the shared Content Claim.
105+
Mockito.verify(contentRepository, Mockito.times(destinations.size() - 1)).incrementClaimaintCount(contentClaim);
106+
107+
final List<FlowFileQueue> destinationQueues = new ArrayList<>();
108+
for (final RepositoryRecord record : records) {
109+
destinationQueues.add(record.getDestination());
110+
}
111+
assertTrue(destinationQueues.contains(queueOne));
112+
assertTrue(destinationQueues.contains(queueTwo));
113+
assertTrue(destinationQueues.contains(queueThree));
114+
}
115+
116+
private Connection mockConnection(final FlowFileQueue queue) {
117+
final Connection connection = Mockito.mock(Connection.class);
118+
when(connection.getFlowFileQueue()).thenReturn(queue);
119+
return connection;
120+
}
121+
}

0 commit comments

Comments
 (0)