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 a5d5e509bf6 NIFI-16288 Allow Connectors to stop while components are 
starting or enabling (#11664)
a5d5e509bf6 is described below

commit a5d5e509bf62d452cf90fd9c5cfc4040689bab95
Author: Mark Payne <[email protected]>
AuthorDate: Thu Sep 10 14:27:21 2026 -0400

    NIFI-16288 Allow Connectors to stop while components are starting or 
enabling (#11664)
---
 .../nifi/controller/StandardProcessorNode.java     |  80 +++++--
 .../controller/service/ServiceStateTransition.java |  53 ++++-
 .../service/StandardControllerServiceNode.java     | 237 +++++++++++---------
 .../connector/StandardConnectorNode.java           | 128 ++++++++---
 .../StandaloneProcessGroupLifecycle.java           |   2 +-
 .../scheduling/StandardProcessScheduler.java       |  12 +-
 .../ControllerServiceEnablingConnector.java        | 113 ++++++++++
 .../connector/ProcessorStartFailureConnector.java  |  95 ++++++++
 .../connector/StandardConnectorNodeIT.java         | 247 ++++++++++++++++++++-
 .../connector/processors/CreateDummyFlowFile.java  |   2 +-
 .../impl/BlockingEnablingCounterService.java       | 122 ++++++++++
 .../impl/FailingEnablingCounterService.java        |  50 +++++
 .../scheduling/TestStandardProcessScheduler.java   |  91 ++++++++
 .../processors/FailOnScheduledProcessor.java       |  36 ++-
 .../TestStandardControllerServiceProvider.java     |  54 ++++-
 .../org.apache.nifi.components.connector.Connector |   2 +
 .../org.apache.nifi.controller.ControllerService   |   4 +-
 .../services/org.apache.nifi.processor.Processor   |   3 +-
 18 files changed, 1140 insertions(+), 191 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
index e24af1afea7..2c042fd816e 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
@@ -187,6 +187,8 @@ public class StandardProcessorNode extends ProcessorNode 
implements Connectable
     private SchedulingStrategy schedulingStrategy; // guarded by synchronized 
keyword
     private ExecutionNode executionNode;
     private final Map<Thread, ActiveTask> activeThreads = new 
ConcurrentHashMap<>(48);
+    private final AtomicReference<AtomicLong> activeStartupAttempt = new 
AtomicReference<>();
+    private final AtomicReference<AtomicLong> runningStartupAttempt = new 
AtomicReference<>();
     private final int hashCode;
     private volatile boolean hasActiveThreads = false;
 
@@ -1519,7 +1521,9 @@ public class StandardProcessorNode extends ProcessorNode 
implements Connectable
         }
 
         if (starting) { // will ensure that the Processor represented by this 
node can only be started once
-            initiateStart(taskScheduler, administrativeYieldMillis, 
timeoutMillis, new AtomicLong(0), processContextFactory, 
schedulingAgentCallback, triggerLifecycleMethods);
+            final AtomicLong startupAttemptCount = new AtomicLong(0);
+            activeStartupAttempt.set(startupAttemptCount);
+            initiateStart(taskScheduler, administrativeYieldMillis, 
timeoutMillis, startupAttemptCount, processContextFactory, 
schedulingAgentCallback, triggerLifecycleMethods);
         } else {
             final String procName = 
processorRef.get().getProcessor().toString();
             procLog.warn("Cannot start {} because it is not currently stopped. 
Current state is {}", procName, currentState);
@@ -1681,6 +1685,10 @@ public class StandardProcessorNode extends ProcessorNode 
implements Connectable
             final AtomicLong startupAttemptCount, final 
Supplier<ProcessContext> processContextFactory, final SchedulingAgentCallback 
schedulingAgentCallback,
             final boolean triggerLifecycleMethods) {
 
+        if (activeStartupAttempt.get() != startupAttemptCount) {
+            return;
+        }
+
         final Processor processor = getProcessor();
         final ComponentLog procLog = new 
StandardComponentLog(StandardProcessorNode.this.getIdentifier(), processor, new 
StandardLoggingContext(StandardProcessorNode.this));
 
@@ -1692,10 +1700,16 @@ public class StandardProcessorNode extends 
ProcessorNode implements Connectable
 
         // Create a task to invoke the @OnScheduled annotation of the processor
         final Callable<Void> startupTask = () -> {
+            if (activeStartupAttempt.get() != startupAttemptCount) {
+                schedulingAgentCallback.onTaskComplete();
+                return null;
+            }
+
             final ScheduledState currentScheduleState = scheduledState.get();
             if (currentScheduleState == ScheduledState.STOPPING || 
currentScheduleState == ScheduledState.STOPPED || getDesiredState() == 
ScheduledState.STOPPED) {
                 LOG.info("Aborting start of {}: scheduledState={}, 
desiredState={}, validationStatus={}",
                         StandardProcessorNode.this, currentScheduleState, 
getDesiredState(), getValidationStatus());
+                activeStartupAttempt.compareAndSet(startupAttemptCount, null);
                 schedulingAgentCallback.onTaskComplete();
                 completeStopAction();
                 return null;
@@ -1707,6 +1721,7 @@ public class StandardProcessorNode extends ProcessorNode 
implements Connectable
                     final ValidationState validationState = 
getValidationState();
                     procLog.warn("Cannot run once {} because Processor is not 
valid (Validation State is {}: {}). Returning to stopped.",
                             StandardProcessorNode.this, validationState, 
validationState.getValidationErrors());
+                    activeStartupAttempt.compareAndSet(startupAttemptCount, 
null);
                     schedulingAgentCallback.onTaskComplete();
                     completeStopAction();
                     return null;
@@ -1725,11 +1740,11 @@ public class StandardProcessorNode extends 
ProcessorNode implements Connectable
                     LOG.debug("Cannot start {} because Processor is currently 
not valid; will try again after 500 ms", StandardProcessorNode.this);
                 }
 
-                // re-initiate the entire process
-                final Runnable initiateStartTask = () -> 
initiateStart(taskScheduler, administrativeYieldMillis, timeoutMillis, 
startupAttemptCount,
-                    processContextFactory, schedulingAgentCallback, 
triggerLifecycleMethods);
-
-                taskScheduler.schedule(initiateStartTask, 500, 
TimeUnit.MILLISECONDS);
+                if (activeStartupAttempt.get() == startupAttemptCount) {
+                    final Runnable initiateStartTask = () -> 
initiateStart(taskScheduler, administrativeYieldMillis, timeoutMillis, 
startupAttemptCount,
+                        processContextFactory, schedulingAgentCallback, 
triggerLifecycleMethods);
+                    taskScheduler.schedule(initiateStartTask, 500, 
TimeUnit.MILLISECONDS);
+                }
 
                 schedulingAgentCallback.onTaskComplete();
                 return null;
@@ -1739,14 +1754,32 @@ public class StandardProcessorNode extends 
ProcessorNode implements Connectable
             completionTimestampRef.set(System.currentTimeMillis() + 
timeoutMillis);
 
             final ProcessContext processContext = processContextFactory.get();
+            final boolean startPermitted;
+            synchronized (this) {
+                final ScheduledState currentState = scheduledState.get();
+                startPermitted = activeStartupAttempt.get() == 
startupAttemptCount && currentState != ScheduledState.STOPPING
+                    && currentState != ScheduledState.STOPPED && 
getDesiredState() != ScheduledState.STOPPED;
+                if (startPermitted) {
+                    runningStartupAttempt.set(startupAttemptCount);
+                    if (triggerLifecycleMethods) {
+                        activateThread();
+                    }
+                }
+            }
 
+            if (!startPermitted) {
+                schedulingAgentCallback.onTaskComplete();
+                completeStopAction();
+                return null;
+            }
+
+            boolean startupCompleted = false;
             try (final NarCloseable ignored = 
NarCloseable.withComponentNarLoader(getExtensionManager(), 
processor.getClass(), processor.getIdentifier())) {
                 try {
                     hasActiveThreads = true;
 
                     if (triggerLifecycleMethods) {
                         LOG.debug("Invoking @OnScheduled methods of {}", 
processor);
-                        activateThread();
                         try {
                             
ReflectionUtils.invokeMethodsWithAnnotation(OnScheduled.class, processor, 
processContext);
                         } finally {
@@ -1760,7 +1793,9 @@ public class StandardProcessorNode extends ProcessorNode 
implements Connectable
                         (desiredState == ScheduledState.RUNNING && 
scheduledState.compareAndSet(ScheduledState.STARTING, ScheduledState.RUNNING))
                             || (desiredState == ScheduledState.RUN_ONCE && 
scheduledState.compareAndSet(ScheduledState.RUN_ONCE, ScheduledState.RUN_ONCE))
                     ) {
+                        startupCompleted = true;
                         LOG.debug("Successfully completed the @OnScheduled 
methods of {}; will now start triggering processor to run", processor);
+                        
activeStartupAttempt.compareAndSet(startupAttemptCount, null);
                         schedulingAgentCallback.trigger(); // callback 
provided by StandardProcessScheduler to essentially initiate component's 
onTrigger() cycle
                     } else {
                         LOG.info("Successfully invoked @OnScheduled methods of 
{} but scheduled state is no longer STARTING so will stop processor now; 
current state = {}, desired state = {}",
@@ -1792,7 +1827,7 @@ public class StandardProcessorNode extends ProcessorNode 
implements Connectable
                 } finally {
                     schedulingAgentCallback.onTaskComplete();
                 }
-            } catch (Exception e) {
+            } catch (final Exception e) {
                 final Throwable cause = (e instanceof 
InvocationTargetException) ? e.getCause() : e;
                 procLog.error("Failed to properly initialize Processor. If 
still scheduled to run, NiFi will attempt to "
                     + "initialize and run the Processor again after the 
'Administrative Yield Duration' has elapsed. Failure is due to " + cause, 
cause);
@@ -1810,13 +1845,17 @@ public class StandardProcessorNode extends 
ProcessorNode implements Connectable
                 }
 
                 // make sure we only continue retry loop if STOP action wasn't 
initiated
-                if (scheduledState.get() != ScheduledState.STOPPING && 
scheduledState.get() != ScheduledState.RUN_ONCE) {
-                    // re-initiate the entire process
+                if (activeStartupAttempt.get() == startupAttemptCount && 
scheduledState.get() == ScheduledState.STARTING && getDesiredState() == 
ScheduledState.RUNNING) {
                     final Runnable initiateStartTask = () -> 
initiateStart(taskScheduler, administrativeYieldMillis, timeoutMillis, 
startupAttemptCount,
                         processContextFactory, schedulingAgentCallback, 
triggerLifecycleMethods);
-
                     taskScheduler.schedule(initiateStartTask, 
administrativeYieldMillis, TimeUnit.MILLISECONDS);
                 } else {
+                    activeStartupAttempt.compareAndSet(startupAttemptCount, 
null);
+                    completeStopAction();
+                }
+            } finally {
+                runningStartupAttempt.compareAndSet(startupAttemptCount, null);
+                if (!startupCompleted && activeStartupAttempt.get() != 
startupAttemptCount && getDesiredState() == ScheduledState.STOPPED) {
                     completeStopAction();
                 }
             }
@@ -1828,7 +1867,8 @@ public class StandardProcessorNode extends ProcessorNode 
implements Connectable
         try {
             // Trigger the task in a background thread.
             taskFuture = schedulingAgentCallback.scheduleTask(startupTask);
-        } catch (RejectedExecutionException rejectedExecutionException) {
+        } catch (final RejectedExecutionException rejectedExecutionException) {
+            activeStartupAttempt.compareAndSet(startupAttemptCount, null);
             final ValidationState validationState = getValidationState();
             LOG.error("Unable to start {}.  Last known validation state was {} 
: {}", this, validationState, validationState.getValidationErrors(), 
rejectedExecutionException);
             return;
@@ -1993,9 +2033,19 @@ public class StandardProcessorNode extends ProcessorNode 
implements Connectable
             // before stop() was called. If that happens the stop processor
             // routine will be initiated in start() method, otherwise the IF
             // part will handle the stop processor routine.
-            final boolean updated = 
this.scheduledState.compareAndSet(ScheduledState.STARTING, 
ScheduledState.STOPPING);
-            if (updated) {
-                LOG.debug("Transitioned state of {} from STARTING to 
STOPPING", this);
+            synchronized (this) {
+                final boolean updated = 
this.scheduledState.compareAndSet(ScheduledState.STARTING, 
ScheduledState.STOPPING);
+                if (updated) {
+                    LOG.debug("Transitioned state of {} from STARTING to 
STOPPING", this);
+                    activeStartupAttempt.set(null);
+                    for (final Thread activeThread : activeThreads.keySet()) {
+                        activeThread.interrupt();
+                    }
+
+                    if (activeThreads.isEmpty() && runningStartupAttempt.get() 
== null) {
+                        completeStopAction();
+                    }
+                }
             }
         }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/ServiceStateTransition.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/ServiceStateTransition.java
index 67de046c2cc..734b16731c4 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/ServiceStateTransition.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/ServiceStateTransition.java
@@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Condition;
@@ -67,7 +68,7 @@ public class ServiceStateTransition {
         }
     }
 
-    public boolean enable(final ControllerServiceReference 
controllerServiceReference) {
+    public boolean enable(final ControllerServiceReference 
controllerServiceReference, final CompletableFuture<?> enabledFuture) {
         writeLock.lock();
         try {
             if (state == ControllerServiceState.ENABLED) {
@@ -75,8 +76,8 @@ public class ServiceStateTransition {
                 return true;
             }
 
-            if (state != ControllerServiceState.ENABLING) {
-                logger.debug("{} cannot be transitioned to enabled because 
it's not currently ENABLING but rather {}", controllerServiceNode, state);
+            if (state != ControllerServiceState.ENABLING || 
!enabledFutures.contains(enabledFuture)) {
+                logger.debug("{} cannot be transitioned to enabled because the 
enable request is no longer active and the current state is {}", 
controllerServiceNode, state);
                 return false;
             }
 
@@ -84,6 +85,7 @@ public class ServiceStateTransition {
             logger.debug("{} is now fully ENABLED", controllerServiceNode);
 
             enabledFutures.forEach(future -> future.complete(null));
+            enabledFutures.clear();
         } finally {
             writeLock.unlock();
         }
@@ -108,31 +110,58 @@ public class ServiceStateTransition {
         return true;
     }
 
-    public boolean transitionToDisabling(final ControllerServiceState 
expectedState, final CompletableFuture<?> disabledFuture) {
+    public boolean isEnabling(final CompletableFuture<?> enabledFuture) {
+        readLock.lock();
+        try {
+            return state == ControllerServiceState.ENABLING && 
enabledFutures.contains(enabledFuture);
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public ControllerServiceState transitionToDisabling(final 
CompletableFuture<?> disabledFuture) {
         writeLock.lock();
         try {
-            if (expectedState != state) {
-                logger.debug("{} cannot be transitioned to DISABLING because 
its state is {}, not the expected {}", controllerServiceNode, state, 
expectedState);
-                return false;
+            final ControllerServiceState previousState = state;
+            if (previousState == ControllerServiceState.DISABLED) {
+                disabledFuture.complete(null);
+            } else if (previousState == ControllerServiceState.DISABLING) {
+                disabledFutures.add(disabledFuture);
+            } else {
+                state = ControllerServiceState.DISABLING;
+                stateChangeCondition.signalAll();
+                disabledFutures.add(disabledFuture);
             }
 
-            state = ControllerServiceState.DISABLING;
-            stateChangeCondition.signalAll();
-            disabledFutures.add(disabledFuture);
-            return true;
+            if (previousState == ControllerServiceState.ENABLING) {
+                for (final CompletableFuture<?> enabledFuture : 
enabledFutures) {
+                    enabledFuture.completeExceptionally(new 
CancellationException("Controller Service enablement cancelled by disable 
request"));
+                }
+
+                enabledFutures.clear();
+            }
+
+            return previousState;
         } finally {
             writeLock.unlock();
         }
     }
 
-    public void disable() {
+    public boolean disable() {
         writeLock.lock();
         try {
+            if (state != ControllerServiceState.DISABLING) {
+                logger.debug("{} cannot be transitioned to DISABLED because 
its state is {}", controllerServiceNode, state);
+                return false;
+            }
+
             state = ControllerServiceState.DISABLED;
             logger.info("{} is now fully DISABLED", controllerServiceNode);
 
             stateChangeCondition.signalAll();
             disabledFutures.forEach(future -> future.complete(null));
+            disabledFutures.clear();
+            return true;
         } finally {
             writeLock.unlock();
         }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
index b2010072027..a2fb24a7c5e 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
@@ -94,7 +94,9 @@ import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
@@ -132,6 +134,8 @@ public class StandardControllerServiceNode extends 
AbstractComponentNode impleme
     private final VerifiableComponentFactory verifiableComponentFactory;
 
     private final AtomicBoolean active;
+    private final AtomicBoolean enablingTaskRunning = new AtomicBoolean();
+    private final AtomicReference<Future<?>> enablingTask = new 
AtomicReference<>();
 
     public StandardControllerServiceNode(final 
LoggableComponent<ControllerService> implementation, final 
LoggableComponent<ControllerService> proxiedControllerService,
                                          final 
ControllerServiceInvocationHandler invocationHandler, final String id, final 
ValidationContextFactory validationContextFactory,
@@ -629,9 +633,9 @@ public class StandardControllerServiceNode extends 
AbstractComponentNode impleme
      * Upon successful invocation of @OnEnabled this service will be 
transitioned to
      * ENABLED state.
      * <br>
-     * In the event where enabling took longer then expected by the user and 
such user
-     * initiated disable operation, this service will be automatically 
disabled as soon
-     * as it reached ENABLED state.
+     * When a disable operation is initiated during enabling, the enable 
future is completed exceptionally
+     * with a {@link CancellationException}. The service remains DISABLING 
until the enable lifecycle method returns
+     * and the corresponding disable lifecycle method finishes.
      */
     @Override
     public CompletableFuture<Void> enable(final ScheduledExecutorService 
scheduler, final long administrativeYieldMillis, final boolean 
completeExceptionallyOnFailure) {
@@ -644,130 +648,154 @@ public class StandardControllerServiceNode extends 
AbstractComponentNode impleme
 
         final CompletableFuture<Void> future = new CompletableFuture<>();
 
-        if 
(!stateTransition.transitionToEnabling(ControllerServiceState.DISABLED, 
future)) {
-            future.complete(null);
-            return future;
+        final boolean transitionedToEnabling;
+        synchronized (active) {
+            transitionedToEnabling = 
stateTransition.transitionToEnabling(ControllerServiceState.DISABLED, future);
+            if (transitionedToEnabling) {
+                active.set(true);
+            }
         }
 
-        synchronized (active) {
-            this.active.set(true);
+        if (!transitionedToEnabling) {
+            future.complete(null);
+            return future;
         }
 
         final AtomicLong enablingDelay = new AtomicLong(0);
         final AtomicLong validationDelay = new AtomicLong(0);
         final ControllerServiceProvider controllerServiceProvider = 
this.serviceProvider;
         final StandardControllerServiceNode serviceNode = this;
-        scheduler.execute(new Runnable() {
+        final Runnable enablingRunnable = new Runnable() {
             @Override
             public void run() {
                 final ConfigurationContext configContext = 
providedConfigurationContext == null
                     ? new StandardConfigurationContext(serviceNode, 
controllerServiceProvider, null)
                     : providedConfigurationContext;
+                boolean enabled = false;
+                synchronized (active) {
+                    if (!active.get() || !stateTransition.isEnabling(future)) {
+                        return;
+                    }
 
-                if (!isActive()) {
-                    LOG.warn("Enabling {} stopped: no active status", 
serviceNode);
-                    stateTransition.disable();
-                    future.complete(null);
-                    return;
-                }
-
-                // Perform validation - if a ConfigurationContext was 
provided, validate against its properties
-                final ValidationState validationState;
-                if (providedConfigurationContext == null) {
-                    performValidation();
-                    validationState = getValidationState();
-                } else {
-                    final Map<String, String> properties = 
providedConfigurationContext.getAllProperties();
-                    final ValidationContext validationContext = 
createValidationContext(properties, getAnnotationData(), getParameterLookup(), 
true);
-                    validationState = performValidation(validationContext);
+                    enablingTaskRunning.set(true);
                 }
 
-                final ValidationStatus validationStatus = 
validationState.getStatus();
-                if (validationStatus == ValidationStatus.VALID) {
-                    LOG.debug("Enabling {} proceeding after performing 
validation", serviceNode);
-                } else {
-                    final Collection<ValidationResult> errors = 
validationState.getValidationErrors();
-                    if (completeExceptionallyOnFailure) {
-                        future.completeExceptionally(new 
IllegalStateException("Enabling %s failed: Validation Status [%s] Errors 
%s".formatted(serviceNode, validationStatus, errors)));
+                try {
+                    // Perform validation - if a ConfigurationContext was 
provided, validate against its properties
+                    final ValidationState validationState;
+                    if (providedConfigurationContext == null) {
+                        performValidation();
+                        validationState = getValidationState();
+                    } else {
+                        final Map<String, String> properties = 
providedConfigurationContext.getAllProperties();
+                        final ValidationContext validationContext = 
createValidationContext(properties, getAnnotationData(), getParameterLookup(), 
true);
+                        validationState = performValidation(validationContext);
                     }
 
-                    final long selectedValidationDelay = 
getDelay(validationDelay, INCREMENTAL_VALIDATION_DELAY_MS);
+                    final ValidationStatus validationStatus = 
validationState.getStatus();
+                    if (validationStatus == ValidationStatus.VALID) {
+                        LOG.debug("Enabling {} proceeding after performing 
validation", serviceNode);
+                    } else {
+                        final Collection<ValidationResult> errors = 
validationState.getValidationErrors();
+                        if (completeExceptionallyOnFailure) {
+                            future.completeExceptionally(new 
IllegalStateException("Enabling %s failed: Validation Status [%s] Errors 
%s".formatted(serviceNode, validationStatus, errors)));
+                        }
 
-                    // Log warning on repeated validation rescheduling
-                    if (selectedValidationDelay > MAXIMUM_DELAY.toMillis()) {
-                        LOG.warn("Validation rescheduled in {} ms for {} 
Errors {}", selectedValidationDelay, serviceNode, errors);
-                    }
+                        final long selectedValidationDelay = 
getDelay(validationDelay, INCREMENTAL_VALIDATION_DELAY_MS);
 
-                    try {
-                        scheduler.schedule(this, selectedValidationDelay, 
TimeUnit.MILLISECONDS);
-                        LOG.debug("Validation rescheduled in {} ms for {}", 
selectedValidationDelay, serviceNode);
-                    } catch (final RejectedExecutionException e) {
-                        LOG.debug("Validation rescheduling rejected for {}", 
serviceNode, e);
-                        future.completeExceptionally(new 
IllegalStateException("Enabling %s rejected: Validation Status [%s] Errors 
%s".formatted(serviceNode, validationStatus, errors)));
+                        // Log warning on repeated validation rescheduling
+                        if (selectedValidationDelay > 
MAXIMUM_DELAY.toMillis()) {
+                            LOG.warn("Validation rescheduled in {} ms for {} 
Errors {}", selectedValidationDelay, serviceNode, errors);
+                        }
+
+                        try {
+                            if (scheduleEnableTask(scheduler, this, 
selectedValidationDelay, future)) {
+                                LOG.debug("Validation rescheduled in {} ms for 
{}", selectedValidationDelay, serviceNode);
+                            }
+                        } catch (final RejectedExecutionException e) {
+                            LOG.debug("Validation rescheduling rejected for 
{}", serviceNode, e);
+                            future.completeExceptionally(new 
IllegalStateException(
+                                "Enabling %s rejected: Validation Status [%s] 
Errors %s".formatted(serviceNode, validationStatus, errors)));
+                        }
+
+                        return;
                     }
 
-                    // Enable command rescheduled or rejected
-                    return;
-                }
+                    synchronized (active) {
+                        if (!active.get() || 
!stateTransition.isEnabling(future)) {
+                            return;
+                        }
+                    }
 
-                final ControllerService controllerService = 
getControllerServiceImplementation();
-                try {
+                    final ControllerService controllerService = 
getControllerServiceImplementation();
                     try (final NarCloseable ignored = 
NarCloseable.withComponentNarLoader(getExtensionManager(), 
controllerService.getClass(), getIdentifier())) {
                         
ReflectionUtils.invokeMethodsWithAnnotation(OnEnabled.class, controllerService, 
configContext);
                     }
 
                     boolean shouldEnable;
                     synchronized (active) {
-                        shouldEnable = active.get() && 
stateTransition.enable(getReferences()); // Transitioning the state to ENABLED 
will complete our future.
+                        shouldEnable = active.get() && 
stateTransition.enable(getReferences(), future);
                     }
 
-                    if (!shouldEnable) {
-                        LOG.info("Disabling {} after enabled due to disable 
action initiated", serviceNode);
-                        // Can only happen if user initiated DISABLE operation 
before service finished enabling. It's state will be
-                        // set to DISABLING (see disable() operation)
-                        invokeDisable(configContext);
-                        stateTransition.disable();
-                        future.complete(null);
-                    } else {
+                    if (shouldEnable) {
+                        enabled = true;
                         LOG.info("Enabled {}", serviceNode);
+                    } else {
+                        LOG.info("Disabling {} after enable lifecycle 
completed while a disable action was in progress", serviceNode);
+                        invokeDisable(configContext);
                     }
                 } catch (final Exception e) {
                     if (completeExceptionallyOnFailure) {
                         future.completeExceptionally(e);
                     }
 
+                    final ControllerService controllerService = 
getControllerServiceImplementation();
                     final Throwable cause = e instanceof 
InvocationTargetException ? e.getCause() : e;
                     final ComponentLog componentLog = new 
StandardComponentLog(getIdentifier(), controllerService, new 
StandardLoggingContext(serviceNode));
                     componentLog.error("Failed to invoke @OnEnabled method", 
cause);
+
                     invokeDisable(configContext);
 
-                    if (isActive()) {
+                    if (isActive() && stateTransition.isEnabling(future)) {
                         // Increment enabling delay to avoid excessive retries
                         final long selectedEnablingDelay = 
getDelay(enablingDelay, administrativeYieldMillis);
-                        scheduler.schedule(this, selectedEnablingDelay, 
TimeUnit.MILLISECONDS);
-                    } else {
-                        stateTransition.disable();
+                        scheduleEnableTask(scheduler, this, 
selectedEnablingDelay, future);
+                    }
+                } finally {
+                    final boolean completeDisable;
+                    synchronized (active) {
+                        enablingTaskRunning.set(false);
+                        completeDisable = !enabled && !active.get() && 
stateTransition.getState() == ControllerServiceState.DISABLING;
+                    }
+
+                    if (completeDisable) {
+                        completeDisabling();
                     }
                 }
             }
-        });
+        };
 
+        scheduleEnableTask(scheduler, enablingRunnable, 0, future);
         return future;
     }
 
+    private boolean scheduleEnableTask(final ScheduledExecutorService 
scheduler, final Runnable task, final long delay,
+            final CompletableFuture<?> enabledFuture) {
+        synchronized (active) {
+            if (!active.get() || !stateTransition.isEnabling(enabledFuture)) {
+                return false;
+            }
+
+            enablingTask.set(scheduler.schedule(task, delay, 
TimeUnit.MILLISECONDS));
+            return true;
+        }
+    }
+
     /**
-     * Will atomically disable this service by invoking its @OnDisabled 
operation.
-     * It uses CAS operation on {@link #stateTransition} to transition this 
service
-     * from ENABLED to DISABLING state. If such transition succeeds the service
-     * will be de-activated (see {@link ControllerServiceNode#isActive()}).
-     * If such transition doesn't succeed (the service is still in ENABLING 
state)
-     * then the service will still be transitioned to DISABLING state to 
ensure that
-     * no other transition could happen on this service. However in such event
-     * (e.g., its @OnEnabled finally succeeded), the {@link 
#enable(ScheduledExecutorService, long, boolean)}
-     * operation will initiate service disabling javadoc for (see {@link 
#enable(ScheduledExecutorService, long, boolean)}
-     * <br>
-     * Upon successful invocation of @OnDisabled this service will be 
transitioned to
-     * DISABLED state.
+     * Atomically transitions this service to DISABLING. Services that are 
ENABLING complete pending enable futures exceptionally
+     * with a {@link CancellationException} and request interruption of the 
active enable task.
+     * Services that are ENABLED invoke their @OnDisabled methods before 
transitioning to DISABLED.
+     * All callers receive a future that completes when the service is 
DISABLED.
      */
     @Override
     public CompletableFuture<Void> disable(final ScheduledExecutorService 
scheduler) {
@@ -777,47 +805,36 @@ public class StandardControllerServiceNode extends 
AbstractComponentNode impleme
          * service since it will attempt to transition service state from
          * ENABLING to ENABLED but only if it's active.
          */
-        synchronized (this.active) {
-            this.active.set(false);
-        }
-
         final CompletableFuture<Void> future = new CompletableFuture<>();
-        // If already disabled, complete immediately
-        if (getState() == ControllerServiceState.DISABLED) {
-            future.complete(null);
-            return future;
+        final ControllerServiceState previousState;
+        final Future<?> task;
+        final boolean taskRunning;
+        synchronized (active) {
+            active.set(false);
+            previousState = stateTransition.transitionToDisabling(future);
+            task = enablingTask.getAndSet(null);
+            taskRunning = enablingTaskRunning.get();
         }
 
-        final boolean transitioned = 
this.stateTransition.transitionToDisabling(ControllerServiceState.ENABLING, 
future);
-        if (transitioned) {
-            // If we transitioned from ENABLING to DISABLING, we need to 
immediately complete the disable
-            // because the enable task may be scheduled to run far in the 
future (up to 10 minutes) due to
-            // validation retries. Rather than making the user wait, we 
immediately transition to DISABLED.
-            scheduler.execute(() -> {
-                stateTransition.disable();
+        if (previousState == ControllerServiceState.ENABLING) {
+            if (task != null) {
+                task.cancel(true);
+            }
+
+            if (!taskRunning) {
+                scheduler.execute(this::completeDisabling);
+            }
 
-                // Now all components that reference this service will be 
invalid. Trigger validation to occur so that
-                // this is reflected in any response that may go back to a 
user/client.
-                for (final ComponentNode component : 
getReferences().getReferencingComponents()) {
-                    component.performValidation();
-                }
-            });
             return future;
         }
 
-        if 
(this.stateTransition.transitionToDisabling(ControllerServiceState.ENABLED, 
future)) {
+        if (previousState == ControllerServiceState.ENABLED) {
             final ConfigurationContext configContext = new 
StandardConfigurationContext(this, this.serviceProvider, null);
             scheduler.execute(() -> {
                 try {
                     invokeDisable(configContext);
                 } finally {
-                    stateTransition.disable();
-
-                    // Now all components that reference this service will be 
invalid. Trigger validation to occur so that
-                    // this is reflected in any response that may go back to a 
user/client.
-                    for (final ComponentNode component : 
getReferences().getReferencingComponents()) {
-                        component.performValidation();
-                    }
+                    completeDisabling();
                 }
             });
         }
@@ -825,12 +842,22 @@ public class StandardControllerServiceNode extends 
AbstractComponentNode impleme
         return future;
     }
 
-    private void invokeDisable(ConfigurationContext configContext) {
+    private void completeDisabling() {
+        if (!stateTransition.disable()) {
+            return;
+        }
+
+        for (final ComponentNode component : 
getReferences().getReferencingComponents()) {
+            component.performValidation();
+        }
+    }
+
+    private void invokeDisable(final ConfigurationContext configContext) {
         final ControllerService controllerService = 
getControllerServiceImplementation();
         try (final NarCloseable ignored = 
NarCloseable.withComponentNarLoader(getExtensionManager(), 
controllerService.getClass(), getIdentifier())) {
             ReflectionUtils.invokeMethodsWithAnnotation(OnDisabled.class, 
controllerService, configContext);
             LOG.debug("Successfully disabled {}", this);
-        } catch (Exception e) {
+        } catch (final Exception e) {
             final Throwable cause = e instanceof InvocationTargetException ? 
e.getCause() : e;
             final ComponentLog componentLog = new 
StandardComponentLog(getIdentifier(), controllerService, new 
StandardLoggingContext(StandardControllerServiceNode.this));
             componentLog.error("Failed to invoke @OnDisabled method due to 
{}", cause);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
index 5d3f7c582dc..d5d836dfc64 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
@@ -90,6 +90,7 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -127,6 +128,8 @@ public class StandardConnectorNode implements 
ConnectorNode, GroupedComponent {
     private volatile String name;
     private volatile FrameworkConnectorInitializationContext 
initializationContext;
 
+    // Serializes Connector start and stop invocations so that a stop request 
cannot complete before an in-flight start invocation returns.
+    private final ReentrantLock componentLifecycleLock = new ReentrantLock();
     private final Object loggingAttributesLock = new Object();
     private volatile Map<String, String> customLoggingAttributes = Map.of();
     private volatile Map<String, String> mergedLoggingAttributes = Map.of();
@@ -861,8 +864,8 @@ public class StandardConnectorNode implements 
ConnectorNode, GroupedComponent {
 
             // Check current state for existing start request in progress
             if (stateUpdated && currentState == ConnectorState.STARTING) {
-                logger.info("{} is currently starting so will not stop the 
Connector until the start has completed", this);
                 stateTransition.addPendingStopFuture(stopCompleteFuture);
+                stopManagedProcessGroup(scheduler);
                 return stopCompleteFuture;
             }
         }
@@ -872,6 +875,33 @@ public class StandardConnectorNode implements 
ConnectorNode, GroupedComponent {
         return stopCompleteFuture;
     }
 
+    private void stopManagedProcessGroup(final FlowEngine scheduler) {
+        if (getCurrentState() != ConnectorState.STOPPING) {
+            return;
+        }
+
+        logger.info("Stopping the managed Process Group for {} so that its 
startup can finish and the Connector can stop", this);
+        final CompletableFuture<Void> processGroupStopFuture;
+        try {
+            processGroupStopFuture = 
activeFlowContext.getRootGroup().getLifecycle().stop();
+        } catch (final Exception e) {
+            logger.warn("Failed to stop the managed Process Group for {}. The 
Connector cannot finish stopping until its components stop, so this will be 
tried again in 10 seconds", this, e);
+            scheduler.schedule(() -> stopManagedProcessGroup(scheduler), 10, 
TimeUnit.SECONDS);
+            return;
+        }
+
+        processGroupStopFuture.whenComplete((result, failure) -> {
+            if (failure != null) {
+                logger.warn("Failed to stop the managed Process Group for {}. 
The Connector cannot finish stopping until its components stop, so this will be 
tried again in 10 seconds",
+                    this, failure);
+                scheduler.schedule(() -> stopManagedProcessGroup(scheduler), 
10, TimeUnit.SECONDS);
+                return;
+            }
+
+            scheduler.schedule(() -> completeDeferredStop(scheduler), 0, 
TimeUnit.SECONDS);
+        });
+    }
+
     @Override
     public Future<Void> drainFlowFiles() {
         logger.debug("Draining FlowFiles for {}", this);
@@ -990,49 +1020,64 @@ public class StandardConnectorNode implements 
ConnectorNode, GroupedComponent {
     }
 
     private void stopComponent(final FlowEngine scheduler, final 
CompletableFuture<Void> stopCompleteFuture) {
-        logger.debug("Stopping component for {}", this);
-        try (final NarCloseable ignored = 
NarCloseable.withComponentNarLoader(extensionManager, 
connectorDetails.getConnector().getClass(), getIdentifier())) {
-            connectorDetails.getConnector().stop(activeFlowContext);
-        } catch (final Exception e) {
-            logger.error("Failed to stop {}. Will try again in 10 seconds", 
this, e);
-            scheduler.schedule(() -> stopComponent(scheduler, 
stopCompleteFuture), 10, TimeUnit.SECONDS);
-            return;
-        }
+        componentLifecycleLock.lock();
+        try {
+            if (getCurrentState() != ConnectorState.STOPPING) {
+                return;
+            }
 
-        stateTransition.setCurrentState(ConnectorState.STOPPED);
-        stopCompleteFuture.complete(null);
-        logger.info("Successfully stopped {}", this);
+            logger.debug("Stopping component for {}", this);
+            try (final NarCloseable ignored = 
NarCloseable.withComponentNarLoader(extensionManager, 
connectorDetails.getConnector().getClass(), getIdentifier())) {
+                connectorDetails.getConnector().stop(activeFlowContext);
+            } catch (final Exception e) {
+                logger.error("Failed to stop {}. Will try again in 10 
seconds", this, e);
+                scheduler.schedule(() -> stopComponent(scheduler, 
stopCompleteFuture), 10, TimeUnit.SECONDS);
+                return;
+            }
 
-        final ConnectorState desiredState = getDesiredState();
-        if (desiredState == ConnectorState.RUNNING) {
-            logger.info("{} was requested to be RUNNING while it was stopping 
so will attempt to start again", this);
-            start(scheduler, new CompletableFuture<>());
+            stateTransition.setCurrentState(ConnectorState.STOPPED);
+            stopCompleteFuture.complete(null);
+            logger.info("Successfully stopped {}", this);
+
+            final ConnectorState desiredState = getDesiredState();
+            if (desiredState == ConnectorState.RUNNING) {
+                logger.info("{} was requested to be RUNNING while it was 
stopping so will attempt to start again", this);
+                start(scheduler, new CompletableFuture<>());
+            }
+        } finally {
+            componentLifecycleLock.unlock();
         }
     }
 
     private void startComponent(final FlowEngine scheduler, final 
CompletableFuture<Void> startCompleteFuture) {
-        logger.debug("Starting component for {}", this);
-        final ConnectorState desiredState = getDesiredState();
-        if (desiredState != ConnectorState.RUNNING) {
-            logger.info("Will not start {} because the desired state is no 
longer RUNNING but is now {}", this, desiredState);
-            completeDeferredStop(scheduler);
-            return;
-        }
-
-        try (final NarCloseable ignored = 
NarCloseable.withComponentNarLoader(extensionManager, 
connectorDetails.getConnector().getClass(), getIdentifier())) {
-            connectorDetails.getConnector().start(activeFlowContext);
-        } catch (final Exception e) {
-            if (getCurrentState() == ConnectorState.STOPPING) {
-                logger.error("Failed to start {} and a stop has since been 
requested, so the Connector will be stopped instead of started", this, e);
+        componentLifecycleLock.lock();
+        try {
+            logger.debug("Starting component for {}", this);
+            final ConnectorState desiredState = getDesiredState();
+            if (desiredState != ConnectorState.RUNNING) {
+                logger.info("Will not start {} because the desired state is no 
longer RUNNING but is now {}", this, desiredState);
                 completeDeferredStop(scheduler);
-            } else {
-                logger.error("Failed to start {} retrying in 10 seconds", 
this, e);
-                scheduler.schedule(() -> startComponent(scheduler, 
startCompleteFuture), 10, TimeUnit.SECONDS);
+                return;
             }
 
-            return;
+            try (final NarCloseable ignored = 
NarCloseable.withComponentNarLoader(extensionManager, 
connectorDetails.getConnector().getClass(), getIdentifier())) {
+                connectorDetails.getConnector().start(activeFlowContext);
+            } catch (final Exception e) {
+                if (getCurrentState() == ConnectorState.STOPPING) {
+                    logger.error("Failed to start {} and a stop has since been 
requested, so the Connector will be stopped instead of started", this, e);
+                    completeDeferredStop(scheduler);
+                } else {
+                    logger.error("Failed to start {} retrying in 10 seconds", 
this, e);
+                    scheduler.schedule(() -> startComponent(scheduler, 
startCompleteFuture), 10, TimeUnit.SECONDS);
+                }
+
+                return;
+            }
+        } finally {
+            componentLifecycleLock.unlock();
         }
 
+        // Reconcile the state after releasing the lock so a completed disable 
operation can acquire it and finish a pending stop.
         // A stop requested while the Connector was starting transitioned the 
current state away from STARTING, so the
         // Connector may be reported as RUNNING only if it is still STARTING. 
Otherwise, this thread owns the stop that
         // was deferred while the start was in flight.
@@ -1048,12 +1093,21 @@ public class StandardConnectorNode implements 
ConnectorNode, GroupedComponent {
     }
 
     private void completeDeferredStop(final FlowEngine scheduler) {
-        if (getCurrentState() == ConnectorState.STOPPING) {
-            logger.info("{} was requested to stop while it was starting so 
will now be stopped", this);
-            stopComponent(scheduler, new CompletableFuture<>());
+        if (!componentLifecycleLock.tryLock()) {
+            logger.debug("{} has not finished its start invocation, so the 
Connector stop will be checked again", this);
+            scheduler.schedule(() -> completeDeferredStop(scheduler), 100, 
TimeUnit.MILLISECONDS);
+            return;
         }
-    }
 
+        try {
+            if (getCurrentState() == ConnectorState.STOPPING) {
+                logger.info("{} was requested to stop while it was starting so 
will now be stopped", this);
+                stopComponent(scheduler, new CompletableFuture<>());
+            }
+        } finally {
+            componentLifecycleLock.unlock();
+        }
+    }
 
     @Override
     public void verifyCanDelete() {
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/facades/standalone/StandaloneProcessGroupLifecycle.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/facades/standalone/StandaloneProcessGroupLifecycle.java
index 3ee3d4a641e..fcc078f3c74 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/facades/standalone/StandaloneProcessGroupLifecycle.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/facades/standalone/StandaloneProcessGroupLifecycle.java
@@ -425,7 +425,7 @@ public class StandaloneProcessGroupLifecycle implements 
ProcessGroupLifecycle {
         final Collection<ProcessorNode> processors = recursive ? 
processGroup.findAllProcessors() : processGroup.getProcessors();
         final List<CompletableFuture<Void>> stopFutures = new ArrayList<>();
         for (final ProcessorNode processor : processors) {
-            final ScheduledState processorState = 
processor.getScheduledState();
+            final ScheduledState processorState = 
processor.getPhysicalScheduledState();
             if (processorState == ScheduledState.DISABLED || processorState == 
ScheduledState.STOPPED) {
                 logger.debug("Not stopping Processor {} because its state is 
{}", processor, processorState);
                 continue;
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/StandardProcessScheduler.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/StandardProcessScheduler.java
index 58fd1e87b3d..6f8744d728e 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/StandardProcessScheduler.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/StandardProcessScheduler.java
@@ -71,6 +71,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
@@ -104,6 +105,7 @@ public final class StandardProcessScheduler implements 
ProcessScheduler {
     private final ReloadComponent reloadComponent;
 
     private final ConcurrentMap<SchedulingStrategy, SchedulingAgent> 
strategyAgentMap = new ConcurrentHashMap<>();
+    private final ConcurrentMap<String, CompletableFuture<Void>> 
processorStartFutures = new ConcurrentHashMap<>();
 
     // thread pool for starting/stopping components
     private volatile boolean shutdown = false;
@@ -424,6 +426,7 @@ public final class StandardProcessScheduler implements 
ProcessScheduler {
                     getSchedulingAgent(procNode).schedule(procNode, 
lifecycleState);
                 }
 
+                processorStartFutures.remove(procNode.getIdentifier(), future);
                 future.complete(null);
             }
 
@@ -443,6 +446,7 @@ public final class StandardProcessScheduler implements 
ProcessScheduler {
 
         procNode.reloadAdditionalResourcesIfNecessary();
 
+        processorStartFutures.put(procNode.getIdentifier(), future);
         procNode.start(componentMonitoringThreadPool, 
administrativeYieldMillis, processorStartTimeoutMillis, processContextFactory, 
callback, failIfStopping, scheduleActions);
         return future;
     }
@@ -595,7 +599,13 @@ public final class StandardProcessScheduler implements 
ProcessScheduler {
             getStateManager(procNode), lifecycleState::isTerminated, 
nodeTypeProvider);
 
         LOG.info("Stopping {}", procNode);
-        return procNode.stop(this, this.componentLifeCycleThreadPool, 
processContext, getSchedulingAgent(procNode), lifecycleState, lifecycleMethods);
+        final CompletableFuture<Void> stopFuture = procNode.stop(this, 
this.componentLifeCycleThreadPool, processContext, 
getSchedulingAgent(procNode), lifecycleState, lifecycleMethods);
+        final CompletableFuture<Void> startFuture = 
processorStartFutures.remove(procNode.getIdentifier());
+        if (startFuture != null) {
+            startFuture.completeExceptionally(new 
CancellationException("Processor start cancelled by stop request"));
+        }
+
+        return stopFuture;
     }
 
     @Override
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/ControllerServiceEnablingConnector.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/ControllerServiceEnablingConnector.java
new file mode 100644
index 00000000000..5bd166a5a9a
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/ControllerServiceEnablingConnector.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.components.connector;
+
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.connector.components.FlowContext;
+import org.apache.nifi.components.connector.processors.CreateDummyFlowFile;
+import 
org.apache.nifi.components.connector.services.impl.BlockingEnablingCounterService;
+import 
org.apache.nifi.components.connector.services.impl.FailingEnablingCounterService;
+import org.apache.nifi.components.connector.util.VersionedFlowUtils;
+import org.apache.nifi.flow.Bundle;
+import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flow.VersionedExternalFlow;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+
+import java.util.List;
+import java.util.Map;
+
+public class ControllerServiceEnablingConnector extends AbstractConnector {
+
+    static final String BLOCKING_ENABLING = "BLOCKING_ENABLING";
+    static final String FAILING_ENABLING = "FAILING_ENABLING";
+    static final String CONFIGURATION_STEP_NAME = "Configuration";
+
+    static final ConnectorPropertyDescriptor ENABLING_BEHAVIOR = new 
ConnectorPropertyDescriptor.Builder()
+        .name("Enabling Behavior")
+        .description("How the managed Controller Service should behave while 
enabling")
+        .type(PropertyType.STRING)
+        .required(true)
+        .defaultValue(BLOCKING_ENABLING)
+        .allowableValues(BLOCKING_ENABLING, FAILING_ENABLING)
+        .build();
+
+    private static final ConnectorPropertyGroup PROPERTY_GROUP = new 
ConnectorPropertyGroup.Builder()
+        .name("Controller Service Settings")
+        .addProperty(ENABLING_BEHAVIOR)
+        .build();
+
+    private static final ConfigurationStep CONFIGURATION_STEP = new 
ConfigurationStep.Builder()
+        .name(CONFIGURATION_STEP_NAME)
+        .propertyGroups(List.of(PROPERTY_GROUP))
+        .build();
+
+    @Override
+    public List<ConfigurationStep> getConfigurationSteps() {
+        return List.of(CONFIGURATION_STEP);
+    }
+
+    @Override
+    public VersionedExternalFlow getInitialFlow() {
+        return 
VersionedFlowUtils.loadFlowFromResource("flows/generate-duplicate-log-flow.json");
+    }
+
+    @Override
+    public VersionedExternalFlow getActiveFlow(final FlowContext 
activeFlowContext) {
+        final String enablingBehavior = 
activeFlowContext.getConfigurationContext().getProperty(CONFIGURATION_STEP, 
ENABLING_BEHAVIOR).getValue();
+        return buildFlow(enablingBehavior);
+    }
+
+    @Override
+    public void applyUpdate(final FlowContext workingContext, final 
FlowContext activeContext) throws FlowUpdateException {
+        getInitializationContext().updateFlow(activeContext, 
getActiveFlow(workingContext));
+    }
+
+    @Override
+    public void onStepConfigured(final String stepName, final FlowContext 
workingContext) throws FlowUpdateException {
+        getInitializationContext().updateFlow(workingContext, 
getActiveFlow(workingContext));
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verifyConfigurationStep(final String 
stepName, final Map<String, String> overrides, final FlowContext flowContext) {
+        return List.of();
+    }
+
+    private VersionedExternalFlow buildFlow(final String enablingBehavior) {
+        final VersionedExternalFlow externalFlow = 
VersionedFlowUtils.loadFlowFromResource("flows/generate-duplicate-log-flow.json");
+        final VersionedProcessGroup rootGroup = externalFlow.getFlowContents();
+
+        final Bundle systemBundle = new Bundle();
+        systemBundle.setArtifact("system");
+        systemBundle.setGroup("default");
+        systemBundle.setVersion("unversioned");
+
+        final String serviceType = BLOCKING_ENABLING.equals(enablingBehavior)
+            ? BlockingEnablingCounterService.class.getName()
+            : FailingEnablingCounterService.class.getName();
+
+        final VersionedControllerService controllerService = 
VersionedFlowUtils.addControllerService(rootGroup, serviceType, systemBundle, 
"Managed Service");
+
+        final VersionedProcessor generateProcessor = 
VersionedFlowUtils.findProcessor(rootGroup,
+            processor -> 
processor.getType().equals(CreateDummyFlowFile.class.getName())).orElseThrow();
+
+        generateProcessor.getProperties().put("Counter Service", 
controllerService.getIdentifier());
+
+        return externalFlow;
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/ProcessorStartFailureConnector.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/ProcessorStartFailureConnector.java
new file mode 100644
index 00000000000..85e72e70b2f
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/ProcessorStartFailureConnector.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.components.connector;
+
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.connector.components.FlowContext;
+import org.apache.nifi.components.connector.processors.CreateDummyFlowFile;
+import 
org.apache.nifi.components.connector.services.impl.StandardCounterService;
+import org.apache.nifi.components.connector.util.VersionedFlowUtils;
+import 
org.apache.nifi.controller.scheduling.processors.FailOnScheduledProcessor;
+import org.apache.nifi.flow.Bundle;
+import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flow.VersionedExternalFlow;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+
+import java.util.List;
+import java.util.Map;
+
+public class ProcessorStartFailureConnector extends AbstractConnector {
+
+    @Override
+    public List<ConfigurationStep> getConfigurationSteps() {
+        return List.of();
+    }
+
+    @Override
+    public VersionedExternalFlow getInitialFlow() {
+        return buildFlow();
+    }
+
+    @Override
+    public VersionedExternalFlow getActiveFlow(final FlowContext 
activeFlowContext) {
+        return buildFlow();
+    }
+
+    @Override
+    public void applyUpdate(final FlowContext workingContext, final 
FlowContext activeContext) throws FlowUpdateException {
+        getInitializationContext().updateFlow(activeContext, 
getActiveFlow(workingContext));
+    }
+
+    @Override
+    public void onStepConfigured(final String stepName, final FlowContext 
workingContext) throws FlowUpdateException {
+        getInitializationContext().updateFlow(workingContext, 
getActiveFlow(workingContext));
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verifyConfigurationStep(final String 
stepName, final Map<String, String> overrides, final FlowContext flowContext) {
+        return List.of();
+    }
+
+    private VersionedExternalFlow buildFlow() {
+        final VersionedExternalFlow externalFlow = 
VersionedFlowUtils.loadFlowFromResource("flows/generate-duplicate-log-flow.json");
+        final VersionedProcessGroup rootGroup = externalFlow.getFlowContents();
+
+        final Bundle systemBundle = new Bundle();
+        systemBundle.setArtifact("system");
+        systemBundle.setGroup("default");
+        systemBundle.setVersion("unversioned");
+
+        final VersionedControllerService controllerService = 
VersionedFlowUtils.addControllerService(
+            rootGroup, StandardCounterService.class.getName(), systemBundle, 
"Managed Service");
+        final VersionedProcessor processor = 
VersionedFlowUtils.findProcessor(rootGroup,
+            candidate -> 
candidate.getType().equals(CreateDummyFlowFile.class.getName())).orElseThrow();
+        processor.setType(FailOnScheduledProcessor.class.getName());
+        processor.setName("Start Failure Processor");
+        processor.getProperties().clear();
+        
processor.getProperties().put(FailOnScheduledProcessor.MANAGED_SERVICE.getName(),
 controllerService.getIdentifier());
+        processor.getPropertyDescriptors().clear();
+
+        for (final VersionedProcessGroup childGroup : 
rootGroup.getProcessGroups()) {
+            if 
(childGroup.getIdentifier().equals(processor.getGroupIdentifier())) {
+                childGroup.getConnections().removeIf(connection -> 
connection.getSource().getId().equals(processor.getIdentifier()));
+                break;
+            }
+        }
+
+        return externalFlow;
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/StandardConnectorNodeIT.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/StandardConnectorNodeIT.java
index 53ff90dab7c..7b28c75b011 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/StandardConnectorNodeIT.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/StandardConnectorNodeIT.java
@@ -28,6 +28,8 @@ import 
org.apache.nifi.components.connector.processors.TerminateFlowFile;
 import 
org.apache.nifi.components.connector.secrets.ParameterProviderSecretsManager;
 import org.apache.nifi.components.connector.secrets.SecretsManager;
 import org.apache.nifi.components.connector.services.CounterService;
+import 
org.apache.nifi.components.connector.services.impl.BlockingEnablingCounterService;
+import 
org.apache.nifi.components.connector.services.impl.FailingEnablingCounterService;
 import org.apache.nifi.components.state.StateManagerProvider;
 import 
org.apache.nifi.components.validation.StandardVerifiableComponentFactory;
 import org.apache.nifi.components.validation.ValidationState;
@@ -42,6 +44,7 @@ import org.apache.nifi.controller.MockStateManagerProvider;
 import org.apache.nifi.controller.NodeTypeProvider;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.ReloadComponent;
+import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.flow.StandardFlowManager;
 import org.apache.nifi.controller.flowanalysis.FlowAnalyzer;
 import org.apache.nifi.controller.queue.DropFlowFileRequest;
@@ -60,8 +63,11 @@ import 
org.apache.nifi.controller.scheduling.RepositoryContextFactory;
 import org.apache.nifi.controller.scheduling.SchedulingAgent;
 import org.apache.nifi.controller.scheduling.StandardLifecycleStateManager;
 import org.apache.nifi.controller.scheduling.StandardProcessScheduler;
+import 
org.apache.nifi.controller.scheduling.processors.FailOnScheduledProcessor;
 import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.controller.service.StandardControllerServiceProvider;
 import org.apache.nifi.engine.FlowEngine;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.mock.MockNodeTypeProvider;
@@ -93,11 +99,11 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.function.IntSupplier;
 import java.util.function.Predicate;
 
 import static java.util.Objects.requireNonNull;
@@ -121,11 +127,10 @@ public class StandardConnectorNodeIT {
     private StandardFlowManager flowManager;
     private FlowEngine componentLifecycleThreadPool;
     private ConnectorRepository connectorRepository;
+    private ControllerServiceProvider controllerServiceProvider;
 
     @BeforeEach
     public void setup() {
-        final ControllerServiceProvider controllerServiceProvider = 
mock(ControllerServiceProvider.class);
-        
when(controllerServiceProvider.disableControllerServicesAsync(anyCollection())).thenReturn(CompletableFuture.completedFuture(null));
         connectorRepository = new StandardConnectorRepository();
 
         final SecretsManager secretsManager = new 
ParameterProviderSecretsManager();
@@ -139,7 +144,10 @@ public class StandardConnectorNodeIT {
         final LifecycleStateManager lifecycleStateManager = new 
StandardLifecycleStateManager();
         final ReloadComponent reloadComponent = mock(ReloadComponent.class);
 
-        final NiFiProperties nifiProperties = 
NiFiProperties.createBasicNiFiProperties("src/test/resources/conf/nifi.properties");
+        final NiFiProperties nifiProperties = 
NiFiProperties.createBasicNiFiProperties("src/test/resources/conf/nifi.properties",
 Map.of(
+            NiFiProperties.ADMINISTRATIVE_YIELD_DURATION, "10 millis",
+            NiFiProperties.PROCESSOR_SCHEDULING_TIMEOUT, "30 secs"
+        ));
 
         final FlowController flowController = mock(FlowController.class);
         when(flowController.isInitialized()).thenReturn(true);
@@ -158,7 +166,6 @@ public class StandardConnectorNodeIT {
 
         
when(flowController.getRepositoryContextFactory()).thenReturn(repoContextFactory);
         
when(flowController.getGarbageCollectionLog()).thenReturn(mock(GarbageCollectionLog.class));
-        
when(flowController.getControllerServiceProvider()).thenReturn(controllerServiceProvider);
         when(flowController.getProvenanceRepository()).thenReturn(provRepo);
         
when(flowController.getBulletinRepository()).thenReturn(bulletinRepository);
         
when(flowController.getLifecycleStateManager()).thenReturn(lifecycleStateManager);
@@ -186,6 +193,8 @@ public class StandardConnectorNodeIT {
         extensionManager.discoverExtensions(systemBundle, Set.of());
 
         flowManager = new StandardFlowManager(nifiProperties, null, 
flowController, flowFileEventRepository, parameterContextManager);
+        controllerServiceProvider = new 
StandardControllerServiceProvider(processScheduler, bulletinRepository, 
flowManager, extensionManager);
+        
when(flowController.getControllerServiceProvider()).thenReturn(controllerServiceProvider);
         flowManager.initialize(controllerServiceProvider, 
mock(PythonBridge.class), mock(FlowAnalyzer.class), 
mock(RuleViolationsManager.class));
         final ProcessGroup rootGroup = flowManager.createProcessGroup("root");
         rootGroup.setName("Root");
@@ -397,6 +406,234 @@ public class StandardConnectorNodeIT {
         assertInstanceOf(CounterService.class, 
serviceNodes.iterator().next().getControllerServiceImplementation());
     }
 
+    @Test
+    public void testStopConnectorWhileManagedServiceBlocksInOnEnabled() throws 
Exception {
+        final ConnectorNode connectorNode = 
initializeControllerServiceEnablingConnector(ControllerServiceEnablingConnector.BLOCKING_ENABLING);
+        final ControllerServiceNode serviceNode = 
getManagedControllerService(connectorNode);
+        final BlockingEnablingCounterService service = 
(BlockingEnablingCounterService) 
serviceNode.getControllerServiceImplementation();
+        service.setBlockDisable(true);
+
+        connectorNode.start(componentLifecycleThreadPool);
+        try {
+            waitForServiceState(serviceNode, ControllerServiceState.ENABLING);
+            waitForEnableInvocation(service::enableInvocationCount, 1);
+
+            final Future<Void> stopFuture = 
connectorNode.stop(componentLifecycleThreadPool);
+            assertTrue(service.awaitEnableInterrupted(5, TimeUnit.SECONDS));
+            assertTrue(service.awaitDisableStarted(5, TimeUnit.SECONDS));
+            assertEquals(ControllerServiceState.DISABLING, 
serviceNode.getState());
+            assertFalse(stopFuture.isDone());
+            assertEquals(1, service.disableInvocationCount());
+
+            service.releaseDisable();
+            stopFuture.get(5, TimeUnit.SECONDS);
+            assertEquals(ConnectorState.STOPPED, 
connectorNode.getCurrentState());
+            assertEquals(ControllerServiceState.DISABLED, 
serviceNode.getState());
+        } finally {
+            service.releaseFirstEnable();
+            service.releaseDisable();
+        }
+    }
+
+    @Test
+    public void testStopConnectorWhileManagedServiceKeepsFailingOnEnabled() 
throws Exception {
+        final ConnectorNode connectorNode = 
initializeControllerServiceEnablingConnector(ControllerServiceEnablingConnector.FAILING_ENABLING);
+        final ControllerServiceNode serviceNode = 
getManagedControllerService(connectorNode);
+
+        connectorNode.start(componentLifecycleThreadPool);
+        final FailingEnablingCounterService service = 
(FailingEnablingCounterService) 
serviceNode.getControllerServiceImplementation();
+        waitForEnableInvocation(service::enableInvocationCount, 2);
+
+        final Future<Void> stopFuture = 
connectorNode.stop(componentLifecycleThreadPool);
+        stopFuture.get(5, TimeUnit.SECONDS);
+
+        assertEquals(ConnectorState.STOPPED, connectorNode.getCurrentState());
+        assertEquals(ControllerServiceState.DISABLED, serviceNode.getState());
+
+        final int enableInvocationCount = service.enableInvocationCount();
+        Thread.sleep(100L);
+        assertEquals(enableInvocationCount, service.enableInvocationCount());
+    }
+
+    @Test
+    public void testRestartWaitsForPreviousManagedServiceEnableAndDisable() 
throws Exception {
+        final ConnectorNode connectorNode = 
initializeControllerServiceEnablingConnector(ControllerServiceEnablingConnector.BLOCKING_ENABLING);
+        final ControllerServiceNode serviceNode = 
getManagedControllerService(connectorNode);
+        final BlockingEnablingCounterService service = 
(BlockingEnablingCounterService) 
serviceNode.getControllerServiceImplementation();
+        service.setIgnoreEnableInterrupt(true);
+        service.setBlockDisable(true);
+
+        connectorNode.start(componentLifecycleThreadPool);
+        try {
+            waitForServiceState(serviceNode, ControllerServiceState.ENABLING);
+            waitForEnableInvocation(service::enableInvocationCount, 1);
+
+            final Future<Void> stopFuture = 
connectorNode.stop(componentLifecycleThreadPool);
+            final Future<Void> restartFuture = 
connectorNode.start(componentLifecycleThreadPool);
+
+            assertTrue(service.awaitEnableInterrupted(5, TimeUnit.SECONDS));
+            waitForServiceState(serviceNode, ControllerServiceState.DISABLING);
+            assertFalse(stopFuture.isDone());
+            assertFalse(restartFuture.isDone());
+            assertEquals(1, service.enableInvocationCount());
+
+            service.releaseFirstEnable();
+            assertTrue(service.awaitDisableStarted(5, TimeUnit.SECONDS));
+            assertEquals(ControllerServiceState.DISABLING, 
serviceNode.getState());
+            assertFalse(stopFuture.isDone());
+            assertFalse(restartFuture.isDone());
+            assertEquals(1, service.enableInvocationCount());
+
+            service.releaseDisable();
+            stopFuture.get(5, TimeUnit.SECONDS);
+            restartFuture.get(5, TimeUnit.SECONDS);
+            waitForEnableInvocation(service::enableInvocationCount, 2);
+            waitForServiceState(serviceNode, ControllerServiceState.ENABLED);
+
+            final List<String> lifecycleEvents = service.getLifecycleEvents();
+            assertTrue(lifecycleEvents.indexOf("enable-finished-1") < 
lifecycleEvents.indexOf("disable-started-1"));
+            assertTrue(lifecycleEvents.indexOf("disable-finished-1") < 
lifecycleEvents.indexOf("enable-started-2"));
+        } finally {
+            service.releaseFirstEnable();
+            service.releaseDisable();
+        }
+    }
+
+    @Test
+    public void 
testStopConnectorWhileManagedProcessorKeepsFailingOnScheduled() throws 
Exception {
+        final ConnectorNode connectorNode = 
initializeProcessorStartFailureConnector();
+        final ProcessorNode processorNode = getManagedProcessor(connectorNode);
+        final FailOnScheduledProcessor processor = (FailOnScheduledProcessor) 
processorNode.getProcessor();
+        processor.setDesiredFailureCount(Integer.MAX_VALUE);
+
+        connectorNode.start(componentLifecycleThreadPool);
+        waitForEnableInvocation(processor::getOnScheduledInvocationCount, 2);
+
+        final Future<Void> stopFuture = 
connectorNode.stop(componentLifecycleThreadPool);
+        stopFuture.get(5, TimeUnit.SECONDS);
+
+        assertEquals(ConnectorState.STOPPED, connectorNode.getCurrentState());
+        assertEquals(ScheduledState.STOPPED, 
processorNode.getPhysicalScheduledState());
+        assertEquals(ControllerServiceState.DISABLED, 
getManagedControllerService(connectorNode).getState());
+
+        final int invocationCount = processor.getOnScheduledInvocationCount();
+        Thread.sleep(100L);
+        assertEquals(invocationCount, 
processor.getOnScheduledInvocationCount());
+    }
+
+    @Test
+    public void testStopConnectorInterruptsManagedProcessorOnScheduled() 
throws Exception {
+        final ConnectorNode connectorNode = 
initializeProcessorStartFailureConnector();
+        final ProcessorNode processorNode = getManagedProcessor(connectorNode);
+        final FailOnScheduledProcessor processor = (FailOnScheduledProcessor) 
processorNode.getProcessor();
+        processor.setDesiredFailureCount(0);
+        processor.setOnScheduledSleepDuration(20, TimeUnit.MINUTES, true, 1);
+
+        connectorNode.start(componentLifecycleThreadPool);
+        waitForEnableInvocation(processor::getOnScheduledInvocationCount, 1);
+
+        final Future<Void> stopFuture = 
connectorNode.stop(componentLifecycleThreadPool);
+        stopFuture.get(5, TimeUnit.SECONDS);
+
+        assertEquals(ConnectorState.STOPPED, connectorNode.getCurrentState());
+        assertEquals(ScheduledState.STOPPED, 
processorNode.getPhysicalScheduledState());
+        assertEquals(ControllerServiceState.DISABLED, 
getManagedControllerService(connectorNode).getState());
+        assertFalse(processor.isSucceeded());
+    }
+
+    @Test
+    public void 
testTerminateManagedProcessorWhoseOnScheduledIgnoresInterrupt() throws 
Exception {
+        final ConnectorNode connectorNode = 
initializeProcessorStartFailureConnector();
+        final ProcessorNode processorNode = getManagedProcessor(connectorNode);
+        final FailOnScheduledProcessor processor = (FailOnScheduledProcessor) 
processorNode.getProcessor();
+        processor.setDesiredFailureCount(0);
+        processor.setOnScheduledSleepDuration(20, TimeUnit.MINUTES, false, 1);
+
+        connectorNode.start(componentLifecycleThreadPool);
+        waitForEnableInvocation(processor::getOnScheduledInvocationCount, 1);
+
+        try {
+            final Future<Void> stopFuture = 
connectorNode.stop(componentLifecycleThreadPool);
+            waitForProcessorState(processorNode, ScheduledState.STOPPING);
+            Thread.sleep(100L);
+
+            assertFalse(stopFuture.isDone());
+            assertEquals(ConnectorState.STOPPING, 
connectorNode.getCurrentState());
+            assertEquals(ControllerServiceState.ENABLED, 
getManagedControllerService(connectorNode).getState());
+
+            processor.setAllowSleepInterrupt(true);
+            processorNode.getProcessGroup().terminateProcessor(processorNode);
+            stopFuture.get(5, TimeUnit.SECONDS);
+
+            assertEquals(ConnectorState.STOPPED, 
connectorNode.getCurrentState());
+            assertEquals(ScheduledState.STOPPED, 
processorNode.getPhysicalScheduledState());
+            assertEquals(ControllerServiceState.DISABLED, 
getManagedControllerService(connectorNode).getState());
+        } finally {
+            processor.setAllowSleepInterrupt(true);
+        }
+    }
+
+    private ConnectorNode initializeControllerServiceEnablingConnector(final 
String enablingBehavior) throws FlowUpdateException {
+        final ConnectorNode connectorNode = 
flowManager.createConnector(ControllerServiceEnablingConnector.class.getName(),
+                "controller-service-enabling-connector", 
SystemBundle.SYSTEM_BUNDLE_COORDINATE, true, true);
+
+        final StepConfiguration stepConfiguration = new 
StepConfiguration(Map.of(
+            ControllerServiceEnablingConnector.ENABLING_BEHAVIOR.getName(), 
new StringLiteralValue(enablingBehavior)));
+
+        final NamedStepConfiguration namedStepConfiguration = new 
NamedStepConfiguration(ControllerServiceEnablingConnector.CONFIGURATION_STEP_NAME,
 stepConfiguration);
+        configure(connectorNode, new 
ConnectorConfiguration(Set.of(namedStepConfiguration)));
+        return connectorNode;
+    }
+
+    private ControllerServiceNode getManagedControllerService(final 
ConnectorNode connectorNode) {
+        final ProcessGroup managedGroup = 
connectorNode.getActiveFlowContext().getManagedProcessGroup();
+        final Set<ControllerServiceNode> services = 
managedGroup.getControllerServices(true);
+        assertEquals(1, services.size());
+        return services.iterator().next();
+    }
+
+    private ConnectorNode initializeProcessorStartFailureConnector() {
+        final ConnectorNode connectorNode = 
flowManager.createConnector(ProcessorStartFailureConnector.class.getName(),
+            "processor-start-failure-connector", 
SystemBundle.SYSTEM_BUNDLE_COORDINATE, true, true);
+        assertNotNull(connectorNode);
+        return connectorNode;
+    }
+
+    private ProcessorNode getManagedProcessor(final ConnectorNode 
connectorNode) {
+        final ProcessGroup managedGroup = 
connectorNode.getActiveFlowContext().getManagedProcessGroup();
+        return managedGroup.findAllProcessors().stream()
+            .filter(processorNode -> processorNode.getProcessor() instanceof 
FailOnScheduledProcessor)
+            .findFirst()
+            .orElseThrow();
+    }
+
+    private void waitForServiceState(final ControllerServiceNode serviceNode, 
final ControllerServiceState desiredState) throws InterruptedException {
+        final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
+        while (serviceNode.getState() != desiredState && System.nanoTime() < 
deadline) {
+            Thread.sleep(10L);
+        }
+
+        assertEquals(desiredState, serviceNode.getState());
+    }
+
+    private void waitForEnableInvocation(final IntSupplier 
enableInvocationCount, final int expectedCount) throws InterruptedException {
+        final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(15);
+        while (enableInvocationCount.getAsInt() < expectedCount && 
System.nanoTime() < deadline) {
+            Thread.sleep(10L);
+        }
+
+        assertTrue(enableInvocationCount.getAsInt() >= expectedCount);
+    }
+
+    private void waitForProcessorState(final ProcessorNode processorNode, 
final ScheduledState desiredState) throws InterruptedException {
+        final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
+        while (processorNode.getPhysicalScheduledState() != desiredState && 
System.nanoTime() < deadline) {
+            Thread.sleep(10L);
+        }
+
+        assertEquals(desiredState, processorNode.getPhysicalScheduledState());
+    }
+
     @Test
     public void testUpdateProcessorPropertyDataQueued() throws 
FlowUpdateException {
         final ConnectorNode connectorNode = initializeDynamicFlowConnector();
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/processors/CreateDummyFlowFile.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/processors/CreateDummyFlowFile.java
index 98ead292e1b..3e7abd37285 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/processors/CreateDummyFlowFile.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/processors/CreateDummyFlowFile.java
@@ -57,7 +57,7 @@ public class CreateDummyFlowFile extends AbstractProcessor {
 
     @Override
     protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
-        return List.of(TEXT);
+        return List.of(TEXT, COUNT_SERVICE);
     }
 
     @Override
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/services/impl/BlockingEnablingCounterService.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/services/impl/BlockingEnablingCounterService.java
new file mode 100644
index 00000000000..3fd80f01047
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/services/impl/BlockingEnablingCounterService.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.components.connector.services.impl;
+
+import org.apache.nifi.annotation.lifecycle.OnDisabled;
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.components.connector.services.CounterService;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class BlockingEnablingCounterService extends AbstractControllerService 
implements CounterService {
+
+    private final AtomicInteger enableCounter = new AtomicInteger();
+    private final AtomicInteger disableCounter = new AtomicInteger();
+    private final CountDownLatch firstEnableRelease = new CountDownLatch(1);
+    private final CountDownLatch enableInterrupted = new CountDownLatch(1);
+    private final CountDownLatch disableStarted = new CountDownLatch(1);
+    private final CountDownLatch disableRelease = new CountDownLatch(1);
+    private final List<String> lifecycleEvents = new CopyOnWriteArrayList<>();
+    private volatile boolean ignoreEnableInterrupt;
+    private volatile boolean blockDisable;
+
+    @OnEnabled
+    public void onEnabled(final ConfigurationContext context) throws 
InterruptedException {
+        final int invocation = enableCounter.incrementAndGet();
+        lifecycleEvents.add("enable-started-" + invocation);
+        if (invocation == 1) {
+            while (firstEnableRelease.getCount() > 0) {
+                try {
+                    firstEnableRelease.await();
+                } catch (final InterruptedException e) {
+                    enableInterrupted.countDown();
+                    lifecycleEvents.add("enable-interrupted-" + invocation);
+                    if (!ignoreEnableInterrupt) {
+                        throw e;
+                    }
+                }
+            }
+        }
+
+        lifecycleEvents.add("enable-finished-" + invocation);
+    }
+
+    @OnDisabled
+    public void onDisabled(final ConfigurationContext context) throws 
InterruptedException {
+        final int invocation = disableCounter.incrementAndGet();
+        lifecycleEvents.add("disable-started-" + invocation);
+        disableStarted.countDown();
+        if (blockDisable) {
+            disableRelease.await();
+        }
+
+        lifecycleEvents.add("disable-finished-" + invocation);
+    }
+
+    public int enableInvocationCount() {
+        return enableCounter.get();
+    }
+
+    public int disableInvocationCount() {
+        return disableCounter.get();
+    }
+
+    public void setIgnoreEnableInterrupt(final boolean ignoreEnableInterrupt) {
+        this.ignoreEnableInterrupt = ignoreEnableInterrupt;
+    }
+
+    public void setBlockDisable(final boolean blockDisable) {
+        this.blockDisable = blockDisable;
+    }
+
+    public boolean awaitEnableInterrupted(final long timeout, final TimeUnit 
timeUnit) throws InterruptedException {
+        return enableInterrupted.await(timeout, timeUnit);
+    }
+
+    public boolean awaitDisableStarted(final long timeout, final TimeUnit 
timeUnit) throws InterruptedException {
+        return disableStarted.await(timeout, timeUnit);
+    }
+
+    public List<String> getLifecycleEvents() {
+        return List.copyOf(lifecycleEvents);
+    }
+
+    public void releaseFirstEnable() {
+        firstEnableRelease.countDown();
+    }
+
+    public void releaseDisable() {
+        disableRelease.countDown();
+    }
+
+    @Override
+    public long increment() {
+        return 0;
+    }
+
+    @Override
+    public long getCount() {
+        return 0;
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/services/impl/FailingEnablingCounterService.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/services/impl/FailingEnablingCounterService.java
new file mode 100644
index 00000000000..b61c8b1b114
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/services/impl/FailingEnablingCounterService.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.components.connector.services.impl;
+
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.components.connector.services.CounterService;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class FailingEnablingCounterService extends AbstractControllerService 
implements CounterService {
+
+    private final AtomicInteger enableCounter = new AtomicInteger();
+
+    @OnEnabled
+    public void onEnabled(final ConfigurationContext context) {
+        enableCounter.incrementAndGet();
+        throw new IllegalStateException("Configured to always fail 
enablement");
+    }
+
+    public int enableInvocationCount() {
+        return enableCounter.get();
+    }
+
+    @Override
+    public long increment() {
+        return 0;
+    }
+
+    @Override
+    public long getCount() {
+        return 0;
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/TestStandardProcessScheduler.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/TestStandardProcessScheduler.java
index 0b90dc5985d..e45c13857e5 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/TestStandardProcessScheduler.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/TestStandardProcessScheduler.java
@@ -27,6 +27,7 @@ import org.apache.nifi.components.state.StateManagerProvider;
 import org.apache.nifi.components.validation.ValidationStatus;
 import org.apache.nifi.components.validation.ValidationTrigger;
 import org.apache.nifi.components.validation.VerifiableComponentFactory;
+import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.controller.AbstractControllerService;
 import org.apache.nifi.controller.ConfigurationContext;
 import org.apache.nifi.controller.ExtensionBuilder;
@@ -95,13 +96,16 @@ import java.util.List;
 import java.util.Map;
 import java.util.Random;
 import java.util.UUID;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -110,7 +114,9 @@ import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -503,6 +509,36 @@ public class TestStandardProcessScheduler {
         assertEquals(0, ts.disableInvocationCount());
     }
 
+    @Test
+    @Timeout(10)
+    public void testRepeatedDisableWhileEnablingCompletesFutures() throws 
Exception {
+        final StandardProcessScheduler scheduler = createScheduler();
+        final ControllerServiceNode serviceNode = 
flowManager.createControllerService(LongEnablingService.class.getName(),
+                "1", systemBundle.getBundleDetails().getCoordinate(), null, 
false, true, null);
+        final LongEnablingService service = (LongEnablingService) 
serviceNode.getControllerServiceImplementation();
+        service.setLimit(TimeUnit.SECONDS.toMillis(1));
+
+        serviceNode.performValidation();
+        final CompletableFuture<Void> enableFuture = 
scheduler.enableControllerService(serviceNode);
+
+        final long enableInvocationDeadline = System.nanoTime() + 
TimeUnit.SECONDS.toNanos(5);
+        while (service.enableInvocationCount() == 0 && System.nanoTime() < 
enableInvocationDeadline) {
+            Thread.sleep(1L);
+        }
+
+        assertEquals(1, service.enableInvocationCount());
+        assertEquals(ControllerServiceState.ENABLING, serviceNode.getState());
+
+        final CompletableFuture<Void> firstDisableFuture = 
scheduler.disableControllerService(serviceNode);
+        final CompletableFuture<Void> secondDisableFuture = 
scheduler.disableControllerService(serviceNode);
+
+        final ExecutionException enableFailure = 
assertThrows(ExecutionException.class, () -> enableFuture.get(5, 
TimeUnit.SECONDS));
+        assertInstanceOf(CancellationException.class, 
enableFailure.getCause());
+        firstDisableFuture.get(5, TimeUnit.SECONDS);
+        secondDisableFuture.get(5, TimeUnit.SECONDS);
+        assertEquals(ControllerServiceState.DISABLED, serviceNode.getState());
+    }
+
     @Test
     @Timeout(10)
     public void testEnableControllerServiceWithConfigurationContext() throws 
Exception {
@@ -548,6 +584,61 @@ public class TestStandardProcessScheduler {
         assertEquals(ControllerServiceState.ENABLED, serviceNode.getState());
     }
 
+    @Test
+    public void testProcessorStopWaitsForSchedulingAgentUnschedule() throws 
Exception {
+        final CountDownLatch schedulingStarted = new CountDownLatch(1);
+        final Semaphore schedulingRelease = new Semaphore(0);
+        final CountDownLatch unschedulingStarted = new CountDownLatch(1);
+        final Semaphore unschedulingRelease = new Semaphore(0);
+        final SchedulingAgent schedulingAgent = mock(SchedulingAgent.class);
+
+        doAnswer(invocation -> scheduleAgent(true, invocation.getArgument(1), 
schedulingStarted, schedulingRelease)).when(schedulingAgent)
+            .schedule(any(Connectable.class), any(LifecycleState.class));
+        doAnswer(invocation -> scheduleAgent(false, invocation.getArgument(1), 
unschedulingStarted, unschedulingRelease)).when(schedulingAgent)
+            .unschedule(any(Connectable.class), any(LifecycleState.class));
+        scheduler.setSchedulingAgent(SchedulingStrategy.TIMER_DRIVEN, 
schedulingAgent);
+
+        final String identifier = UUID.randomUUID().toString();
+        final Processor processor = new NoOpProcessor();
+        processor.initialize(new 
StandardProcessorInitializationContext(identifier, null, null, null, 
KerberosConfig.NOT_CONFIGURED));
+        final LoggableComponent<Processor> loggableComponent = new 
LoggableComponent<>(processor, systemBundle.getBundleDetails().getCoordinate(), 
null);
+        final ProcessorNode processorNode = new 
StandardProcessorNode(loggableComponent, identifier, new 
StandardValidationContextFactory(serviceProvider), scheduler,
+            serviceProvider, mock(ReloadComponent.class), 
mock(VerifiableComponentFactory.class), extensionManager, new 
SynchronousValidationTrigger());
+        rootGroup.addProcessor(processorNode);
+        processorNode.performValidation();
+
+        // Hold the startup task inside SchedulingAgent.schedule() after the 
Processor transitions to RUNNING.
+        scheduler.startProcessor(processorNode, true);
+        assertTrue(schedulingStarted.await(5, TimeUnit.SECONDS));
+
+        // Start the stop sequence while the startup task is still returning 
from the scheduling callback.
+        final CompletableFuture<Void> stopFuture = 
scheduler.stopProcessor(processorNode, 
ProcessorStopLifecycleMethods.TRIGGER_ALL);
+
+        try {
+            // Allow startup to return, then hold the stop task inside 
SchedulingAgent.unschedule().
+            schedulingRelease.release();
+            assertTrue(unschedulingStarted.await(5, TimeUnit.SECONDS));
+
+            // The Processor cannot report that it is stopped while 
unscheduling is still running.
+            assertFalse(stopFuture.isDone());
+        } finally {
+            schedulingRelease.release();
+            unschedulingRelease.release();
+        }
+
+        // Releasing the unschedule callback allows the normal stop sequence 
to finish.
+        stopFuture.get(5, TimeUnit.SECONDS);
+        assertEquals(ScheduledState.STOPPED, 
processorNode.getPhysicalScheduledState());
+    }
+
+    private Void scheduleAgent(final boolean scheduled, final LifecycleState 
lifecycleState,
+            final CountDownLatch callbackStarted, final Semaphore 
callbackRelease) {
+        lifecycleState.setScheduled(scheduled);
+        callbackStarted.countDown();
+        callbackRelease.acquireUninterruptibly();
+        return null;
+    }
+
     // Test that if processor throws Exception in @OnScheduled, it keeps 
getting scheduled
     @Test
     @Timeout(10)
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/processors/FailOnScheduledProcessor.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/processors/FailOnScheduledProcessor.java
index 601b600ccc3..6590499c5f9 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/processors/FailOnScheduledProcessor.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/processors/FailOnScheduledProcessor.java
@@ -18,22 +18,33 @@
 package org.apache.nifi.controller.scheduling.processors;
 
 import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.connector.services.CounterService;
 import org.apache.nifi.processor.AbstractProcessor;
 import org.apache.nifi.processor.ProcessContext;
 import org.apache.nifi.processor.ProcessSession;
 import org.apache.nifi.processor.exception.ProcessException;
 
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
 public class FailOnScheduledProcessor extends AbstractProcessor {
 
-    private volatile int invocationCount = 0;
+    public static final PropertyDescriptor MANAGED_SERVICE = new 
PropertyDescriptor.Builder()
+        .name("Managed Service")
+        .description("Managed Controller Service used to verify lifecycle 
ordering")
+        .identifiesControllerService(CounterService.class)
+        .required(false)
+        .build();
+
+    private final AtomicInteger invocationCount = new AtomicInteger();
+    private final AtomicBoolean succeeded = new AtomicBoolean();
     private volatile int desiredFailureCount = 1;
     private volatile long onScheduledSleepMillis = 0L;
     private volatile int onScheduledSleepIterations = 0;
     private volatile boolean allowSleepInterrupt = true;
-    private final AtomicBoolean succeeded = new AtomicBoolean();
 
     public void setDesiredFailureCount(final int desiredFailureCount) {
         this.desiredFailureCount = desiredFailureCount;
@@ -51,26 +62,24 @@ public class FailOnScheduledProcessor extends 
AbstractProcessor {
 
     @OnScheduled
     public void onScheduled() throws InterruptedException {
-        invocationCount++;
+        final int invocation = invocationCount.incrementAndGet();
 
-        if (invocationCount <= onScheduledSleepIterations && 
onScheduledSleepMillis > 0L) {
+        if (invocation <= onScheduledSleepIterations && onScheduledSleepMillis 
> 0L) {
             final long sleepFinish = System.currentTimeMillis() + 
onScheduledSleepMillis;
 
             while (System.currentTimeMillis() < sleepFinish) {
                 try {
                     Thread.sleep(Math.max(0, sleepFinish - 
System.currentTimeMillis()));
-                } catch (final InterruptedException ie) {
+                } catch (final InterruptedException e) {
                     if (allowSleepInterrupt) {
                         Thread.currentThread().interrupt();
-                        throw ie;
-                    } else {
-                        continue;
+                        throw e;
                     }
                 }
             }
         }
 
-        if (invocationCount < desiredFailureCount) {
+        if (invocation < desiredFailureCount) {
             throw new ProcessException("Intentional failure for unit test");
         } else {
             succeeded.set(true);
@@ -78,7 +87,7 @@ public class FailOnScheduledProcessor extends 
AbstractProcessor {
     }
 
     public int getOnScheduledInvocationCount() {
-        return invocationCount;
+        return invocationCount.get();
     }
 
     public boolean isSucceeded() {
@@ -86,6 +95,11 @@ public class FailOnScheduledProcessor extends 
AbstractProcessor {
     }
 
     @Override
-    public void onTrigger(ProcessContext context, ProcessSession session) 
throws ProcessException {
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return List.of(MANAGED_SERVICE);
+    }
+
+    @Override
+    public void onTrigger(final ProcessContext context, final ProcessSession 
session) throws ProcessException {
     }
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/TestStandardControllerServiceProvider.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/TestStandardControllerServiceProvider.java
index 41664bae145..371e8edd05d 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/TestStandardControllerServiceProvider.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/TestStandardControllerServiceProvider.java
@@ -71,7 +71,10 @@ import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -94,6 +97,7 @@ public class TestStandardControllerServiceProvider {
     private static ExtensionDiscoveringManager extensionManager;
     private static Bundle systemBundle;
     private FlowManager flowManager;
+    private ReloadComponent reloadComponent;
 
     @BeforeAll
     public static void setNiFiProps() {
@@ -110,6 +114,7 @@ public class TestStandardControllerServiceProvider {
     @BeforeEach
     public void setup() {
         flowManager = mock(FlowManager.class);
+        reloadComponent = mock(ReloadComponent.class);
 
         final ConcurrentMap<String, ProcessorNode> processorMap = new 
ConcurrentHashMap<>();
         doAnswer((Answer<ProcessorNode>) invocation -> {
@@ -144,7 +149,7 @@ public class TestStandardControllerServiceProvider {
             .processScheduler(mock(ProcessScheduler.class))
             .nodeTypeProvider(mock(NodeTypeProvider.class))
             .validationTrigger(mock(ValidationTrigger.class))
-            .reloadComponent(mock(ReloadComponent.class))
+            .reloadComponent(reloadComponent)
             .verifiableComponentFactory(mock(VerifiableComponentFactory.class))
             .stateManagerProvider(mock(StateManagerProvider.class))
             .extensionManager(extensionManager)
@@ -172,6 +177,53 @@ public class TestStandardControllerServiceProvider {
         provider.disableControllerService(serviceNode);
     }
 
+    @Test
+    public void testEnableAfterConcurrentDisable() throws Exception {
+        final StandardProcessScheduler scheduler = createScheduler();
+        final StandardControllerServiceProvider provider = new 
StandardControllerServiceProvider(scheduler, null, flowManager, 
extensionManager);
+        final ControllerServiceNode serviceNode = 
createControllerService(ServiceB.class.getName(), "B", 
systemBundle.getBundleDetails().getCoordinate(), provider);
+        final ScheduledExecutorService lifecycleExecutor = 
Executors.newScheduledThreadPool(2);
+        final AtomicReference<CompletableFuture<Void>> disableFutureReference 
= new AtomicReference<>();
+        final AtomicReference<Thread> enableThreadReference = new 
AtomicReference<>();
+
+        // Reloading holds the lifecycle monitor. Start enabling on another 
thread so that disabling can be requested before the monitor is released.
+        doAnswer(invocation -> {
+            final Thread enableThread = new Thread(() -> 
serviceNode.enable(lifecycleExecutor, 10, true), "Controller Service Enable");
+            enableThreadReference.set(enableThread);
+            enableThread.start();
+
+            final long waitDeadline = System.nanoTime() + 
TimeUnit.SECONDS.toNanos(5);
+            while (enableThread.getState() != Thread.State.BLOCKED && 
System.nanoTime() < waitDeadline) {
+                Thread.sleep(10);
+            }
+            assertEquals(Thread.State.BLOCKED, enableThread.getState());
+
+            disableFutureReference.set(serviceNode.disable(lifecycleExecutor));
+            return null;
+        }).when(reloadComponent).reload(any(ControllerServiceNode.class), 
anyString(), any(BundleCoordinate.class), any());
+
+        try {
+            serviceNode.performValidation();
+            assertEquals(ValidationStatus.VALID, 
serviceNode.getValidationStatus(5, TimeUnit.SECONDS));
+
+            serviceNode.reload(Collections.emptySet());
+
+            final Thread enableThread = enableThreadReference.get();
+            enableThread.join(TimeUnit.SECONDS.toMillis(5));
+            assertFalse(enableThread.isAlive());
+            disableFutureReference.get().get(5, TimeUnit.SECONDS);
+
+            // A later enable request must remain effective after the 
concurrent enable and disable operations finish.
+            provider.enableControllerService(serviceNode);
+
+            assertTrue(serviceNode.awaitEnabled(5, TimeUnit.SECONDS));
+            assertEquals(ControllerServiceState.ENABLED, 
serviceNode.getState());
+        } finally {
+            lifecycleExecutor.shutdownNow();
+            scheduler.shutdown();
+        }
+    }
+
     @Test
     @Timeout(10)
     public void testEnableDisableWithReference() {
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/META-INF/services/org.apache.nifi.components.connector.Connector
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/META-INF/services/org.apache.nifi.components.connector.Connector
index 93fb310b0ac..8bd1dfec594 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/META-INF/services/org.apache.nifi.components.connector.Connector
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/META-INF/services/org.apache.nifi.components.connector.Connector
@@ -16,3 +16,5 @@
 org.apache.nifi.controller.flow.NopConnector
 org.apache.nifi.components.connector.DynamicFlowConnector
 org.apache.nifi.components.connector.DynamicAllowableValuesConnector
+org.apache.nifi.components.connector.ControllerServiceEnablingConnector
+org.apache.nifi.components.connector.ProcessorStartFailureConnector
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/META-INF/services/org.apache.nifi.controller.ControllerService
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/META-INF/services/org.apache.nifi.controller.ControllerService
index d9e0a9a8385..692e0763f98 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/META-INF/services/org.apache.nifi.controller.ControllerService
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/META-INF/services/org.apache.nifi.controller.ControllerService
@@ -19,4 +19,6 @@ org.apache.nifi.controller.service.mock.ServiceB
 org.apache.nifi.controller.service.mock.ServiceC
 org.apache.nifi.controller.service.mock.ServiceD
 
-org.apache.nifi.components.connector.services.impl.StandardCounterService
\ No newline at end of file
+org.apache.nifi.components.connector.services.impl.StandardCounterService
+org.apache.nifi.components.connector.services.impl.BlockingEnablingCounterService
+org.apache.nifi.components.connector.services.impl.FailingEnablingCounterService
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/META-INF/services/org.apache.nifi.processor.Processor
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/META-INF/services/org.apache.nifi.processor.Processor
index ff061c32002..3d0e8a7a63f 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/META-INF/services/org.apache.nifi.processor.Processor
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -23,4 +23,5 @@ 
org.apache.nifi.components.connector.processors.TerminateFlowFile
 org.apache.nifi.components.connector.processors.LogFlowFileContents
 org.apache.nifi.components.connector.processors.ExposeFileValues
 org.apache.nifi.components.connector.processors.Sleep
-org.apache.nifi.components.connector.processors.OnPropertyModifiedTracker
\ No newline at end of file
+org.apache.nifi.components.connector.processors.OnPropertyModifiedTracker
+org.apache.nifi.controller.scheduling.processors.FailOnScheduledProcessor
\ No newline at end of file

Reply via email to