Skip to content

Commit 14b4701

Browse files
committed
NIFI-15862: Addressed review feedback
1 parent 80678a2 commit 14b4701

6 files changed

Lines changed: 417 additions & 63 deletions

File tree

nifi-framework-api/src/main/java/org/apache/nifi/diagnostics/ThreadDumpTask.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,25 +33,25 @@ public class ThreadDumpTask implements DiagnosticTask {
3333

3434
@Override
3535
public DiagnosticsDumpElement captureDump(final boolean verbose) {
36-
final StringBuilder sb = new StringBuilder();
36+
String threadDump;
3737

3838
Path tempDirectory = null;
3939
try {
4040
final HotSpotDiagnosticMXBean diagnosticMXBean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
4141
// dumpThreads requires that the destination file does not already exist. Creating a private
4242
// temporary directory and writing to a fresh filename inside it avoids a time-of-check to
4343
// time-of-use race that would exist if we created a temp file and then deleted it before the
44-
// JNI call.
44+
// dumpThreads call.
4545
tempDirectory = Files.createTempDirectory("nifi-thread-dump-");
4646
final Path tempFile = tempDirectory.resolve("thread-dump.txt");
4747
try {
4848
diagnosticMXBean.dumpThreads(tempFile.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.TEXT_PLAIN);
49-
sb.append(Files.readString(tempFile));
49+
threadDump = Files.readString(tempFile);
5050
} finally {
5151
Files.deleteIfExists(tempFile);
5252
}
5353
} catch (final IOException e) {
54-
sb.append("Failed to capture thread dump: ").append(e.getMessage());
54+
threadDump = "Failed to capture thread dump: " + e.getMessage();
5555
} finally {
5656
if (tempDirectory != null) {
5757
try {
@@ -61,6 +61,6 @@ public DiagnosticsDumpElement captureDump(final boolean verbose) {
6161
}
6262
}
6363

64-
return new StandardDiagnosticsDumpElement("Thread Dump", Collections.singletonList(sb.toString()));
64+
return new StandardDiagnosticsDumpElement("Thread Dump", Collections.singletonList(threadDump));
6565
}
6666
}

nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,16 @@ public class FlowController implements ReportingTaskProvider, FlowAnalysisRulePr
293293

294294
private static final String ZOOKEEPER_STATE_PROVIDER_SERVER_CLASS = "org.apache.nifi.controller.state.providers.zookeeper.server.ZooKeeperStateProviderServer";
295295

296+
/**
297+
* Fixed size of the thread pool that runs framework tasks (status-history capture, analytics predictions,
298+
* Python extension discovery, and the component start/stop lifecycle). Processor and reporting-task
299+
* invocations are no longer dispatched on this pool -- they run on virtual threads managed by
300+
* {@link VirtualThreadSchedulingAgent} -- so this pool only needs to accommodate the background framework work.
301+
*/
302+
private static final int FRAMEWORK_TASK_POOL_SIZE = 8;
303+
296304
private final AtomicInteger maxTimerDrivenThreads;
297-
private final AtomicReference<FlowEngine> timerDrivenEngineRef;
305+
private final AtomicReference<FlowEngine> frameworkTaskEngineRef;
298306
private final VirtualThreadSchedulingAgent virtualThreadSchedulingAgent;
299307

300308
private final ContentRepository contentRepository;
@@ -553,7 +561,7 @@ private FlowController(
553561
stateManagerProvider.enableClusterProvider();
554562
}
555563

556-
timerDrivenEngineRef = new AtomicReference<>(new FlowEngine(maxTimerDrivenThreads.get(), "Timer-Driven Process"));
564+
frameworkTaskEngineRef = new AtomicReference<>(new FlowEngine(FRAMEWORK_TASK_POOL_SIZE, "FrameworkTaskEngine"));
557565

558566
final FlowFileRepository flowFileRepo = createFlowFileRepository(nifiProperties, extensionManager, resourceClaimManager);
559567
flowFileRepository = flowFileRepo;
@@ -600,7 +608,7 @@ private FlowController(
600608

601609
lifecycleStateManager = new StandardLifecycleStateManager();
602610
reloadComponent = new StandardReloadComponent(this);
603-
processScheduler = new StandardProcessScheduler(timerDrivenEngineRef.get(), this, stateManagerProvider, this.nifiProperties, lifecycleStateManager);
611+
processScheduler = new StandardProcessScheduler(frameworkTaskEngineRef.get(), this, stateManagerProvider, this.nifiProperties, lifecycleStateManager);
604612

605613
parameterContextManager = new StandardParameterContextManager();
606614
final long maxAppendableBytes = getMaxAppendableBytes();
@@ -790,7 +798,7 @@ private FlowController(
790798
analyticsEngine = new CachingConnectionStatusAnalyticsEngine(flowManager, statusHistoryRepository, statusAnalyticsModelMapFactory,
791799
predictionIntervalMillis, queryIntervalMillis, modelScoreName, modelScoreThreshold);
792800

793-
timerDrivenEngineRef.get().scheduleWithFixedDelay(() -> {
801+
frameworkTaskEngineRef.get().scheduleWithFixedDelay(() -> {
794802
try {
795803
Long startTs = System.currentTimeMillis();
796804
RepositoryStatusReport statusReport = flowFileEventRepository.reportTransferEvents(startTs);
@@ -811,7 +819,7 @@ private FlowController(
811819
eventAccess = new StandardEventAccess(flowManager, flowFileEventRepository, processScheduler, authorizer, provenanceRepository,
812820
auditService, analyticsEngine, flowFileRepository, contentRepository);
813821

814-
timerDrivenEngineRef.get().scheduleWithFixedDelay(() -> {
822+
frameworkTaskEngineRef.get().scheduleWithFixedDelay(() -> {
815823
try {
816824
statusHistoryRepository.capture(getNodeStatusSnapshot(), eventAccess.getControllerStatus(), getGarbageCollectionStatus(), new Date());
817825
} catch (final Exception e) {
@@ -1305,7 +1313,7 @@ public void initializeFlow(final QueueProvider queueProvider) throws IOException
13051313

13061314
notifyComponentsConfigurationRestored();
13071315

1308-
timerDrivenEngineRef.get().scheduleWithFixedDelay(() -> {
1316+
frameworkTaskEngineRef.get().scheduleWithFixedDelay(() -> {
13091317
try {
13101318
updateRemoteProcessGroups();
13111319
} catch (final Throwable t) {
@@ -1319,7 +1327,7 @@ public void initializeFlow(final QueueProvider queueProvider) throws IOException
13191327
LOG.info("Scheduled Flow Registry synchronization every {}", registrySyncInterval);
13201328

13211329
// Schedule the flow registry synchronization task
1322-
timerDrivenEngineRef.get().scheduleWithFixedDelay(() -> {
1330+
frameworkTaskEngineRef.get().scheduleWithFixedDelay(() -> {
13231331
final ProcessGroup rootGroup = flowManager.getRootGroup();
13241332
final List<ProcessGroup> allGroups = rootGroup.findAllProcessGroups();
13251333
allGroups.add(rootGroup);
@@ -1560,7 +1568,7 @@ public void trigger(final ComponentNode component) {
15601568
scheduleLongRunningTaskMonitor();
15611569

15621570
final Runnable discoverPythonExtensions = () -> extensionManager.discoverNewPythonExtensions(pythonBundle);
1563-
timerDrivenEngineRef.get().scheduleWithFixedDelay(discoverPythonExtensions, 1, 1, TimeUnit.MINUTES);
1571+
frameworkTaskEngineRef.get().scheduleWithFixedDelay(discoverPythonExtensions, 1, 1, TimeUnit.MINUTES);
15641572
} finally {
15651573
writeLock.unlock("onFlowInitialized");
15661574
}
@@ -1815,7 +1823,7 @@ public Authorizer getAuthorizer() {
18151823
public boolean isTerminated() {
18161824
this.readLock.lock();
18171825
try {
1818-
return null == this.timerDrivenEngineRef.get() || this.timerDrivenEngineRef.get().isTerminated();
1826+
return null == this.frameworkTaskEngineRef.get() || this.frameworkTaskEngineRef.get().isTerminated();
18191827
} finally {
18201828
this.readLock.unlock("isTerminated");
18211829
}
@@ -1839,7 +1847,7 @@ public void shutdown(final boolean kill) {
18391847

18401848
readLock.lock();
18411849
try {
1842-
if (isTerminated() || timerDrivenEngineRef.get().isTerminating()) {
1850+
if (isTerminated() || frameworkTaskEngineRef.get().isTerminating()) {
18431851
throw new IllegalStateException("Controller already stopped or still stopping...");
18441852
}
18451853

@@ -1889,18 +1897,23 @@ public void shutdown(final boolean kill) {
18891897
}
18901898

18911899
if (kill) {
1892-
this.timerDrivenEngineRef.get().shutdownNow();
1900+
this.frameworkTaskEngineRef.get().shutdownNow();
1901+
// frameworkTaskEngineRef.shutdownNow() only interrupts threads in the platform-thread framework pool.
1902+
// Processor/reporting-task work runs on virtual threads owned by the VirtualThreadSchedulingAgent, so
1903+
// those threads must be interrupted explicitly on the kill path rather than waiting for the final
1904+
// processScheduler.shutdown() call below to do it. This is idempotent with the later shutdown() call.
1905+
virtualThreadSchedulingAgent.shutdown();
18931906
LOG.info("Initiated immediate shutdown of flow controller...");
18941907
} else {
1895-
this.timerDrivenEngineRef.get().shutdown();
1908+
this.frameworkTaskEngineRef.get().shutdown();
18961909
LOG.info("Initiated graceful shutdown of flow controller...waiting up to {} seconds", gracefulShutdownSeconds);
18971910
}
18981911

18991912
try {
19001913
// Give thread pool up to the configured amount of time to finish, but no less than 2 seconds,
19011914
// in order to allow for a more graceful shutdown.
19021915
final long millisToWait = Math.max(2000, shutdownEnd - System.currentTimeMillis());
1903-
this.timerDrivenEngineRef.get().awaitTermination(millisToWait, TimeUnit.MILLISECONDS);
1916+
this.frameworkTaskEngineRef.get().awaitTermination(millisToWait, TimeUnit.MILLISECONDS);
19041917
} catch (final InterruptedException ie) {
19051918
LOG.info("Interrupted while waiting for controller termination.");
19061919
}
@@ -1911,7 +1924,7 @@ public void shutdown(final boolean kill) {
19111924
LOG.warn("Unable to shut down FlowFileRepository", t);
19121925
}
19131926

1914-
if (this.timerDrivenEngineRef.get().isTerminated()) {
1927+
if (this.frameworkTaskEngineRef.get().isTerminated()) {
19151928
LOG.info("Controller has been terminated successfully.");
19161929
} else {
19171930
LOG.warn("Controller hasn't terminated properly. There exists an uninterruptable thread that "
@@ -2134,43 +2147,24 @@ public int getMaxTimerDrivenThreadCount() {
21342147
}
21352148

21362149
public int getActiveTimerDrivenThreadCount() {
2137-
return timerDrivenEngineRef.get().getActiveCount() + virtualThreadSchedulingAgent.getActiveThreadCount();
2150+
return frameworkTaskEngineRef.get().getActiveCount() + virtualThreadSchedulingAgent.getActiveThreadCount();
21382151
}
21392152

21402153
public void setMaxTimerDrivenThreadCount(final int maxThreadCount) {
2154+
if (maxThreadCount < 1) {
2155+
throw new IllegalArgumentException("Cannot set max number of threads to less than 1");
2156+
}
2157+
21412158
writeLock.lock();
21422159
try {
2143-
setMaxThreadCount(maxThreadCount, "Timer Driven", this.timerDrivenEngineRef.get(), this.maxTimerDrivenThreads);
2160+
final int previousMax = maxTimerDrivenThreads.getAndSet(maxThreadCount);
21442161
virtualThreadSchedulingAgent.setMaxThreadCount(maxThreadCount);
2162+
LOG.info("Maximum Timer-Driven Thread Count updated [{}] previous [{}]", maxThreadCount, previousMax);
21452163
} finally {
21462164
writeLock.unlock("setMaxTimerDrivenThreadCount");
21472165
}
21482166
}
21492167

2150-
/**
2151-
* Updates the number of threads that can be simultaneously used for executing processors.
2152-
* This method must be called while holding the write lock!
2153-
*
2154-
* @param maxThreadCount Requested new thread pool size
2155-
* @param poolName Thread Pool Name
2156-
* @param engine Flow Engine executor or null when terminated
2157-
* @param maxThreads Internal tracker for Maximum Threads
2158-
*/
2159-
private void setMaxThreadCount(final int maxThreadCount, final String poolName, final FlowEngine engine, final AtomicInteger maxThreads) {
2160-
if (maxThreadCount < 1) {
2161-
throw new IllegalArgumentException("Cannot set max number of threads to less than 1");
2162-
}
2163-
2164-
maxThreads.getAndSet(maxThreadCount);
2165-
if (engine == null) {
2166-
LOG.debug("[{}] Engine not found: Maximum Thread Count not updated", poolName);
2167-
} else {
2168-
final int previousCorePoolSize = engine.getCorePoolSize();
2169-
engine.setCorePoolSize(maxThreadCount);
2170-
LOG.info("[{}] Maximum Thread Count updated [{}] previous [{}]", poolName, maxThreadCount, previousCorePoolSize);
2171-
}
2172-
}
2173-
21742168
public UserAwareEventAccess getEventAccess() {
21752169
return eventAccess;
21762170
}

nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/DynamicSemaphore.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,20 @@ public int availablePermits() {
8181
return semaphore.availablePermits();
8282
}
8383

84+
/**
85+
* Returns the number of permits currently in use, computed atomically against any concurrent
86+
* call to {@link #setMaxPermits(int)}. A non-atomic {@code getMaxPermits() - availablePermits()}
87+
* outside this class can observe a transient inconsistency if a resize is in progress between
88+
* the two reads, which is undesirable for metrics that feed cluster heartbeats.
89+
*
90+
* @return the number of permits that have been acquired but not yet released. The returned value
91+
* is a best-effort snapshot because permits may be acquired or released by other threads
92+
* before the caller can act on it, but it is consistent with a single point in time.
93+
*/
94+
public synchronized int getInUsePermits() {
95+
return maxPermits - semaphore.availablePermits();
96+
}
97+
8498
/**
8599
* Extends {@link Semaphore} in order to expose the protected {@link #reducePermits(int)}
86100
* method, which is needed in order to dynamically shrink the pool of available permits.

0 commit comments

Comments
 (0)