@@ -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 }
0 commit comments